when erlang gen-server's terminate is call

概述

GenServer的terminate callback在進程退出時會被調用.
但若沒有:erlang.process_flag(:trap_exit, true), 進程可能被悄無聲息地kill掉, 而不走terminate回調.git

terminate 被調用的幾種狀況

gen_server定義了6個callback接口:github

init/1
handle_call/3
handle_cast/2
handle_info/2
terminate/2
code_change/3

對於callback的實現者來講,理解callback函數的觸發點是最重要的,本文只討論terminate的調用。app

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不會被調用。ui

示例

App.Supervisor下有一個App.Worker, 代碼以下.code

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).orm

  • Supervisor.stop(App.Supervisor)
  • Supervisor.terminate_child(App.Supervisor, App.Worker)
  • Process.exit(:erlang.whereis(App.Worker), :normal)

均不會觸發terminate回調.server

  • GenServer.call(App.Worker, :stop)
  • GenServer.call(App.Worker, :raise)

會觸發terminate回調.blog

有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, 當時未記錄源碼內容.

相關文章
相關標籤/搜索