Ковбой/Ранчо убивает процесс обработки, когда клиент закрывает соединение

#erlang #elixir #phoenix #cowboy #ranch

Вопрос:

У меня есть приложение Phoenix со сложной бизнес-логикой, лежащей в основе конечной точки HTTP. Эта логика включает взаимодействие с базой данных и несколькими внешними службами, и после запуска обработки запроса она не должна прерываться до тех пор, пока не будут выполнены все операции.

Но похоже, что Ковбой или Ранчо убивают процесс обработки запросов (контроллер Феникса), если клиент внезапно закрывает соединение, что приводит к частично выполненному бизнес-процессу. Чтобы отладить это, у меня есть следующий код в действии контроллера:

 Process.flag(:trap_exit, true)

receive do
  msg -> Logger.info("Message: #{inspect msg}")
after
  10_000 -> Logger.info("Timeout")
end
 

И для имитации закрытия соединения я установил тайм-аут: curl --request POST 'http://localhost:4003' --max-time 3 .
Через 3 секунды в консоли IEx я вижу, что процесс вот-вот завершится: Message: {:EXIT, #PID<0.4196.0>, :shutdown} .

Поэтому мне нужно, чтобы контроллер выполнил свою работу и ответил клиенту, если он все еще там, или ничего не сделал, если соединение потеряно. Что будет лучшим способом достичь этого:

  • прерывание выходов в действии контроллера и игнорирование сообщений о выходе;
  • порождайте не связанные Task действия контроллера и ждите его результатов;
  • как-то настроить Cowboy/Ranch так, чтобы он не убивал процесс обработки, если это возможно (пытался exit_on_close безуспешно)?

Ответ №1:

Процессы обработки будут уничтожены после завершения запроса, такова их цель. Если вы хотите обработать некоторые данные в фоновом режиме, запустите дополнительный процесс. Самым простым способом сделать это был бы предложенный вами 2-й метод, но с небольшими изменениями в использовании Task.Supervisor .

Поэтому в своем супервайзере приложений вы начинаете Task.Supervisor с имени по вашему выбору:

 children = [
  {Task.Supervisor, name: MyApp.TaskSupervisor}
]

Supervisor.start_link(children, strategy: :one_for_one)
 

А затем в обработчике вашего запроса:

 parent = self()
ref = make_ref()

Task.Supervisor.start_child(MyApp.TaskSupervisor, fn() ->
  send(parent, {ref, do_long_running_stuff()})
end)

receive do
  {^ref, result} -> notify_user(result)
end
 

Таким образом, вам не нужно беспокоиться о том, как справиться с ситуацией, когда пользователя больше нет для получения сообщения.