如何在每晚12点创建一个 Elixir GenServer 定时事件

huangapple go评论90阅读模式
英文:

How to create an Elixir GenServer timed event at 12am every night

问题

I have a LiveView web app and every night outside working hours (ideally at 12PM each night) I want to perform a single action (run a piece of code)

我有一个 LiveView 网页应用程序,每天晚上在工作时间之外(理想情况下每晚12点)我想执行单个操作(运行一段代码)

I have a GenServer that starts when the Phoenix App starts. The problem is that the phoenix server will be started at different times during the day and I do not know what time frame to set the scheduler.

我有一个 GenServer,在 Phoenix 应用程序启动时启动。问题是 Phoenix 服务器在一天内的不同时间启动,我不知道要设置调度器的时间框架。

To solve the problem (using my current code) I can perform the following:

为了解决这个问题(使用我的当前代码),我可以执行以下操作:

When the user starts the phoenix app, capture the current day as a date and store it in state. Run the Genserver every hour to check if the day has changed. If the day changed run the code and store the new day in state. Repeat.

当用户启动 Phoenix 应用程序时,捕获当前日期并将其存储在状态中。每小时运行 Genserver 以检查日期是否已更改。如果日期已更改,则运行代码并将新日期存储在状态中。重复。

This would work fine but the problem is I don't understand GenServers well enough to store and compare values as described. I figure I might need an agent to pass data around but I'm kind of just guessing.

这应该能正常工作,但问题是我不够了解 GenServers,以便像描述的那样存储和比较值。我想我可能需要一个代理来传递数据,但我有点在猜测。

I read this answer at it seems really clean but I do not know how to integrate the module: https://stackoverflow.com/a/32086769/1152980

我阅读了这个答案,它似乎非常干净,但我不知道如何集成这个模块:https://stackoverflow.com/a/32086769/1152980

My code is below and is based on this: https://stackoverflow.com/a/32097971/1152980

我的代码如下,基于这个:https://stackoverflow.com/a/32097971/1152980

defmodule App.Periodically do
  use GenServer

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, DateTime)
  end

  def init(state) do
    schedule_work(state) 
    {:ok, state}
  end

  def handle_info(:work, state) do
    IO.inspect state.utc_now
    IO.inspect "_____________NEW"
    IO.inspect DateTime.utc_now
    # When phoenix app starts - store current date
    # When Genserver runs.....
    # run conditional on stored date with current date
    # If they are different THEN RUN CODE
    # If they are the same - do nothing
    IO.inspect state
    schedule_work(state) 
    {:noreply, state}
  end

  defp schedule_work(state) do
    IO.inspect "test"
    Process.send_after(self(), :work, 10000 ) # 1 sec for testing will change to 1 hour
  end
end
defmodule App.Periodically do
  use GenServer

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, DateTime)
  end

  def init(state) do
    schedule_work(state) 
    {:ok, state}
  end

  def handle_info(:work, state) do
    IO.inspect state.utc_now
    IO.inspect "_____________NEW"
    IO.inspect DateTime.utc_now
    # 当 Phoenix 应用程序启动时 - 存储当前日期
    # 当 Genserver 运行时.....
    # 使用存储的日期与当前日期进行条件运行
    # 如果它们不同,然后运行代码
    # 如果它们相同 - 什么都不做
    IO.inspect state
    schedule_work(state) 
    {:noreply, state}
  end

  defp schedule_work(state) do
    IO.inspect "test"
    Process.send_after(self(), :work, 10000 ) # 1 秒用于测试,将更改为 1 小时
  end
end

(Note: The code contains HTML escape sequences like ", you may need to replace them with double quotes " in your Elixir code.)

英文:

I have a LiveView web app and every night outside working hours (ideally at 12PM each night) I want to perform a single action (run a piece of code)

I have a GenServer that starts when the Phoenix App starts. The problem is that the phoenix server will be started at different times during the day and I do not know what time frame to set the scheduler.

To solve the problem (using my current code) I can perform the following:

When the user starts the phoenix app, capture the current day as a date and store it in state.
Run the Genserver every hour to check if the day has changed. If the day changed run the code and store the new day in state.

Repeat.

This would work fine but the problem is I don't understand GenServers well enough to store and compare values as described. I figure I might need an agent to pass data around but I'm kind of just guessing.

I read this answer at it seems really clean but I do not know how to integrate the module:
https://stackoverflow.com/a/32086769/1152980

My code is below and is based on this: https://stackoverflow.com/a/32097971/1152980

defmodule App.Periodically do
  use GenServer

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, DateTime)
  end

  def init(state) do
    schedule_work(state) 
    {:ok, state}
  end

  def handle_info(:work, state) do
    IO.inspect state.utc_now
    IO.inspect "_____________NEW"
    IO.inspect DateTime.utc_now
    # When phoenix app starts - store current date
    # When Genserver runs.....
    # run conditional on stored date with current date
    # If they are different THEN RUN CODE
    # If they are the same - do nothing
    

    
  IO.inspect state
    schedule_work(state) 
    {:noreply, state}
  end

  defp schedule_work(state) do
   IO.inspect "test"
    Process.send_after(self(), :work, 10000 ) # 1 sec for testing will change to 1 hour
  end
end

答案1

得分: 1

这里最简单的解决方案可能是利用现有的包,比如quantum,该包实现了一个可配置的定期进程,看起来很适合你描述的用例。只需调整示例的Heartbeat模块为你自己想要调用的模块,例如:

config :my_app, MyApp.MySchedulerModuleThatICreatedFollowingTheDocs,
  jobs: [
    # 每天午夜运行:
    {"@daily", {MyApp.SomeModule, :some_function_name_as_an_atom, ["positional", "arguments", "to", "the", "given", "function"]}}
  ]

将转换为每天午夜调用MyApp.SomeModule.some_function_name_as_an_atom("positional", "arguments", "to", "the", "given", "function")

如果你想为此编写自己的GenServer,则必须处理一些边缘情况:如何处理服务器在一天内多次重新启动,如何处理它恰好在午夜启动的情况,时区等等,还有一些可能是@Aleksei会考虑到的其他情况。

从我写的一篇文章中调整代码,你可能会得到一个类似这样的模块:

defmodule Cronlike do
  @moduledoc """
  在你的监督树中:

        children = [
          Supervisor.child_spec({Cronlike, %{mod: IO, fun: :puts, args: ["Working Hard"]}, id: :job1)
        ]
        Supervisor.start_link(children, [strategy: :one_for_one, name: MyApp.Supervisor])

  或者,手动启动:

        iex> Cronlike.start_link(%{mod: IO, fun: :puts, args: ["Working Hard"]})
  """
  use GenServer

  def start_link(state) do
    GenServer.start_link(__MODULE__, state)
  end

  @impl true
  def init(state) do
    Process.send_after(self(), :do_work, ms_til_midnight())
    {:ok, state}
  end

  def ms_til_midnight do
    now = DateTime.utc_now()

    unix_now_ms = DateTime.to_unix(now, :millisecond)

    tomorrow = now |> DateTime.add(1, :day) |> DateTime.to_date()
    tomorrow_midnight = DateTime.new!(tomorrow, Time.new!(0, 0, 0))

    tomorrow_midnight_unix_ms = tomorrow_midnight |> DateTime.to_unix(:millisecond)

    tomorrow_midnight_unix_ms - unix_now_ms
  end

  @impl true
  def handle_info(:do_work, %{mod: mod, fun: fun, args: args} = state) do
    # 做工作
    apply(mod, fun, args)
    # 一天的毫秒数 = 86_400_000
    Process.send_after(self(), :do_work, 86_400_000)
    {:noreply, state}
  end
end

它接受配置参数来定义周期性调用的modfunargs。为简单起见,状态可以更多地被忽略,你可以只是硬编码要调用的单个函数。然而,为了更好的可用性,你可以想象在state中添加一个选项,该选项将决定以多频率运行任务,但这将要求你将示例的ms_til_midnight/0函数重构为更灵活的形式。你可以看到这个最棘手的部分其实只是计算直到午夜还有多少毫秒,所以我保留了这种冗长的写法。

英文:

The easiest solution here would probably be to leverage an existing package such as quantum which implements a configurable recurring process that seems like it would fit the use-cases you have described. Just adjust the example Heartbeat module for your own module that you wish to call, e.g.

config :my_app, MyApp.MySchedulerModuleThatICreatedFollowingTheDocs,
  jobs: [
    # Runs every midnight:
    {"@daily",         {MyApp.SomeModule, :some_function_name_as_an_atom, ["positional", "arguments", "to", "the", "given", "function"]}}
  ]

Would translate to calling MyApp.SomeModule.some_function_name_as_an_atom("positional", "arguments", "to", "the", "given", "function") every day at midnight.

If you want to write your own GenServer for this, you have to handle a couple edge-cases: how to handle the server restarting multiple times in a day, how to handle the case that it starts EXACTLY at midnight, something with timezones, and probably a couple others that @Aleksei would think about.

Adjusting code from an article I wrote, you might wind up with a module something like this:

defmodule Cronlike do
  @moduledoc """
  In your supervision tree:

        children = [
          Supervisor.child_spec({Cronlike, %{mod: IO, fun: :puts, args: ["Working Hard"]}, id: :job1)
        ]
        Supervisor.start_link(children, [strategy: :one_for_one, name: MyApp.Supervisor])

  Or, start it manually:

        iex> Cronlike.start_link(%{mod: IO, fun: :puts, args: ["Working Hard"]})
  """
  use GenServer

  def start_link(state) do
    GenServer.start_link(__MODULE__, state)
  end

  @impl true
  def init(state) do
    Process.send_after(self(), :do_work, ms_til_midnight())
    {:ok, state}
  end

  def ms_til_midnight do
    now = DateTime.utc_now()

    unix_now_ms = DateTime.to_unix(now, :millisecond)

    tomorrow = now |> DateTime.add(1, :day) |> DateTime.to_date()
    tomorrow_midnight = DateTime.new!(tomorrow, Time.new!(0, 0, 0))

    tomorrow_midnight_unix_ms = tomorrow_midnight |> DateTime.to_unix(:millisecond)

    tomorrow_midnight_unix_ms - unix_now_ms
  end

  @impl true
  def handle_info(:do_work, %{mod: mod, fun: fun, args: args} = state) do
    # Do the work
    apply(mod, fun, args)
    # ms in day = 86_400_000
    Process.send_after(self(), :do_work, 86_400_000)
    {:noreply, state}
  end
end

It accepts configuration parameters to define the mod, fun, and args to call periodically. For simplicity, the state could more or less be ignored and you could just hardcode a single function to be called. However, for better usability, you can imagine adding in an option (in the state) that would dictate how frequently to run the task, but this would require you to refactor the example ms_til_midnight/0 function into something more flexible. You can see the trickiest part of this is simply the math to calculate how many milliseconds until midnight, so I've left that verbose.

huangapple
  • 本文由 发表于 2023年3月9日 19:50:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/75684216.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定