Обработка результата start_link / 3 при использовании Supervisor - PullRequest
0 голосов
/ 17 мая 2018

У меня есть супервизор, настроенный для наблюдения за 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 получает различные обратные вызовы, когда сообщения поступают через веб-сокет. Это нормально, но есть дополнительный обратный вызов, handle_info/3, который я хочу использовать для обработки моих собственных событий. Для этого мне нужно отправить сообщение самому процессу.

Я вижу, что могу получить PID из результата start_link/3, но супервизор автоматически вызывает его. Как я могу получить PID этого процесса, чтобы отправить ему сообщение, сохраняя при этом контроль? Нужно ли внедрять и дополнительный уровень надзора?

Ответы [ 3 ]

0 голосов
/ 17 мая 2018

Вам не обязательно нужен 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 могут изменяться при сбое / перезапуске процесса, а имя сохраняется навсегда.

0 голосов
/ 18 мая 2018

Супервизоры, пиды и функции запуска

Супервизоры ожидают, что функции запуска возвратят одно из следующих трех значений:

{: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 - это примитив, создающий новую уникальную ссылку, обычно для этого использования.

0 голосов
/ 17 мая 2018

Вы должны использовать GenServer для хранения PID, а затем можете ссылаться на него по мере необходимости. Поток будет примерно таким: Создайте MyServer genserver, который поддерживает ваш PID slackbot. Затем внутри 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
...