昨天SC遇到一个问题,创建一个gen_server的时候会在init方法中检查依赖的外部服务是否可用;如果不可用的话他就直接返回{stop,Reason},gen_server进程创建失败;这个目标他很容易就达到了,但是进程启动失败之后却创建了Crash Report,这种异常情况是可以预料的并不需要创建崩溃报告Crash Report;为什么会产生崩溃报告Crash Report?如何消除呢?他的init代码大体上是这样的:
init([]) ->
process_flag(trap_exit, true),
case is_service_available() of
{ok, Sock} ->
{ok, #tcp_connector_state{sock = Sock}};
{error, Reason} ->
?Error("service not available:~p~n", [Reason]),
{stop,Reason}
end.
首先要确认的是gen_server对init回调函数的返回值规格说明, 官方文档链接:http://www.erlang.org/doc/man/gen_server.html
Module:init(Args) -> Result
Types:
Args = term()
Result = {ok,State} | {ok,State,Timeout} | {ok,State,hibernate}
| {stop,Reason} | ignore
State = term()
Timeout = int()>=0 | infinity
Reason = term()
并且下面的解释也说了如果启动中遇到错误应该返回{stop,Reason}:
If something goes wrong during the initialization the function should return {stop,Reason} where Reason is any term, or ignore.
同时查看gen_server对start_link/start方法的注解:
If Module:init/1 fails with Reason, the function returns {error,Reason}. If Module:init/1 returns {stop,Reason} or ignore, the process is terminated and the function returns {error,Reason} or ignore,respectively.
貌似也没有什么问题,下面最直接的方法就是去看看gen_server代码对init的各种返回值的处理了:
%%% ---------------------------------------------------
%%% Initiate the new process.
%%% Register the name using the Rfunc function
%%% Calls the Mod:init/Args function.
%%% Finally an acknowledge is sent to Parent and the main
%%% loop is entered.
%%% ---------------------------------------------------
init_it(Starter, self, Name, Mod, Args, Options) ->
init_it(Starter, self(), Name, Mod, Args, Options);
init_it(Starter, Parent, Name0, Mod, Args, Options) ->
Name = name(Name0),
Debug = debug_options(Name, Options),
case catch Mod:init(Args) of
{ok, State} ->
proc_lib:init_ack(Starter, {ok, self()}),
loop(Parent, Name, State, Mod, infinity, Debug);
{ok, State, Timeout} ->
proc_lib:init_ack(Starter, {ok, self()}),
loop(Parent, Name, State, Mod, Timeout, Debug);
{stop, Reason} ->
%% For consistency, we must make sure that the
%% registered name (if any) is unregistered before
%% the parent process is notified about the failure.
%% (Otherwise, the parent process could get
%% an 'already_started' error if it immediately
%% tried starting the process again.)
unregister_name(Name0),
proc_lib:init_ack(Starter, {error, Reason}),
exit(Reason);
ignore ->
unregister_name(Name0),
proc_lib:init_ack(Starter, ignore),
exit(normal);
{'EXIT', Reason} ->
unregister_name(Name0),
proc_lib:init_ack(Starter, {error, Reason}),
exit(Reason);
Else ->
Error = {bad_return_value, Else},
proc_lib:init_ack(Starter, {error, Error}),
exit(Error)
end.
看到这里一切都明朗了,当Mod:init(Args)返回值是 {stop, Reason} 的时候在完成清理工作之后最后一步做了exit(Reason);这样退出又会怎样呢?注意gen_server创建进程是使用proc_lib创建的,而使用proc_lib创建的进程退出的原因不是normal也不是shutdown的时候,就会创建一个进程崩溃报告,这个会写入默认的SASL的事件handler,错误报告会在只有在启动了SASL的时候才能看到.这个在之前的文章中[Erlang 0017]Erlang/OTP基础模块 proc_lib 中我们已经提到提到过. proc_lib的官方文档:http://www.erlang.org/doc/man/proc_lib.html
原因知道了,解决方法也就很显然了,从上面的代码中可以看到当Mod:init返回值是ignore的时候处理方式是exit(normal);而进程这样退出是不会创建Crash Report的,搞定:
init([]) ->
process_flag(trap_exit, true),
case is_service_available() of
{ok, Sock} ->
{ok, #tcp_connector_state{sock = Sock}};
{error, Reason} ->
?Error("service not available:~p~n", [Reason]),
ignore
end.
还没有结束
这个问题比较简单,但是思考解决方案的过程还是做一下调整的,可能是更直接:
- 首先我们的目标是不想生成崩溃报告Crash Report,那崩溃报告是谁产生的呢?
- 这是一个gen_server进程,它创建使用的是proc_lib
- 而proc_lib创建的进程只有非正常退出的时候才会创建Crash Report,换句话说退出的原因不是normal或者shutdown
- 看一下gen_server对init回调函数的处理,哪一种退出是不exit(normal)
OK.