正确的Elixir OTP方法来构建定期任务

时间:2016-02-12 14:03:28

标签: elixir otp

我有一个工作流程,涉及每30秒左右醒来并轮询数据库以获取更新,对此采取措施,然后再回到睡眠状态。撇开数据库轮询不会扩展和其他类似问题,使用主管,工作人员,任务等构建此工作流的最佳方法是什么?

我将列出一些我已经拥有的想法和我的想法/反对。请帮我弄清楚Elixir-y最方法。 (顺便说一下,我对Elixir还很新。)

1。无限循环函数调用

只需在其中放置一个简单的递归循环,就像这样:

def do_work() do
  # Check database
  # Do something with result
  # Sleep for a while
  do_work()
end

在跟随tutorial on building a web crawler时,我看到类似的东西。

我在这里遇到的一个问题是由于递归导致的无限堆栈深度。这会不会导致堆栈溢出,因为我们在每个循环结束时递归?这个结构在the standard Elixir guide for Tasks中使用,所以我可能错误的是堆栈溢出问题。

更新 - 如答案中所述,Elixir中的tail call recursion表示堆栈溢出不是问题。最后调用自己的循环是一种可以接受的无限循环方式。

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

这里的基本思想是使用一次运行然后退出的任务,但是将其与具有one-to-one重启策略的Supervisor配对,以便每次完成后重新启动。任务检查数据库,休眠,然后退出。主管看到出口并开始新的出口。

这有利于居住在主管内部,但似乎是滥用主管。除了错误捕获和重新启动之外,它还用于循环。

(注意:使用Task.Supervisor可能还有其他功能,而不是普通的Supervisor,而我只是不理解它。)

第3。任务+无限递归循环

基本上,将1和2组合在一起,因此它是一个使用无限递归循环的Task。现在它由Supervisor管理,如果崩溃将重新启动,但不会作为工作流程的正常部分反复重启。这是目前我最喜欢的方法。

4。其他吗

我担心的是,我缺少一些基本的OTP结构。例如,我熟悉Agent和GenServer,但最近我偶然发现了Task。也许正是这种情况下有某种Looper,或者是覆盖它的Task.Supervisor的一些用例。

5 个答案:

答案 0 :(得分:16)

我在这里有点晚了,但对于那些仍在寻找正确方法的人,我认为值得一提的是GenServer documentation本身:

  

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

答案 1 :(得分:10)

我最近才开始使用OTP,但我想我可能会给你一些指示:

  1. 这是Elixir这样做的方式,我从Dave Thomas的编程Elixir中引用了它,因为它解释得比我更好:
      

    递归问候语功能可能会让你有点担心。一切   它接收到一条消息的时间,最终会自行调用。在很多   语言,为堆栈添加新框架。经过大量的   消息,你可能会耗尽内存。在Elixir中不会发生这种情况,   因为它实现了尾调用优化。如果最后一件事   函数确实是调用自身,没有必要进行调用。   相反,运行时可以简单地跳回到开头   功能。如果递归调用有参数,那么这些替换了   原始参数作为循环发生。

  2. 任务(如在任务模块中)用于单个任务,即短期进程,因此它们可能是您想要的。或者,为什么不让一个进程(可能在启动时)生成该任务并让它循环并每隔x次访问一次数据库?
  3. 和4,或许可以考虑使用具有以下架构Supervisor的GenServer - > GenServer - >工作人员在需要完成任务时产生(这里你可能只使用spawn fn - > ...结束,不需要担心选择任务或其他模块)然后在完成时退出。

答案 2 :(得分:3)

我认为通常接受的方法是做你正在寻找的方法#1。因为Erlang和Elixir会自动优化tail calls,所以您不必担心堆栈溢出。

答案 3 :(得分:2)

还有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

答案 4 :(得分:2)

我会使用GenServer并在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