Scheduling jobs with Quantum (or just GenServer)

A lot of people ask which library is my go-to for scheduling jobs in Elixir. To be honest, I don't usually need any 3rd-party deps for this!

Just using GenServer

This site has a scheduler that runs daily at midnight and gives a point of "green elixir" to members each day and other users once a week. I named the module Campsite.Scheduler (and included it in the children block of application.ex:

defmodule Campsite.Scheduler do
  use GenServer

  @full_day 3600 * 24 * 1000
  @admin_green_rate 5

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{}, name: CampScheduler)
  end

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

  def handle_info(:daily, state) do
    if Date.day_of_week(DateTime.utc_now()) == 1 do
      award_green_to_all()
    else
      award_green_to_members()
    end

    schedule_daily()
    {:noreply, state}
  end

  # ... internal app logic

  defp schedule_daily() do
    {:ok, midnight} = Time.new(0, 0, 0)
    ms_past_midnight = abs(Time.diff(midnight, Time.utc_now(), :millisecond))
    ms_until_midnight = @full_day - ms_past_midnight
    Process.send_after(self(), :daily, ms_until_midnight)
  end
end

All in all, it's not very complex, but there is some arithmetic to calculate the time until midnight. In an app where there are a lot of cron-like calculations, it might make sense to reach for erlcron.

Using Quantum

A newer option is quantum. It's built on top of GenServer and GenStage and comes with a convenient cron-like syntax and quite a bit of robustness out of the box. To use it...

  • add {:quantum, "~> 3.0"} (or the current version) to your deps in mix.exs
  • create a scheduler module with use Quantum, otp_app: :app_name
  • configure the new scheduler in config.exs to add jobs to it

Example scheduler:

defmodule Campsite.QuantumScheduler do
  use Quantum, otp_app: :campsite
end

Example config.exs block for the above scheduler:

config :campsite, Campsite.QuantumScheduler,
  jobs: [
    # prints the time (truncated to the second) every 3 seconds
    {{:extended, "*/3 * * * * *"}, fn ->
      DateTime.utc_now()
      |> DateTime.truncate(:second)
      |> IO.puts()
    end},
  ]
Back to index

No Comments