在线文档教程

GenServer

GenServer行为

用于实现客户端 - 服务器关系的服务器的行为模块。

GenServer是一个像任何其他Elixir进程一样的进程,它可以用来保持状态,异步执行代码等等。使用使用此模块实现的通用服务器进程(GenServer)的优点是,它将具有一组标准接口函数,并包含用于跟踪和错误报告的功能。它也适合于监督树。

GenServer行为抽象出了常见的客户机-服务器交互。开发人员只需要实现他们感兴趣的回调和功能。

让我们从一个代码示例开始,然后探索可用的回调。假设我们想要一个像堆栈一样工作的GenServer,允许我们推送和弹出条目:

defmodule Stack do use GenServer # Callbacks def handle_call(:pop, _from, [h | t]) do {:reply, h, t} end def handle_cast{:push, item}, state) do {:noreply, [item | state]} end end # Start the server {:ok, pid} = GenServer.start_link(Stack, [:hello]) # This is the client GenServer.call(pid, :pop) #=> :hello GenServer.cast(pid, {:push, :world}) #=> :ok GenServer.call(pid, :pop) #=> :world

我们通过调用start_link / 3来启动我们的堆栈,将模块传递给服务器实现和它的初始参数(一个表示包含该项目的堆栈的列表:hello)。 我们主要通过发送两种类型的消息来与服务器进行交互。 呼叫消息期望来自服务器的回复(因此是同步的),而投射消息则不会。

每次执行GenServer.call/3时,客户端都会发送一条必须由GenServer中的handle_call / 3回调处理的消息。 cast / 2消息必须由handle_cast / 2处理。

使用GenServer和回调

中需要实现6个回调。GenServer...通过添加use GenServer对于您的模块,Elixir将自动为您定义所有6个回调,由您来实现您想要自定义的回调。

use GenServer还定义了child_spec/1函数,允许将定义的模块置于监视树下。生成child_spec/1可以使用以下选项进行自定义:

  • :id-子规范id,不符合当前模块

  • :start- 如何启动子进程(默认为调用__MODULE__.start_link/1

  • :restart - 当子程序应该重新启动时,默认为 :permanent

  • :shutdown-如何关闭子程序

例如:

use GenServer, restart: :transient, shutdown: 10_000

Supervisor有关更多信息的文档。

名称登记

start_link / 3和start / 3都支持GenServer通过:name选项在开始时注册名称。 注册名称也会在终止时自动清除。 支持的值是:

  • Atom-GenServer是在本地注册的,其名称为Process.register/2...

  • {:global, term}-使用:global模块...

  • {:via, module, term} - GenServer注册给定的机制和名称。该:via选项需要一个模块,出口register_name/2,unregister_name/1,whereis_name/1和send/2。一个这样的例子是使用这些函数的:global模块,用于保存Elixir节点网络全局可用的进程名称和它们相关的PID名称。Elixir还附带一个本地,分散和可扩展的注册表,Registry用于本地存储动态生成的名称。例如,我们可以启动并注册我们的Stack服务器本地,如下所示:#启动服务器并使用名称MyStack {:ok,_} = GenServer.start_link(Stack,[:hello],name:MyStack)在本地注册#现在消息可以直接发送到MyStack GenServer.call (MyStack,:弹出)#=>:helloOnce服务器启动时,在该模块中的剩余的功能(call/3,cast/2,和朋友)也将接受的原子,或任何:global或:via元组。通常,支持以下格式:

  • 一个 pid

  • 一个atom,如果服务器在本地注册

  • {atom, node}如果服务器在另一个节点上本地注册

  • {:global, term}如果服务器是全局注册的

  • {:via, module, name}如果服务器是通过替代注册表注册的

如果有兴趣在本地注册动态名称,不要使用原子,因为原子永远不会被收集,因此动态生成的原子不会被收集。对于这种情况,您可以使用该Registry模块设置您自己的本地注册表。

客户端/服务器API

虽然在上面的例子中,我们已经使用过GenServer.start_link/3和朋友直接启动并与服务器进行通信,但大多数情况下我们并不GenServer直接调用这些函数。相反,我们将这些调用包装在代表服务器公共API的新函数中。

下面是我们的Stack模块的更好实现:

defmodule Stack do use GenServer # Client def start_link(default) do GenServer.start_link(__MODULE__, default) end def push(pid, item) do GenServer.cast(pid, {:push, item}) end def pop(pid) do GenServer.call(pid, :pop) end # Server (callbacks) def handle_call(:pop, _from, [h | t]) do {:reply, h, t} end def handle_call(request, from, state) do # Call the default implementation from GenServer super(request, from, state) end def handle_cast{:push, item}, state) do {:noreply, [item | state]} end def handle_cast(request, state) do super(request, state) end end

在实践中,同一模块中同时具有服务器和客户端功能是很常见的。如果服务器和/或客户端实现变得越来越复杂,则可能希望将它们放在不同的模块中。

接收“常规”消息

目标GenServer是为开发人员抽象“接收”循环,自动处理系统消息,支持代码更改,同步调用等。因此,您不应该在GenServer回调中调用自己的“接收”,因为这样做会导致GenServer行为不当。

除了所提供的同步和异步通信call/3cast/2,通过这样的功能发送的“正规”的消息Kernel.send/2Process.send_after/4和类似的,可以在内部处理handle_info/2回调。

handle_info/2可以在许多情况下使用,例如处理由Process.monitor/1.另一个用例handle_info/2是执行周期性的工作,在Process.send_after/4*

defmodule MyApp.Periodically do use GenServer def start_link do GenServer.start_link(__MODULE__, %{}) end def init(state) do schedule_work() # Schedule work to be performed on start {:ok, state} end def handle_info(:work, state) do # Do the desired work here schedule_work() # Reschedule once more {:noreply, state} end defp schedule_work() do Process.send_after(self(), :work, 2 * 60 * 60 * 1000) # In 2 hours end end

使用:sys模块进行调试

GenServers作为特殊进程可以使用该:sys模块进行调试。通过各种钩子,这个模块允许开发人员反思进程的状态并跟踪执行过程中发生的系统事件,例如接收到的消息,发送的回复和状态更改。

让我们从:sys模块用于调试:

  • :sys.get_state/2-允许检索进程的状态。对于GenServer进程,它将是回调模块状态,作为最后一个参数传递到回调函数中。

  • :sys.get_status/2 - 允许检索过程的状态。此状态包括进程字典,进程正在运行或挂起,父PID,调试程序状态以及行为模块的状态,其中包括回调模块状态(由返回的状态:sys.get_state/2)。可以通过定义可选的GenServer.format_status/2回调来改变这种状态的表现。

  • :sys.trace/3-将所有系统事件打印到:stdio...

  • :sys.statistics/3-管理进程统计数据的收集。

  • :sys.no_debug/2-关闭给定进程的所有调试处理程序。一旦我们完成调试,关闭调试是非常重要的。过多的调试处理程序或那些应该关闭但没有关闭的处理程序会严重损害系统的性能。

  • :sys.suspend/2-允许挂起一个进程,使它只回复系统消息,而不回复其他消息。挂起的进程可以通过:sys.resume/2...

让我们看看如何使用这些函数来调试前面定义的堆栈服务器。

iex> {:ok, pid} = Stack.start_link([]) iex> :sys.statistics(pid, true) # turn on collecting process statistics iex> :sys.trace(pid, true) # turn on event printing iex> Stack.push(pid, 1) *DBG* <0.122.0> got cast {push,1} *DBG* <0.122.0> new state [1] :ok iex> :sys.get_state(pid) [1] iex> Stack.pop(pid) *DBG* <0.122.0> got call pop from <0.80.0> *DBG* <0.122.0> sent 1 to <0.80.0>, new state [] 1 iex> :sys.statistics(pid, :get) {:ok, [start_time: {{2016, 7, 16}, {12, 29, 41}}, current_time: {{2016, 7, 16}, {12, 29, 50}}, reductions: 117, messages_in: 2, messages_out: 0]} iex> :sys.no_debug(pid) # turn off all debug handlers :ok iex> :sys.get_status(pid) {:status, #PID<0.122.0>, {:module, :gen_server}, [["$initial_call": {Stack, :init, 1}, # pdict "$ancestors": [#PID<0.80.0>, #PID<0.51.0>]], :running, # :running | :suspended #PID<0.80.0>, # parent [], # debugger state [header: 'Status for generic server <0.122.0>', # module status data: [{'Status', :running}, {'Parent', #PID<0.80.0>}, {'Logged events', []}], data: [{'State', [1]}]]]}

了解更多

如果您想了解关于gen server的更多信息,Elixir入门指南提供了类似于教程的介绍。Erlang中的文档和链接还可以提供更多的信息。

  • :gen_server模块文档

摘要

类型

debug()

这些start*函数支持的调试选项

from()

描述调用请求的客户端的元组

name()

GenServer名称

on_start()

start*函数的返回值

option()

属性使用的选项值。start*功能

options()

类使用的选项。start*功能

server()

服务器引用

功能

abcast(nodes \ [node()| Node.list()],name, request)

将本地注册为name在指定的节点上

call(server, request, timeout \ 5000)

server等待它的回复

cast(server, request)

发送异步请求到server

multi_call(nodes \ [node() | Node.list()], name, request, timeout \ :infinity)

调用本地注册为指定节点上名称的所有服务器

reply(client, reply)

对客户的答复

start(module, args, options \ [])

启动一个没有链接的GenServer进程(在监督树之外)

start_link(module, args, options \ [])

开始GenServer链接到当前进程的进程

stop(server, reason \ :normal, timeout \ :infinity)

用给定的reason同步停止服务器

whereis(server)

返回GenServer进程的pid或{name,node},如果没有进程与给定服务器关联,则返回nil

回调

code_change(old_vsn, state, extra)

调用以更改GenServer加载模块的不同版本(热代码交换)和状态的术语结构应该更改的状态

format_status(reason,pdict_and_state)

在某些情况下调用以检索GenServer状态的格式化版本

handle_call(request, from, state)

调用来处理同步call/3消息。call/3将阻塞直到收到答复(除非呼叫超时或节点断开连接)

handle_cast(request, state)

调用以处理异步cast/2讯息

handle_info(msg, state)

调用以处理所有其他消息。

init(args)

在服务器启动时调用。start_link/3start/3将被阻塞,直到它返回。

terminate(reason, state)

当服务器即将退出时调用。它应该做任何必要的清理

类型

terminate(reason, state)

debug() :: [:trace | :log | :statistics | {:log_to_file, Path.t}]

这些start*函数支持的调试选项

from()

from() :: {pid, tag :: term}

描述调用请求的客户端的元组。

pid是调用者的PID值,并且tag用于标识调用的唯一术语。

name()

name() :: atom | {:global, term} | {:via, module, term}

GenServer名称

on_start()

on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}

start*函数的返回值

option()

option :: {:debug, debug} | {:name, name} | {:timeout, timeout} | {:spawn_opt, Process.spawn_opt}

start*功能使用的选项值

options()

options() :: [option]

start*功能使用的选项

server()

server() :: pid | name | {atom, node}

服务器引用

功能

abcast(nodes \ node() | Node.list(), name, request)

abcast([node], name :: atom, term) :: :abcast

将所有在本地注册的服务器都转换为name指定的节点。

此函数立即返回,并忽略不存在的节点或服务器名称不存在的节点。

更多信息请查看multi_call/4

call(server, request, timeout \ 5000)

call(server, term, timeout) :: term

server等待它的答复。

客户端发送给定的request到服务器,并等待到回复到达或超时。handle_call/3将在服务器上调用以处理请求。

server可以是该模块文档的“名称注册”部分中描述的任何值。

超时

timeout是一个大于零的整数,它指定等待答复的毫秒数,或者:infinity无限期等待的原子。默认值是5000。如果在指定的时间内没有收到回复,函数调用失败,调用者退出。如果调用者捕获失败并继续运行,并且服务器刚刚迟到,则它可能会在稍后随时到达调用者的消息队列中。在这种情况下,调用者必须为此做好准备,并放弃任何这样的垃圾消息,这些垃圾消息是引用第一个元素的二元元组。

cast(server, request)

cast(server, term) :: :ok

发送异步请求到server...

无论目标服务器(或节点)是否存在,此函数始终返回:ok。 因此,目标服务器是否成功处理该消息是未知的。

将在服务器上调用handle_cast / 2来处理请求。 如果服务器位于尚未连接到呼叫者的节点上,则呼叫将阻塞,直到发生连接。 这与OTP中的行为不同:gen_server其中消息由另一个进程在这种情况下发送,这可能导致到其他节点的消息不按顺序到达。

multi_call(nodes \ node() | Node.list(), name, request, timeout \ :infinity)

multi_call([node], name :: atom, term, timeout) :: {replies :: [{node, term}], bad_nodes :: [node]}

调用本地注册为name在指定的nodes...

首先,request发送给每个节点nodes; 然后,主叫方等待回复。该函数返回一个两元素元组{replies, bad_nodes},其中:

  • replies- 是一个{node, reply}元组列表,其中node是回复的节点,reply也是它的回复

  • bad_nodes- 是一个不存在的节点列表,或者具有给定的服务器name不存在或没有回复的节点列表

nodes是发送请求的节点名称列表。默认值是所有已知节点(包括此节点)的列表。

为了避免迟到的答案(在超时之后)污染了调用者的消息队列,中间人进程被用来做实际的调用。迟到的答案在到达终止的过程时将被丢弃。

实例

假设Stack的文档中提到的GenServerGenServer模块注册为Stack:"foo@my-machine":"bar@my-machine"节点:

GenServer.multi_call(Stack, :pop) #=> {[{:"foo@my-machine", :hello}, {:"bar@my-machine", :world}], []}

reply(client, reply)

reply(from, term) :: :ok

回复客户。

这个函数可以用来显式地发送一个应答到一个被调用的客户端,call/3或者multi_call/4当不能在返回值中指定应答handle_call/3

client必须是回调from接受的参数(第二个参数)handle_call/3reply是一个任意的术语,将作为呼叫的返回值返回给客户端。

请注意,reply/2可以从任何进程调用,而不仅仅是最初接收该调用的GenServer(只要该GenServer from以某种方式传达该参数)。

此函数总是返回:ok...

实例

def handle_call(:reply_in_one_second, from, state) do Process.send_after(self(), {:reply, from}, 1_000) {:noreply, state} end def handle_info{:reply, from}, state) do GenServer.reply(from, :one_second_has_passed) {:noreply, state} end

start(module, args, options \ [])

start(module, any, options) :: on_start

启动一个GenServer没有链接的过程(在监督树之外)。

start_link/3想了解更多信息。

start_link(module, args, options \ [])

start_link(module, any, options) :: on_start

开始链接到当前进程的GenServer进程。

这通常用于启动GenServer作为监督树的一部分。

一旦服务器启动,init/1给定函数module调用args作为初始化服务器的参数。为了确保同步启动过程,此函数直到init/1已经回来。

请注意,以start_link / 3开头的GenServer链接到父进程,并且将在父进程崩溃的情况下退出。 由于以下原因,GenServer也将退出:如果配置为在init / 1回调中捕获退出,则情况正常。

备选方案

  • :name - 用于模块文档的“名称注册”部分所述的名称注册

  • :timeout-如果存在,则允许服务器花费给定的毫秒初始化时间,否则将被终止,启动函数将返回{:error, :timeout}

  • :debug-如果存在,则在:sys模块被调用

  • :spawn_opt-如果存在,则将其值作为选项传递给基本流程,如Process.spawn/4

返回值

如果服务器成功创建并初始化,则此函数返回{:ok, pid},其中pid是服务器的PID。如果具有指定服务器名称的进程已经存在,则此函数返回{:error, {:already_started, pid}}该进程的PID。

如果init/1回调失败reason,则此函数返回{:error, reason}。否则,如果它返回,{:stop, reason}或者:ignore进程终止并且该函数分别返回{:error, reason}:ignore

stop(server, reason \ :normal, timeout \ :infinity)

stop(server, reason :: term, timeout) :: :ok

用给定的服务器同步停止服务器reason

terminate/2给定的回调server将在退出之前调用。:ok如果服务器以给定的原因终止,则此函数返回; 如果以其他原因结束,则该呼叫退出。

此函数保持OTP有关错误报告的语义。如果原因不是:normal:shutdown{:shutdown, _},将记录错误报告。

whereis(server)

whereis(server) :: pid | {atom, node} | nil

返回pid{name, node}一个GenServer过程中,或者如果没有处理与给定的关联server则返回nil

实例

例如,要查找服务器进程,监视它并向其发送强制转换:

process = GenServer.whereis(server) monitor = Process.monitor(process) GenServer.cast(process, :hello)

回调

code_change(old_vsn, state, extra)

code_change(old_vsn, state :: term, extra :: term) :: {:ok, new_state :: term} | {:error, reason :: term} when old_vsn: term | {:down, term}

被调用来改变GenServer何时加载模块的不同版本(热码交换)的状态以及状态的术语结构应该被改变。

old_vsn@vsn升级时模块的以前版本(由属性定义)。降级先前版本时,将以包含第一个元素的2元组封装:downstate是该状态的当前状态,GenServerextra是更改状态所需的任何额外数据。

返回{:ok,new_state}将状态更改为new_state并且代码更改成功。

返回{:错误,原因}由于原因原因导致代码更改失败,并且状态保持为之前的状态。

如果code_change/3引发代码更改失败,循环将继续其先前的状态。因此,此回调通常不包含副作用。

format_status(reason, pdict_and_state) (optional)

format_status(reason, pdict_and_state :: list) :: term when reason: :normal | :terminate

在某些情况下调用以检索GenServer状态的格式化版本。

此回调可用于控制该状态的外观GenServer。例如,它可以用来返回状态的紧凑表示,GenServer以避免打印大的状态条件。

  • sys.get_status / 1或:sys.get_status / 2之一被调用以获取GenServer的状态; 在这种情况下,原因是:normal

  • GenServer异常终止,并且记录一个错误; 在这种情况下,reason:terminate

pdict_and_state是一个两元素列表[pdict, state],其中pdict{key, value}表示当前进程字典的元组列表,GenServerstate是当前进程的状态GenServer

handle_call(request, from, state)

handle_call(request :: term, from, state :: term) :: {:reply, reply, new_state} | {:reply, reply, new_state, timeout | :hibernate} | {:noreply, new_state} | {:noreply, new_state, timeout | :hibernate} | {:stop, reason, reply, new_state} | {:stop, reason, new_state} when reply: term, new_state: term, reason: term

调用来处理同步call/3消息。call/3将阻塞直到收到答复(除非呼叫超时或节点断开连接)。

请求是由呼叫/ 3发送的请求消息,from是包含呼叫者的PID和唯一标识该呼叫的术语的2元组,并且state是GenServer的当前状态。

返回{:reply,reply,new_state}将响应答复发送给调用方,并以新状态new_state继续循环。

返回{:reply,reply,new_state,timeout}类似于{:reply,reply,new_state},除非handle_info(:timeout,new_state)在超时毫秒后将被调用,如果没有收到消息。

返回{:reply,reply,new_state,:hibernate}类似于{:reply,reply,new_state},只是进程处于休眠状态,并且一旦消息在消息队列中就会继续循环。 如果消息已经在消息队列中,这将立即生效。 休眠一个GenServer会导致垃圾收集,并留下一个连续的堆,最大限度地减少进程使用的内存。

休眠状态不应该积极使用,因为太多的时间可能会花在垃圾收集上。通常情况下,只有在消息预计不会很快的时候才会使用,并且最大限度地减少过程的内存显示为有益。

返回{:noreply, new_state}不会向调用者发送响应,并以新状态继续循环new_state。答复必须与..reply/2.一起发送。

有三个主要的用例用于不使用返回值进行答复:

  • 在从回调返回之前进行答复,因为响应是在调用慢速函数之前已知的。

  • 从回调返回后回复,因为响应还不可用。

  • 从另一个进程(如任务)进行答复。

当从另一个进程回复时,如果另一个进程没有响应而退出,GenServer应该退出,因为调用者将阻止等待回复。

返回{:noreply, new_state, timeout | :hibernate}类似于{:noreply, new_state}除了与:reply元组一样发生超时或休眠。

返回{:stop,reason,reply,new_state}会停止循环并终止/ 2,并以原因原因和状态new_state进行调用。 然后,回复将作为响应发送,并且该流程有理由退出。

返回{:stop, reason, new_state}类似于{:stop, reason, reply, new_state}除了不发送回复外。

如果这个回调没有实现,默认的实现use GenServer将会返回{:stop, {:bad_call, request}, state}

handle_cast(request, state)

handle_cast(request :: term, state :: term) :: {:noreply, new_state} | {:noreply, new_state, timeout | :hibernate} | {:stop, reason :: term, new_state} when new_state: term

调用cast/2来处理异步消息。

请求是cast / 2发送的请求消息,state是GenServer的当前状态。

返回{:noreply, new_state}继续新状态new_state的循环。

返回{:noreply,new_state,timeout}类似于{:noreply,new_state},除非handle_info(:timeout,new_state)将在超时毫秒后被调用,如果没有收到消息。

返回{:noreply,new_state,:hibernate}类似于{:noreply,new_state},除了在继续循环之前进程处于休眠状态。 有关更多信息,请参阅handle_call / 3。

返回{:stop,reason,new_state}会停止循环并终止/ 2,其原因原因和状态为new_state。 该过程有理由退出。

如果此回调未实现,则使用GenServer的默认实现将返回{:stop,{:bad_cast,request},state}。

handle_info(msg, state)

handle_info(msg :: :timeout | term, state :: term) :: {:noreply, new_state} | {:noreply, new_state, timeout | :hibernate} | {:stop, reason :: term, new_state} when new_state: term

调用以处理所有其他消息。

msg是消息,状态是GenServer的当前状态。 发生超时时,消息是:超时。

返回值与handle_cast/2一样

如果这个回调没有实现,默认的实现use GenServer将会返回{:noreply, state}

init(args)

init(args :: term) :: {:ok, state} | {:ok, state, timeout | :hibernate} | :ignore | {:stop, reason :: any} when state: any

在服务器启动时调用。start_link/3start/3会一直阻塞直到它回来。

args是传递给start_link/3的参数项(第二个参数)。

返回{:ok,state}将导致start_link / 3返回{:ok,pid}并且进程进入其循环。

返回{:ok,state,timeout}类似于{:ok,state},除非在超时时间内没有收到消息,否则将在超时毫秒后调用handle_info(:timeout,state)。

返回{:ok,state,:hibernate}类似于{:ok,state},除了进程在进入循环之前休眠。 有关休眠的更多信息,请参阅handle_call / 3。

返回:ignore将导致start_link/3返回:ignore并且进程将正常退出而不进入循环或调用terminate/2。如果在监督树的一部分使用时,父监督员不会无法启动,也不会立即尝试重新启动GenServer。监督树的其余部分将(重新)开始,所以GenServer不应该被其他进程需要。Supervisor.restart_child/2随着子规范保存在父级主管中,可以稍后开始。主要用例如下:

  • 该组件GenServer由配置禁用,但稍后可能会启用。

  • 发生了错误,并且将由不同的机制处理Supervisor。可能这种方法涉及Supervisor.restart_child/2延迟后尝试重新启动。

返回{:stop, reason}将导致start_link/3返回,{:error, reason}并且该流程以合理的方式退出reason而不进入循环或调用terminate/2

terminate(reason, state)

terminate(reason, state :: term) :: term when reason: :normal | :shutdown | {:shutdown, term} | term

当服务器即将退出时调用。它应该做任何需要的清理。

reason是退出原因并且state是当前的状态GenServer。返回值被忽略。

terminate/2如果回调(除外init/1)执行以下操作之一,则会调用它:

  • 返回一个:stop元组

  • 提高

  • 调用 Kernel.exit/1

  • 返回无效值。

  • GenServer陷阱离开(使用Process.flag/2父进程发送一个退出信号

如果监督树的一部分,一个GenServerSupervisor将它关闭时发出的退出信号。退出信号基于孩子规范中的关机策略。如果是:brutal_killGenServer被毁坏,因此terminate/2没有被调用。然而,如果它是一个超时,那么Supervisor将发送退出信号:shutdown,并且GenServer将有超时的持续时间来调用terminate/2- 如果在超时之后进程仍然活着,它将被终止。

如果GenServer在任何进程没有陷入退出时接收到退出信号(不是正常的),它将以相同的原因突然退出,因此不会调用terminate / 2。 请注意,进程不会默认陷阱出口,并且退出信号在链接进程退出或其节点断开连接时发送。

因此不保证terminate/2GenServer退出时被调用。出于这样的原因,我们通常建议重要的清理规则通过使用监控或链接本身在分离的过程中发生。例如,如果GenServer控件a port(例如:gen_tcp.socket),或者File.io_device/0它们将在收到GenServer退出信号时关闭,并且不需要关闭terminate/2

如果reason不是:normal:shutdown,也不是{:shutdown, term}则记录错误。