Exception handling gen_服务器模块内部的异常处理最佳实践

Exception handling gen_服务器模块内部的异常处理最佳实践,exception-handling,erlang,Exception Handling,Erlang,我刚开始学习Erlang,这是我的一个测试项目中的一个模块。我这样做是为了更好地理解监督树的工作原理,练习快速失败代码和一些编程最佳实践 udp_侦听器进程侦听udp消息。它的作用是侦听来自网络中其他主机的通信请求,并使用UDP消息中定义的端口号通过TCP与它们联系 每当套接字接收到UDP消息时,就会调用handle\u info(…)函数,它对UDP消息进行解码并将其传递给tcp\u客户端进程 据我所知,我的代码中唯一的失败点是在句柄信息(…)中调用的解码udp\u消息(数据) 当此功能失败时

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

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

每当套接字接收到UDP消息时,就会调用
handle\u info(…)
函数,它对UDP消息进行解码并将其传递给
tcp\u客户端
进程

据我所知,我的代码中唯一的失败点是在
句柄信息(…)
中调用的
解码udp\u消息(数据)

当此功能失败时,整个
udp\u侦听器
进程是否重新启动?我应该阻止这种事情发生吗

难道不仅仅是
handle\u info(…)
函数不应该在不影响
udp\u侦听器的情况下悄无声息地消失吗

如何在
解码udp\u消息(数据)
上记录异常?我想注册某个地方的主机,它的失败消息

-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).
我喜欢现在的方式,通过添加以下代码,我可以在未来处理协议更改,而不会丢失与旧服务器的向后兼容性:

handle_ping(Host, <<PortNumber:16, Foo:8, Bar:32>>) ->
    contact_host(Host, PortNumber, Foo, Bar);
handle_ping(Host, <<PortNumber:16>>) ->
    ...
handle\u ping(主机,)->
联系_主机(主机、端口号、Foo、Bar);
句柄\u ping(主机,)->
...
@塞缪尔·里瓦斯

tcp_客户端
是另一个拥有自己的管理器的gen_服务器,它将处理自己的故障


->Socket=State
现在只出现在
终止
功能中
gen_udp:close(Socket)。
更容易吸引眼球

您的
解码信息
不是唯一的故障点<代码>联系\u主机
很可能也会失败,但您要么忽略错误元组,要么在
tcp\u客户端
实现中处理该故障

除此之外,如果您的
udp\u侦听器是由具有正确策略的主管启动的,那么您的错误处理方法将起作用。如果
数据
不完全是16位,则匹配将失败,进程将崩溃,出现
错误匹配
异常。然后主管将开始一个新的


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

回到您的代码,如果您想要正确且信息丰富的错误管理,我将使用
decode\u udp\u message
函数在这几行中做一些事情(保留您当前的语义,请参见本响应的末尾,因为我认为它们不是您想要的):

解码udp\u消息()->
端口号;
解码udp消息(Ohter)->
%%如果你愿意,你可以在这里登录,如果对你来说足够好的话,你也可以带着崩溃消息生活
erlang:错误({invalid_udp_message,{length,byte_size(Other)}})。
正如您所说,这将占用整个UDP连接。如果进程由主管重新启动,则它将重新连接(这可能会导致问题,除非您使用
reuseaddr
sockopt)。除非您计划每秒失败很多次,并且打开连接成为一种负担,否则这将很好。如果是这样,你有几个选择

  • 假设您可以控制所有的故障点并在那里处理错误而不会崩溃。例如,在此场景中,您可以忽略格式错误的消息。这在这样的简单场景中可能很好,但不安全,因为很容易忽略故障点
  • 将要保持容错的关注点分开。在这种情况下,我将有一个进程来保存连接,另一个进程来解码消息。对于后者,您可以使用“解码服务器”,或者根据您的首选项和预期负载为每条消息生成一个
总结:

  • 一旦代码发现超出正常行为范围的内容,就立即失败是一个好主意,但请记住使用管理器来恢复功能
  • 就我的经验而言,让它崩溃是一种不好的做法,您应该努力寻找明确的错误原因,当您的系统增长时,这将使您的生活更加轻松
  • 流程是隔离故障恢复范围的工具,如果您不希望一个系统受到故障/重新启动的影响,只需生成流程来处理要隔离的复杂性
  • 有时,性能会受到影响,您需要在适当的位置妥协并处理错误,而不是让流程崩溃,但像往常一样,在这个意义上避免过早优化
关于与错误处理无关的代码的一些注释:

  • 您在
    decode\u udp\u消息
    中的注释似乎暗示您希望解析前16位,但实际上您强制
    数据
    正好为16位
  • 在某些调用中,您执行类似于
    ->Socket=State
    的操作,这种缩进可能是不好的样式,而且变量的重命名有些不必要
    handle_ping(Host, <<PortNumber:16, Foo:8, Bar:32>>) ->
        contact_host(Host, PortNumber, Foo, Bar);
    handle_ping(Host, <<PortNumber:16>>) ->
        ...
    
    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)}}).