使用 Supervisor 时 start_link/3 的处理结果

Handling result of start_link/3 when using Supervisor

我设置了一个主管来监督 Slack websocket:

children = [
  %{
    id: Slack.Bot,
    start: {Slack.Bot, :start_link, [MyBot, [], "api_token"]}
  }
]
opts = [strategy: :one_for_one, name: MyBot.Supervisor]
Supervisor.start_link(children, opts)

MyBot 当消息通过 websocket 到达时接收各种回调。这很好,但还有一个额外的回调 handle_info/3,我想用它来处理我自己的事件。为此,我需要自己向流程发送消息。

我看到我可以从 start_link/3 的结果中获取 PID,但这是由 Supervisor 自动调用的。如何获取此进程的 PID 以便向其发送消息,同时仍对其进行监督?我是否必须实施额外的监督层?

您应该使用 GenServer 来存储 PID,然后您可以根据需要引用它。流程将是这样的:制作一个 MyServer genserver 来保存你的 slackbot PID。然后,在 GenServer 内部,您可以在调用或转换处理程序中执行类似 send(state.slack, :display_leaderboard) 的操作。

defmodule MyServer do
  use GenServer

  def child_spec(team_id) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [team_id]},
      type: :worker
    }
  end

  def start_link(team_id) do
    GenServer.start_link(__MODULE__, team_id)
  end

  def init(team_id) do
    {:ok, pid} = Slack.Bot.start_link(MyBot, [], team_id)
    {:ok, %{slack: pid}}
  end

您不一定需要 PID。 Elixir 允许使用 named 进程并且 Process.send/3 完全接受 names 作为第一个参数。鉴于您在示例中将机器人命名为 MyBot.Supervisor,以下将成功向其发送消息:

Process.send(MyBot.Supervisor, :message_to_bot, [:noconnect])

或者,如果您的机器人 运行 在不同的节点上:

Process.send({MyBot.Supervisor, :node_name}, :message_to_bot, [:noconnect])

一般来说,使用名称而不是 PID 是 Elixir 中与发送消息相关的所有内容的常见做法,因为 PID 在进程 crashes/restarts 时可能会发生变化,而名称将永远保留。

主管、pids 和启动函数

主管希望启动功能 return 这三个值之一:

{:ok, pid}
{:ok, pid, any}
{:error, any}

在您的代码中,开始函数是 Slack.Bot.start_link/4,最后一个参数默认为空列表。

您注意到您无法访问 pid,因为启动函数的结果在使用 Elixir 的 Supervisor.start_link/2. In some cases, it makes sense to invoke Supervisor.start_child/2 instead, which returns the pid of the started child (and additional info, if any). And for completeness, the pids of supervised processes can also be queried with Supervisor.which_children/1.

时丢失了

但是,主管的作用是监督流程并在必要时重新启动流程。当一个进程重新启动时,它会得到一个新的 pid。因此,pid 不是长时间引用进程的正确方法。

Pid 和名称

您的问题的解决方案是通过名称引用该过程。虚拟机维护进程名称(以及端口)的映射,并允许通过名称而不是 pids(和端口引用)来引用进程(和端口)。注册进程的原语是Process.register/2。大多数需要 pid 的函数(如果不是全部)也接受注册名称。名称在节点内是唯一的。

虽然 spawn* 原语不通过名称注册进程,但构建在它们之上的代码通常提供通过启动过程注册名称的能力。 Slack.Bot.start_link/4Supervisor.start_link/2 都是这种情况。通常,这是您的代码通过将 :name 选项传递给 Supervisor.start_link/2 来完成的操作。顺便说一句,除非您稍后需要参考 Supervisor 进程,否则这是无用的,这可能不是您的几段代码所暗示的情况。

案例Slack.Bot.start_link/4

为了能够引用您的机器人进程,只需确保使用带有您选择的名称(原子)的 :name 选项调用 Slack.Bot.start_link/4,例如 MyBot.这是在子规范中完成的。

children = [
  %{
    id: Slack.Bot,
    start: {Slack.Bot, :start_link, [MyBot, [], "api_token", %{name: MyBot}]}
  }
]
opts = [strategy: :one_for_one]
Supervisor.start_link(children, opts)

因此,主管将使用提供的四个参数 ([MyBot, [], "api_token", [name: MyBot]) 调用 Slack.Bot.start_link/4 函数,并且 Slack.Bot.start_link/4 将使用提供的名称注册进程。

如果你选择MyBot作为上面的名字,你可以给它发一条消息:

Process.send(MyBot, :message_to_bot, [])

或使用 Kernel.send/2 原语:

send(MyBot, :message_to_bot)

然后将由 handle_info/3 回调处理。

作为旁注,具有注册名称的 OTP 监督树中的进程可能应该基于 OTP 模块,并让 OTP 框架进行注册。在 OTP 框架中,名称注册发生在初始化阶段的早期,如果存在冲突,进程将停止并且 start_link return 出现错误 ({:error,{:already_started,pid}})。

Slack.Bot.start_link/4 确实基于 OTP 模块:它基于 :websocket_client 模块,后者本身基于 OTP 的 :gen_fsm。然而,在它的 current implementation 中,函数没有将名称向下传递给 :websocket_client.start_link/4 而不是向下传递给 :gen_fsm.start_link/4,而是直接使用 Process.register/2 注册名称。因此,如果存在名称冲突,机器人可能仍会连接到 Slack。

异步消息和回复

Process.send/3 以及 Kernel.send/2 原语异步发送消息。这些函数 return 立即生效。

如果第一个参数是进程的pid,即使进程不再是运行,这些函数也会成功。如果它是一个原子,如果没有进程以此名称注册,它们将失败。

要从机器人进程获得回复,您需要实施某种机制,让机器人进程知道将回复发送到何处。此机制由 OTP 的 gen_server 及其对应的 Elixir GenServer.call/2 提供,但此处不作为 Slack.Bot API.

的一部分提供

Erlang 的方法是发送一个带有调用者 pid 的元组,通常作为第一个参数。所以你会这样做:

send(MyBot, {self(), :message_to_bot})
receive do result -> result end

然后机器人接收并回复消息为:

def handle_info({caller, message}, slack, state) do
    ...
    send(caller, result)
end

这是调用的一个非常简单的版本。 GenServer.call/2 做更多的事情,比如处理超时,确保响应不是你会得到的一些随机消息,而是调用的结果,并且进程在调用期间不会消失。在这个简单的版本中,您的代码可以永远等待回复。

为防止这种情况,您至少应该添加一个超时时间和一种方法来确保这不是随机消息,例如:

def call_bot(message) do
    ref = make_ref()
    send(MyBot, {self(), ref, message})
    receive do
        {:reply, ^ref, result} -> {:ok, result}
    after 5_000 ->
        {:error, :timeout}
    end
end

对于 handle_info 部分,只需 return 元组中传递的不透明引用:

def handle_info({caller, ref, message}, slack, state) do
    ...
    send(caller, {:reply, ref, result})
end

make_ref/0 是创建新的、唯一的 ref 的原语,通常用于此用途。