概述
GenServer的terminate callback在进程退出时会被调用.
但若没有:erlang.process_flag(:trap_exit, true), 进程可能被悄无声息地kill掉, 而不走terminate回调.
terminate 被调用的几种情况
gen_server定义了6个callback接口:
init/1
handle_call/3
handle_cast/2
handle_info/2
terminate/2
code_change/3
对于callback的实现者来说,理解callback函数的触发点是最重要的,本文只讨论terminate的调用。
terminate的被调用有如下几种情况:
- handle_call返回 {stop, Reason, Reply, StateN} -> terminate(Reason, StateN)
- handle_info handle_cast返回 {stop, Reason, StateN} -> terminate(Reason, StateN)
- handle_call handle_info handle_cast 抛出异常(在gen_server.erl中被捕获为{‘EXIT’, What}) -> terminate(What, State)
- handle_call handle_info handle_cast 返回值非法 -> terminate({bad_return_value, Reply}, State)
- 收到来自Parent的{‘EXIT’, Parent, Reason}消息:
设置了一个timeout或者无限等待的情况下,supervisor是通过exit(Pid, shutdown)来通知子进程退出的,所以,若supervisor下的gen_server worker进程没有设为系统进程,worker进程不会收到来自Parent的Exit消息,故terminate不会被调用。
示例
App.Supervisor下有一个App.Worker, 代码如下.
defmodule App do
use Application
require Logger
def start(_type, _args) do
children = [
%{id: App.Supervisor, start: {App.Supervisor, :start_link, []}, type: :supervisor}
]
opts = [strategy: :one_for_one, name: __MODULE__]
Supervisor.start_link(children, opts)
end
end
defmodule App.Supervisor do
use Supervisor
require Logger
def start_link() do
Logger.info("app.sup start")
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
children = [
%{id: App.Worker, start: {App.Worker, :start_link, []}, type: :worker}
]
opts = [strategy: :one_for_one, name: __MODULE__]
Supervisor.init(children, opts)
end
end
defmodule App.Worker do
use GenServer
require Logger
require Record
Record.defrecordp(:state, [])
def start_link() do
Logger.info("app.worker start")
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
# :erlang.process_flag(:trap_exit, true)
{:ok, nil}
end
def handle_call(:stop, _from, state) do
{:stop, nil, state}
end
def handle_call(:raise, _from, state) do
raise RuntimeError
{:reply, nil, state}
end
def terminate(reason, state) do
Logger.warn("app.worker terminate #{inspect(reason)} #{inspect(state)}")
:ok
end
end
没有trap_exit
若没有:erlang.process_flag(:trap_exit, true).
- Supervisor.stop(App.Supervisor)
- Supervisor.terminate_child(App.Supervisor, App.Worker)
- Process.exit(:erlang.whereis(App.Worker), :normal)
均不会触发terminate回调.
- GenServer.call(App.Worker, :stop)
- GenServer.call(App.Worker, :raise)
会触发terminate回调.
有trap_exit
- Supervisor.stop(App.Supervisor)
- Supervisor.terminate_child(App.Supervisor, App.Worker)
会触发terminate回调.
20:25:00.568 [warn] app.worker terminate :shutdown nil
- Process.exit(:erlang.whereis(App.Worker), :normal)
会收到{:EXIT, #PID<0.149.0>, :normal}消息, 若处理了消息, 不会触发terminate回调.
20:25:59.849 [warn] unknown_info:{:EXIT, #PID<0.134.0>, :normal}
- Process.exit(:erlang.whereis(App.Worker), :shutdown)
20:27:55.170 [warn] unknown_info:{:EXIT, #PID<0.134.0>, :shutdown}
- GenServer.call(App.Worker, :stop)
- GenServer.call(App.Worker, :raise)
会触发terminate回调.
总结
terminate被调用的方式有如下几种:
- 返回值触发
- 代码错误抛出异常被捕获
- 收到来自系统的Exit消息
如果希望gen_server进程崩溃时terminate一定被调用到(exit(Pid, kill)除外),设为system进程即可。
本篇文章部分内容来自于多年前写的blog, 当时未记录源码内容.