GenServer with a interval that comes from a user from web UI

Say, I want to do some tasks, always in background, every hour. I’ll use GenServer.
But the frequency of doing the tasks also can be changed, namely, it can be set up by user, for example, via web UI.
How can I create such a GenServer?

I know how to create a normal one only, with a constant frequency.

Maybe store the frequency interval in the state of the GenServer. Then use calls to change it from the outside.

1 Like

Here is a simple example, though it’s kind of crude. Like @kokolegorille said, you keep track of the interval as part of the genserver state and then supply a handler that allows you to update the interval. A caveat is that updating the interval doesn’t take effect until the previous interval’s action has already completed. I’m not sure if there is a way cancel a Process.send_after. There might be more robust ways of handling this.

defmodule UserSpecifiedInterval do
  use GenServer

  @default_interval 5 * 1000 # 5 seconds

  #
  # Client
  #

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts)
  end

  def set_interval(pid, interval) do
    GenServer.cast(pid, {:set_interval, interval})
  end

  #
  # Server
  #

  def init(opts) do
    interval = opts[:interval] || @default_interval
    poll(interval)
    {:ok, {%{arbitrary: "state"}, interval}}
  end

  def handle_cast({:set_interval, new_interval}, {state, _old_interval}) do
    {:noreply, {state, new_interval}}
  end

  def handle_info(:do_thing, {state, interval}) do
    # do whatever thing you do at the interval
    IO.puts "Doing thing at interval #{interval}"
    poll(interval)
    {:noreply, {state, interval}}
  end

  defp poll(interval) do
    IO.puts "Polling at interval #{interval}"
    Process.send_after(self(), :do_thing, interval)
  end
end
2 Likes

It is possible to cancel Process.send_after as mentionned in the docs.

This function returns a timer reference, which can be read with read_timer/1 or canceled with cancel_timer/1.

As it is also possible to read it, You can adjust the next timer with the remaining delta time of the one You want to cancel.

1 Like

Honestly I’d just have a single genserver that holds the time until the ‘next’ thing to process and just timeouts until then, that way if a message comes in to add/remove then it can recalculate what is soonest again and repeat…

2 Likes

think quantum could also be used…

import Crontab.CronExpression

user_id = 4
YourApp.Scheduler.new_job()
|> Quantum.Job.set_name(:"job_user_#{user_id}")
|> Quantum.Job.set_schedule(~e[0 * * * *])
|> Quantum.Job.set_task(fn -> :ok end)
|> YourApp.Scheduler.add_job()

https://hexdocs.pm/quantum/runtime.html

1 Like