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
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.