Супервизоры, пиды и функции запуска
Супервизоры ожидают, что функции запуска возвратят одно из следующих трех значений:
{:ok, pid}
{:ok, pid, any}
{:error, any}
В вашем коде функция запуска - Slack.Bot.start_link/4
, а последними аргументами по умолчанию является пустой список.
Вы замечаете, что не можете получить доступ к pid, потому что результаты функций запуска теряются при использовании Elixir's Supervisor.start_link/2
. В некоторых случаях имеет смысл вместо этого вызывать Supervisor.start_child/2
, который возвращает pid запущенного дочернего элемента (и дополнительную информацию, если таковая имеется). А для полноты pids контролируемых процессов также можно запросить с помощью Supervisor.which_children/1
.
Однако роль супервизора состоит в том, чтобы контролировать процессы и перезапускать их при необходимости. Когда процесс перезапускается, он получает новый pid. По этой причине pid не является подходящим способом для ссылки на процесс в течение длительного времени.
Пидс и имена
Решение вашей проблемы - обратиться к процессу по name . Виртуальная машина поддерживает отображение имен процессов (а также портов) и позволяет ссылаться на процессы (и порты) по имени, а не по 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
, который сам основан на :gen_fsm
от OTP. Однако в текущей реализации 1075 * вместо передачи имени до :websocket_client.start_link/4
, которое передает его до :gen_fsm.start_link/4
, функция регистрирует имя непосредственно с помощью Process.register/2
. В результате в случае конфликта имен бот может все равно подключиться к Slack.
Асинхронные сообщения и ответы
Process.send/3
, а также Kernel.send/2
примитив отправляют сообщение асинхронно. Эти функции сразу возвращаются.
Если первый параметр является pid процесса, эти функции выполняются успешно, даже если процесс больше не выполняется. Если это атом, они потерпят неудачу, если процесс с таким именем не зарегистрирован.
Чтобы получить ответ от процесса бота, вам нужно внедрить некоторый механизм, когда процесс бота знает, куда отправить ответ. Этот механизм предоставлен OTP gen_server и его аналогом Elixir GenServer.call/2
, но он не доступен здесь как часть Slack.Bot API.
Способ Эрланга сделать это - отправить кортеж с 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
- это примитив, создающий новую уникальную ссылку, обычно для этого использования.