Правильный эликсир 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