Правильный эликсир OTP способ структурировать повторяющиеся задачи
У меня есть рабочий процесс, который включает в себя пробуждение каждые 30 секунд или около того и опрос базы данных для обновлений, принятие мер по этому вопросу, а затем возврат к сну. Отказ от того, что опрос базы данных не масштабируется и другие подобные проблемы, каков наилучший способ структурирования этого рабочего процесса с помощью диспетчеров, рабочих, задач и т.д.?
Я выложу несколько идей, которые у меня были, и мои мысли за/против. Пожалуйста, помогите мне разобраться в самом подходе Elixir-y. (Я все еще очень новичок в Elixir, кстати.)
1. Бесконечный вызов через функцию вызова
Просто поставьте простой рекурсивный цикл, например:
def do_work() do
# Check database
# Do something with result
# Sleep for a while
do_work()
end
Я видел что-то подобное, следуя инструкциям по созданию веб-искателя.
Одна из проблем, которые я имею здесь, - бесконечная глубина стека из-за рекурсии. Разве это не приведет к переполнению стека, так как мы рекурсируем в конце каждого цикла? Эта структура используется в стандартном руководстве Elixir для задач, поэтому я, вероятно, ошибаюсь в проблеме.
Обновление. Как упоминалось в ответах, рекурсия хвоста в Elixir означает, что переполнение стека здесь не проблема, Циклы, которые называют себя в конце, являются принятым способом бесконечного цикла.
2. Используйте задачу, перезагрузите каждое время
Основная идея здесь - использовать задачу, которая запускается один раз и затем выходит, но сопрягайте ее с Supervisor с стратегией перезапуска one-to-one
, поэтому она запускается каждый раз после ее завершения. Задача проверяет базу данных, спит, а затем завершает работу. Супервизор видит выход и запускает новый.
Это дает возможность жить внутри Супервизора, но это похоже на злоупотребление Супервизора. Он используется для циклирования в дополнение к захвату ошибок и перезапуску.
(Примечание. Возможно, что-то еще можно сделать с Task.Supervisor, в отличие от обычного супервизора, и я просто не понимаю его.)
3. Задача + Бесконечная петля рекурсии
В принципе, объедините 1 и 2, так что это задача, которая использует бесконечный цикл рекурсии. Теперь он управляется Супервизором и перезагружается, если он разбился, но не перезапускается снова и снова как нормальная часть рабочего процесса. В настоящее время это мой любимый подход.
4. Другое?
Я беспокоюсь, что есть некоторые фундаментальные структуры OTP, которые мне не хватает. Например, я знаком с Agent и GenServer, но я недавно наткнулся на Task. Может быть, есть какой-то Looper для именно этого случая или какой-то прецедент Task.Supervisor, который его охватывает.
Ответы
Ответ 1
Я только недавно начал использовать OTP, но я думаю, что могу дать вам несколько указателей:
- Что Elixir способ сделать это, я взял цитату из программирования Elixir Дейвом Томасом, поскольку он объясняет это лучше, чем я:
Рекурсивная функция приветствия может вас немного беспокоить. каждый время, когда оно получает сообщение, оно в конечном итоге вызывает себя. Во многих языков, который добавляет новый фрейм в стек. После большого количества сообщений, может закончиться нехватка памяти. Это не происходит в Elixir, поскольку он реализует оптимизацию хвостового вызова. Если последнее, что функция is is call сама, theres нет необходимости делать вызов. Вместо этого среда выполнения может просто вернуться к началу функция. Если рекурсивный вызов имеет аргументы, то они заменяют исходные параметры по мере возникновения цикла.
- Задачи (как и в модуле Task) предназначены для одной задачи, короткоживущих процессов, поэтому они могут быть тем, что вы хотите. В качестве альтернативы, почему бы не создать процесс, который порождается (возможно, при запуске), чтобы иметь эту задачу, и иметь ли он цикл и доступ к БД каждые x времени?
- и 4, возможно, посмотрите на использование GenServer со следующей архитектурой. Supervisor → GenServer → Рабочие, которые появляются при необходимости для задачи (здесь вы можете просто использовать spwn fn → ... end, на самом деле не нужно беспокоиться о выборе Task или другого модуля), а затем выйти после завершения.
Ответ 2
Я немного опоздал, но для тех, кто по-прежнему ищет правильный способ сделать это, я думаю, стоит упомянуть документацию GenServer:
handle_info/2
может использоваться во многих ситуациях, например, при обработке сообщений мониторинга DOWN, отправленных Process.monitor/1
. Другим вариантом использования handle_info/2
является выполнение периодической работы с помощью Process.send_after/4
:
defmodule MyApp.Periodically do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, %{})
end
def init(state) do
schedule_work() # Schedule work to be performed on start
{:ok, state}
end
def handle_info(:work, state) do
# Do the desired work here
schedule_work() # Reschedule once more
{:noreply, state}
end
defp schedule_work() do
Process.send_after(self(), :work, 2 * 60 * 60 * 1000) # In 2 hours
end
end
Ответ 3
Я думаю, что общепринятым способом делать то, что вы ищете, является подход № 1. Поскольку Erlang и Elixir автоматически оптимизируют tail calls, вам не нужно беспокоиться о переполнении стека.
Ответ 4
Есть другой способ с Stream.cycle. Здесь пример макроса
defmodule Loop do
defmacro while(expression, do: block) do
quote do
try do
for _ <- Stream.cycle([:ok]) do
if unquote(expression) do
unquote(block)
else
throw :break
end
end
catch
:break -> :ok
end
end
end
end
Ответ 5
Я бы использовал GenServer и в init return
{:ok, <state>, <timeout_in_ milliseconds>}
Задание таймаута вызывает вызов функции handle_info
при достижении тайм-аута.
И я могу убедиться, что этот процесс запущен, добавив его в мой главный руководитель проекта.
Это пример того, как его можно использовать:
defmodule MyApp.PeriodicalTask do
use GenServer
@timeout 50_000
def start_link do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, %{}, @timeout}
end
def handle_info(:timeout, _) do
#do whatever I need to do
{:noreply, %{}, @timeout}
end
end