我有一个主管设置来监督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以便向其发送消息,同时仍然保持监督?我是否必须实施和额外的监督层?
答案 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)
主管希望启动函数返回以下三个值中的一个:
{: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(和端口引用)引用进程(和端口)。注册进程的原语是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
是一个原语创建一个新的,唯一的引用,通常用于此用途。