英文:
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
它接受配置参数来定义周期性调用的mod
、fun
和args
。为简单起见,状态可以更多地被忽略,你可以只是硬编码要调用的单个函数。然而,为了更好的可用性,你可以想象在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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论