gen_server模块中的异常处理最佳实践

时间:2015-07-10 15:55:06

标签: exception-handling erlang

我刚刚开始学习Erlang,这是我测试项目中的一个模块。我这样做是为了让我能更好地了解监督树的工作原理,练习快速失败的代码和一些编程最佳实践。

udp_listener进程侦听UDP消息。它的作用是侦听来自网络中其他主机的通信请求,并使用UDP消息中定义的端口号通过TCP与它们联系。

每次套接字收到UDP消息时都会调用handle_info(...)函数,它会解码UDP消息并将其传递给tcp_client进程。

根据我的理解,我的代码中唯一的失败点是decode_udp_message(Data)handle_info(...)内被调用。

当此函数失败时,整个udp_listener进程是否重新启动?我应该阻止这种情况发生吗?

handle_info(...)函数不应该在不影响udp_listener进程的情况下自行死亡吗?

如何在decode_udp_message(Data)上记录例外?我想在主机上注册,这是失败的消息。

-module(udp_listener).
-behaviour(gen_server).
-export([init/1, handle_call/3, handle_cast/2, 
         handle_info/2, terminate/2, code_change/3]).

%% ====================================================================
%% API functions
%% ====================================================================

-export([start_link/1]).

start_link(Port) ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, Port, []).

%% ====================================================================
%% Behavioural functions 
%% ====================================================================

%% init/1
%% ====================================================================
-spec init(Port :: non_neg_integer()) -> Result when
    Result :: {ok, Socket :: port()}
            | {stop, Reason :: term()}.
%% ====================================================================
init(Port) ->
    SocketTuple = gen_udp:open(Port, [binary, {active, true}]),
    case SocketTuple of
        {ok, Socket}        -> {ok, Socket};
        {error, eaddrinuse} -> {stop, udp_port_in_use};
        {error, Reason}     -> {stop, Reason}
    end.

% Handles "!" messages from the socket
handle_info({udp, Socket, Host, _Port, Data}, State) -> Socket = State,
    handle_ping(Host, Data),
    {noreply, Socket}.

terminate(_Reason, State) -> Socket = State,
    gen_udp:close(Socket).

handle_cast(_Request, State)        -> {noreply, State}.
handle_call(_Request, _From, State) -> {noreply, State}.
code_change(_OldVsn, State, _Extra) -> {ok, State}.

%% ====================================================================
%% Internal functions
%% ====================================================================

handle_ping(Host, Data) ->
    PortNumber = decode_udp_message(Data),
    contact_host(Host, PortNumber).

decode_udp_message(Data) when is_binary(Data) ->
    % First 16 bits == Port number
    <<PortNumber:16>> = Data,
    PortNumber.

contact_host(Host, PortNumber) ->
    tcp_client:connect(Host, PortNumber).

结果

我已根据您的回答更改了我的代码,decode_udp_message已消失,因为handle_ping执行了我需要的操作。

handle_ping(Host, <<PortNumber:16>>) ->
    contact_host(Host, PortNumber);

handle_ping(Host, Data) ->
    %% Here I'll log the invalid datagrams but the process won't be restarted

我喜欢现在的方式,通过添加以下代码,我可以在将来处理协议更改,而不会失去与旧服务器的向后兼容性:

handle_ping(Host, <<PortNumber:16, Foo:8, Bar:32>>) ->
    contact_host(Host, PortNumber, Foo, Bar);
handle_ping(Host, <<PortNumber:16>>) ->
    ...

@塞缪尔-Rivas的

tcp_client是另一个拥有自己主管的gen_server,它将处理自己的失败。

-> Socket = State现在只出现在terminate函数中。 gen_udp:close(Socket).在眼睛上更容易。

2 个答案:

答案 0 :(得分:4)

我认为&#34;让它崩溃&#34;经常被误解为&#34;不处理错误&#34; (一个更强大和更奇怪的建议)。你问题的答案(&#34;我是否应该处理错误&#34;)是&#34;它取决于&#34;。

错误处理的一个问题是用户体验。您永远不会想要向您的用户抛出堆栈跟踪监督树。正如塞缪尔·里瓦斯所指出的那样,另一个问题是,从一个崩溃的过程进行调试可能会很痛苦(特别是初学者)。

Erlang的设计倾向于使用非本地客户端的服务器。在这种架构中,客户端必须能够处理服务器突然变得不可用(当您点击SO上的&#34; post&#34;按钮时,您的wifi连接只会 ),服务器必须能够处理来自客户的突然辍学。在这种情况下,我会翻译&#34;让它崩溃&#34; as&#34;因为所有各方都可以处理服务器消失并返回,为什么不将它用作错误处理程序?而不是编写大量代码行来从所有边缘情况中恢复(然后仍然缺少一些),只需删除所有连接并返回已知良好状态。&#34;

&#34;它取决于&#34;进来了也许知道谁发送了坏数据报对你来说真的很重要(因为你也在编写客户端)。也许客户总是想要回复(希望不是UDP)。

就个人而言,我首先编写&#34;成功路径&#34;,其中既包括成功的成功,也包括我想向客户展示的错误。我没有想到的或客户不需要了解的所有内容都会在重新启动过程中处理。

答案 1 :(得分:3)

您的decode_message不是唯一的失败点。 contact_host很可能也会失败,但您要么忽略错误元组,要么在tcp_client实施中处理失败。

除此之外,只要管理员使用正确的策略启动udp_listener,您就可以使用错误处理方法。如果Data不完全是16位,则匹配将失败,并且进程将因badmatch异常而崩溃。然后主管将开始一个新的。

许多在线风格指南都会宣传这种风格。我认为他们错了。即使在那里失败就是你想要的,但这并不意味着你不能提供比坏匹配更好的理由。所以我会在那里写一些更好的错误处理。通常,我会抛出一个信息丰富的元组,但对于gen服务器来说这很棘手,因为它们将每个调用都包裹在catch内,这会将抛出变为有效值。这是不幸的,但这是一个其他长期解释的话题,所以出于实际目的,我会在这里抛出错误。第三种选择只是使用错误元组({ok, Blah} | {error, Reason}),但这很快就会变得复杂。使用哪个选项也是一个长期解释/辩论的主题,所以现在我只是继续我自己的方法。

回到你的代码,如果你想要正确和提供信息的错误管理,我会用decode_udp_message函数在这一行中做一些事情(保留你当前的语义,看看这个回复的结尾,因为我认为他们不是你想要的):

decode_udp_message(<<PortNumber:16>>) ->
    PortNumber;
decode_udp_message(Ohter) ->
    %% You could log here if you want or live with the crash message if that is good enough for you
    erlang:error({invalid_udp_message, {length, byte_size(Other)}}).

正如您所说,这将采用整个UDP连接。如果进程由主管重新启动,那么它将重新连接(除非您使用reuseaddr sockopt,否则可能会导致问题)。除非你计划每秒多次失败并打开连接成为负担,否则这将没有问题。如果是这种情况,您有几种选择。

  • 假设您可以控制所有故障点并在那里处理错误而不会崩溃。例如,在这种情况下,您可以忽略格式错误的消息。这在像这样的简单场景中可能很好,但是不安全,因为很容易忽略故障点。
  • 分离您希望保持容错的问题。在这种情况下,我将有一个进程来保持连接,另一个进程来解码消息。对于后者,您可以使用&#34;解码服务器&#34;或根据您的偏好和您期望的负载产生一条消息。

要点:

  • 一旦您的代码发现正常行为之外的某些内容失败是个好主意,但请记住使用主管来恢复功能
  • 只是让它崩溃,在我的经验中是一种不好的做法,你应该努力寻找明确的错误原因,这将使你的系统成长时的生活更轻松
  • 如果您不希望一个系统受到故障/重启的影响,只需要生成关闭流程来处理您想要隔离的复杂性,
  • 进程就是隔离故障恢复范围的工具
  • 有时性能会受到妨碍,您需要妥协并处理错误,而不是让流程崩溃,但像往常一样,避免在这种意义上过早优化

有关与错误处理无关的代码的一些注意事项:

  • 您在decode_udp_message中的评论似乎暗示您要解析第一个 16位,但实际上您强迫Data完全 16位。
  • 在你的一些调用中,你会做-> Socket = State这样的事情,缩进可能是坏的风格,而且变量的重命名也有些不必要。您可以在函数头中更改State的{​​{1}},或者,如果您想明确说明您的状态是套接字,请将您的函数头写为Socket