正确的 Elixir OTP 方法来构建重复性任务
Proper Elixir OTP way to structure a recurring task
我有一个工作流程,涉及每 30 秒左右醒来一次并轮询数据库以获取更新,对此采取行动,然后再回到睡眠状态。撇开数据库轮询无法扩展和其他类似问题不谈,使用主管、工作人员、任务等构建此工作流的最佳方式是什么?
我将列出一些我的想法和想法 for/against。请帮我找出最 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中使用了这个结构,所以我对堆栈溢出问题的理解可能是错误的
Update - 如答案中所述,Elixir 中的 tail call recursion 意味着堆栈溢出在这里不是问题。在最后调用自己的循环是一种可接受的无限循环方式。
2。使用任务,每次重启
这里的基本思想是使用一个运行一次然后退出的任务,但是将它与一个具有 one-to-one
重启策略的主管配对,这样它每次完成后都会重新启动。 Task 检查数据库,休眠,然后退出。主管看到出口并开始新的出口。
这有住在Supervisor里面的好处,但是好像是在滥用Supervisor。除了错误捕获和重新启动之外,它还用于循环。
(注意:Task.Supervisor 可能还有一些其他的事情可以做,而不是普通的 Supervisor,我只是不明白。)
3。任务 + 无限递归循环
基本上,将 1 和 2 结合起来,因此它是一个使用无限递归循环的任务。现在它由 Supervisor 管理,如果崩溃会重新启动,但不会作为工作流程的正常部分一遍又一遍地重新启动。这是目前我最喜欢的方法。
4.其他?
我担心的是我缺少一些基本的 OTP 结构。例如,我熟悉 Agent 和 GenServer,但最近才偶然发现 Task。也许正是这种情况下有某种 Looper,或者涵盖它的 Task.Supervisor 的一些用例。
我认为完成您正在寻找的事情的普遍接受的方法是方法 #1。因为 Erlang 和 Elixir 自动优化 tail calls 你不需要担心堆栈溢出。
我最近才开始使用 OTP,但我想我可以给你一些建议:
- 这就是 Elixir 的做法,我引用了 Dave Thomas 的 Programming Elixir 的话,因为它比我解释得更好:
The recursive greet function might have worried you a little. Every
time it receives a message, it ends up calling itself. In many
languages, that adds a new frame to the stack. After a large number of
messages, you might run out of memory. This doesn’t happen in Elixir,
as it implements tail-call optimization. If the very last thing a
function does is call itself, there’s no need to make the call.
Instead, the runtime can simply jump back to the start of the
function. If the recursive call has arguments, then these replace the
original parameters as the loop occurs.
- 任务(如在任务模块中)用于单个任务、短暂的进程,因此它们可能是您想要的。或者,为什么不生成一个进程(可能在启动时)来执行该任务并让它每隔 x 次循环并访问数据库?
- 和 4,也许考虑使用具有以下架构的 GenServer需要担心选择任务或其他模块)然后在完成后退出。
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
我会使用 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
我在这里有点晚了,但对于那些仍在寻找正确方法的人来说,我认为值得一提的是 GenServer documentation 本身:
handle_info/2
can be used in many situations, such as handling monitor DOWN messages sent by Process.monitor/1
. Another use case for handle_info/2
is to perform periodic work, with the help of 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
我有一个工作流程,涉及每 30 秒左右醒来一次并轮询数据库以获取更新,对此采取行动,然后再回到睡眠状态。撇开数据库轮询无法扩展和其他类似问题不谈,使用主管、工作人员、任务等构建此工作流的最佳方式是什么?
我将列出一些我的想法和想法 for/against。请帮我找出最 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中使用了这个结构,所以我对堆栈溢出问题的理解可能是错误的
Update - 如答案中所述,Elixir 中的 tail call recursion 意味着堆栈溢出在这里不是问题。在最后调用自己的循环是一种可接受的无限循环方式。
2。使用任务,每次重启
这里的基本思想是使用一个运行一次然后退出的任务,但是将它与一个具有 one-to-one
重启策略的主管配对,这样它每次完成后都会重新启动。 Task 检查数据库,休眠,然后退出。主管看到出口并开始新的出口。
这有住在Supervisor里面的好处,但是好像是在滥用Supervisor。除了错误捕获和重新启动之外,它还用于循环。
(注意:Task.Supervisor 可能还有一些其他的事情可以做,而不是普通的 Supervisor,我只是不明白。)
3。任务 + 无限递归循环
基本上,将 1 和 2 结合起来,因此它是一个使用无限递归循环的任务。现在它由 Supervisor 管理,如果崩溃会重新启动,但不会作为工作流程的正常部分一遍又一遍地重新启动。这是目前我最喜欢的方法。
4.其他?
我担心的是我缺少一些基本的 OTP 结构。例如,我熟悉 Agent 和 GenServer,但最近才偶然发现 Task。也许正是这种情况下有某种 Looper,或者涵盖它的 Task.Supervisor 的一些用例。
我认为完成您正在寻找的事情的普遍接受的方法是方法 #1。因为 Erlang 和 Elixir 自动优化 tail calls 你不需要担心堆栈溢出。
我最近才开始使用 OTP,但我想我可以给你一些建议:
- 这就是 Elixir 的做法,我引用了 Dave Thomas 的 Programming Elixir 的话,因为它比我解释得更好:
The recursive greet function might have worried you a little. Every time it receives a message, it ends up calling itself. In many languages, that adds a new frame to the stack. After a large number of messages, you might run out of memory. This doesn’t happen in Elixir, as it implements tail-call optimization. If the very last thing a function does is call itself, there’s no need to make the call. Instead, the runtime can simply jump back to the start of the function. If the recursive call has arguments, then these replace the original parameters as the loop occurs.
- 任务(如在任务模块中)用于单个任务、短暂的进程,因此它们可能是您想要的。或者,为什么不生成一个进程(可能在启动时)来执行该任务并让它每隔 x 次循环并访问数据库?
- 和 4,也许考虑使用具有以下架构的 GenServer需要担心选择任务或其他模块)然后在完成后退出。
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
我会使用 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
我在这里有点晚了,但对于那些仍在寻找正确方法的人来说,我认为值得一提的是 GenServer documentation 本身:
handle_info/2
can be used in many situations, such as handling monitor DOWN messages sent byProcess.monitor/1
. Another use case forhandle_info/2
is to perform periodic work, with the help ofProcess.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