使用 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/4
和 Supervisor.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 的原语,通常用于此用途。
我设置了一个主管来监督 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/4
和 Supervisor.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 的原语,通常用于此用途。