GenServer 的正常关闭

Graceful shutdown of GenServer

我用 GenServer 编写了一个 Elixir 应用程序,它在启动时启动外部应用程序并关闭它并在退出时进行其他清理。我在 init/1 callback and cleanup code in the terminate/2 回调中添加了启动功能。

GenServer启动时init代码运行正常,手动发送:stop信号时也会调用terminate方法,但在在 IEx 中意外关闭和中断的情况(如按 Ctrl+C 的情况),不会调用终止代码。


目前,我浏览了大量的论坛帖子、博客文章和文档,包括:

From Elixir Docs - GenServers:

If the GenServer receives an exit signal (that is not :normal) from any process when it is not trapping exits it will exit abruptly with the same reason and so not call terminate/2. Note that a process does NOT trap exits by default and an exit signal is sent when a linked process exits or its node is disconnected.

Therefore it is not guaranteed that terminate/2 is called when a GenServer exits. For such reasons, we usually recommend important clean-up rules to happen in separated processes either by use of monitoring or by links themselves.

但我完全不知道如何获得 :init.stoplinked processes 或其他任何东西(因为这是我第一次使用 GenServers)。


这是我的代码:

defmodule MyAwesomeApp do
  use GenServer

  def start do
    GenServer.start_link(__MODULE__, nil)
  end

  def init(state) do
    # Do Bootup stuff

    IO.puts "Starting: #{inspect(state)}"
    {:ok, state}
  end

  def terminate(reason, state) do
    # Do Shutdown Stuff

    IO.puts "Going Down: #{inspect(state)}"
    :normal
  end
end

MyAwesomeApp.start

我可以向您推荐两种解决方案。

文档中提到了第一个。

Note that a process does NOT trap exits.

您必须让您的生成服务器进程陷阱退出。为此:

Process.flag(:trap_exit, true)

这会使您的进程在退出时调用 terminate/2

但是还有一个解决办法,就是把这个初始化交给上层主管。然后让主管将外部应用程序引用传递给 gen 服务器。但是在这里,您没有类似 terminate 的回调来在必要时退出外部应用程序。当主管停止时,外部应用程序将被杀死。

为了增加调用 terminate 回调的机会,服务器进程应该捕获退出。然而,即便如此,在某些情况下也可能不会调用回调(例如,当进程被残忍地杀死时,或者当它自身崩溃时)。有关详细信息,请参阅 here

如前所述,如果你想礼貌地关闭你的系统,你应该调用 :init.stop,这将递归关闭监督树,导致 terminate 回调被调用。

如您所见,无法捕捉到 BEAM OS 进程从内部突然退出。这是一个自定义的 属性:BEAM 进程突然终止,所以它不能 运行 任何代码(因为它终止了)。因此,如果 BEAM 被粗暴地终止,则不会调用回调。

如果您无条件地想在 BEAM 死机时做某事,您需要从另一个 OS 进程中检测到这一点。我不确定您的确切用例是什么,但假设您对此有强烈需求,那么 运行 在同一台(或另一台)机器上的另一个 BEAM 节点可以在这里工作。然后你可以让一个节点上的一个进程监视另一个节点上的另一个进程,这样即使 BEAM 被残忍地杀死你也可以做出反应。

然而,如果你不需要无条件地 运行 一些清理逻辑,你的生活会更简单,所以考虑 terminate 中的代码是必须的,还是更好的-有。

如果您尝试让它在 iexProcess.flag(:trap_exit, true) 中工作但不起作用,请确保您使用的是 GenServer.start 而不是 GenServer.start_link,否则 shell 进程将崩溃,陷阱无关紧要。

这是一个例子:

defmodule Server do

    use GenServer
    require Logger

    def start() do
      GenServer.start(__MODULE__, [], [])
    end


    def init(_) do
      Logger.info "starting"
      Process.flag(:trap_exit, true) # your trap_exit call should be here
      {:ok, :some_state}
    end

    # handle the trapped exit call
    def handle_info({:EXIT, _from, reason}, state) do
      Logger.info "exiting"
      cleanup(reason, state)
      {:stop, reason, state} # see GenServer docs for other return types
    end

    # handle termination
    def terminate(reason, state) do
      Logger.info "terminating"
      cleanup(reason, state)
      state
    end

    defp cleanup(_reason, _state) do
      # Cleanup whatever you need cleaned up
    end

end

在 iex 中,您现在应该会看到一个陷阱退出调用

iex> {:ok, pid} = Server.start()
iex> Process.exit(pid, :something_bad)