适当的Elixir OTP方式来构建定期任务

16
我有一个工作流程,需要每隔30秒钟唤醒一次并轮询数据库获取更新,然后执行相应的操作,再回到睡眠状态。暂且不考虑数据库轮询不可扩展等类似问题,使用监管器、工作者、任务等方法,最佳的工作流程结构是什么?
我列出了几个想法以及我的优缺点分析,请帮助我找出最符合Elixir理念的方法。(顺便说一下,我还很新于Elixir。)
1. 通过函数调用进行无限循环
只需在其中放置一个简单的递归循环,例如:
def do_work() do
  # Check database
  # Do something with result
  # Sleep for a while
  do_work()
end

我在跟随构建网络爬虫的教程时看到了类似的内容。

我在这里有一个担忧,由于递归导致无限堆栈深度,这最终会导致堆栈溢出,因为我们在每个循环结束时都进行递归。这种结构在Elixir标准任务指南中使用,所以我可能对堆栈溢出问题是错误的。

更新 - 如答案中所述,Elixir中的尾调用递归意味着堆栈溢出在这里不是问题。在末尾调用自身的循环是一种接受的方式来进行无限循环。

2. 使用任务,每次重新启动

基本思想是使用一个运行一次然后退出的任务(Task),但将其与一个具有“一对一”重启策略的监管器(Supervisor)配对,因此每次完成后都会重新启动。任务检查数据库,睡眠,然后退出。监管器看到退出并启动一个新的任务。
这样做的好处是它位于监管器内部,但似乎滥用了监管器。它不仅用于错误捕获和重启,还用于循环。
(注意:可能可以使用Task.Supervisor代替常规监管器,但我只是没有理解。)
基本上,将1和2结合起来,使其成为使用无限递归循环的任务(Task)。现在它由监管器管理,如果崩溃则会重新启动,但不会像正常工作流程的一部分那样反复重启。这是目前我最喜欢的方法。
我的担忧是我可能错过了一些基本的OTP结构。例如,我熟悉Agent和GenServer,但最近才发现Task。也许有一种专门针对这种情况的Looper,或者Task.Supervisor的某种用例涵盖了它。
5个回答

20

虽然我来得有点晚,但对于那些仍在寻找正确方法的人来说,我认为值得提及GenServer文档本身:

handle_info/2可以用于许多情况,例如处理由Process.monitor/1发送的监视DOWN消息。 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

是的,我在几个地方都使用了这个确切的模式。如果您想要在已经是GenServer的东西中发生周期性任务,我绝对推荐它。 - Micah

13

我最近才开始使用OTP,但我认为我可能能给你一些指导:

  1. 这是Elixir的做法,我引用了Dave Thomas在《Programming Elixir》中的一句话,他的解释比我好:

    递归的问候函数可能让你有点担心。每次它接收到一个消息,它都会调用自己。在许多语言中,这会在堆栈上添加一个新帧。经过大量的消息后,你可能会耗尽内存。在Elixir中,这不会发生,因为它实现了尾递归优化。如果一个函数最后要做的事情是调用自己,那么就没有必要进行调用。相反,运行时可以直接跳回函数的开头。如果递归调用有参数,则这些参数将替换原始参数作为循环发生时的参数。

  2. 任务(如Task模块中的)用于单个任务、短期进程,因此它们可能是您想要的。或者,为什么不拥有一个进程,在启动时生成以执行该任务,并使其循环并每隔 x 时间访问数据库?
  3. 至于第4个问题,也许可以考虑使用以下架构的GenServer:Supervisor -> GenServer -> 需要为任务生成的工作进程(在这里,您可以只使用 "spawn fn -> ... end ",不真正需要担心选择任务或其他模块),然后在完成任务后退出。

3

我认为通常实现您所需功能的方法是方法#1。因为Erlang和Elixir会自动优化尾调用,所以您不需要担心堆栈溢出问题。


3

使用Stream.cycle还有另外一种方法。以下是while宏的示例:

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

2
我会使用GenServer,在init函数中返回。您可以访问init函数了解更多信息。
{: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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接