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

时间:2018-05-17 01:43:36

标签: elixir 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)
当通过websocket获得消息时,

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

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

3 个答案:

答案 0 :(得分:3)

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

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

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

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

通常,使用名称而不是PID是Elixir中与发送消息相关的所有内容的常见做法,因为当进程崩溃/重新启动时,PID是要更改的主题,而名称将永久保留。

答案 1 :(得分:1)

您应该使用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

答案 2 :(得分:1)

主管,pids和启动功能

主管希望启动函数返回以下三个值中的一个:

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

在你的代码中,start函数是Slack.Bot.start_link/4,默认情况下最后一个参数是空列表。

您注意到无法访问pid,因为使用Elixir的Supervisor.start_link/2会导致启动函数的结果丢失。在某些情况下,调用Supervisor.start_child/2是有意义的,它会返回已启动子项的pid(以及其他信息,如果有的话)。为了完整起见,还可以使用Supervisor.which_children/1查询监督过程的pids。

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

Pids和名称

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

虽然spawn*原语不按名称注册进程,但构建在它们之上的代码通常提供通过启动过程注册名称的能力。这是Slack.Bot.start_link/4以及Supervisor.start_link/2的情况。通常,这是您的代码通过将:name选项传递给Supervisor.start_link/2而执行的操作。顺便说一句,这是没用的,除非您稍后需要参考Supervisor进程,这可能不是您的代码的几个部分暗示的情况。

Slack.Bot.start_link/4

的情况

为了能够引用您的机器人流程,只需确保使用Slack.Bot.start_link/4选项调用:name,并使用您选择的名称(原子),例如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)

因此,主管将使用提供的四个参数(Slack.Bot.start_link/4)调用[MyBot, [], "api_token", [name: MyBot]函数,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返回错误({: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原语异步发送消息。这些功能立即返回。

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

要从bot进程获得回复,您需要实现一些机制,其中bot进程知道将回复发送到何处。这个机制由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部分,只需返回在元组中传递的不透明引用:

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

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