Packages
quantum
3.5.0
3.5.3
3.5.2
retired
3.5.1
retired
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.2
3.0.1
3.0.0
3.0.0-rc.3
3.0.0-rc.2
3.0.0-rc.1
2.4.0
2.3.4
2.3.3
2.3.2
2.3.1
retired
2.3.0
retired
2.2.7
2.2.6
2.2.5
retired
2.2.4
retired
2.2.3
retired
2.2.2
retired
2.2.1
retired
2.2.0
retired
2.1.3
retired
2.1.2
retired
2.1.1
retired
2.1.0
retired
2.1.0-beta.1
retired
2.0.4
retired
2.0.3
retired
2.0.2
retired
2.0.1
retired
2.0.0
retired
2.0.0-beta.2
retired
2.0.0-beta.1
retired
1.9.3
retired
1.9.2
retired
1.9.1
retired
1.9.0
retired
1.8.1
retired
1.8.0
retired
1.7.1
retired
1.7.0
retired
1.6.1
retired
1.6.0
retired
1.5.0
retired
1.4.0
retired
1.3.2
retired
1.3.1
retired
1.3.0
retired
1.2.4
retired
1.2.3
retired
1.2.2
retired
1.2.1
retired
1.2.0
retired
1.1.0
retired
1.0.4
retired
1.0.3
retired
1.0.2
retired
1.0.1
retired
1.0.0
retired
Cron-like job scheduler for Elixir.
Current section
Files
Jump to
Current section
Files
lib/quantum/clock_broadcaster.ex
defmodule Quantum.ClockBroadcaster do
@moduledoc false
# Broadcasts the time to run jobs for
use GenStage
require Logger
alias __MODULE__.{Event, InitOpts, StartOpts, State}
@spec start_link(opts :: StartOpts.t()) :: GenServer.on_start()
def start_link(%StartOpts{name: name} = opts) do
__MODULE__
|> GenStage.start_link(
struct!(InitOpts, Map.take(opts, [:start_time, :storage, :scheduler, :debug_logging])),
name: name
)
|> case do
{:ok, pid} ->
{:ok, pid}
{:error, {:already_started, pid}} ->
Process.monitor(pid)
{:ok, pid}
{:error, _reason} = error ->
error
end
end
@impl GenStage
@spec init(opts :: InitOpts.t()) :: {:producer, State.t()}
def init(%InitOpts{
debug_logging: debug_logging,
storage: storage,
scheduler: scheduler,
start_time: start_time
}) do
start_time =
scheduler
|> Module.concat(Storage)
|> GenServer.whereis()
|> storage.last_execution_date()
|> case do
:unknown -> start_time
date -> date
end
|> NaiveDateTime.truncate(:second)
# Roll back one second since handle_tick will start at `now + 1`.
|> NaiveDateTime.add(-1, :second)
:timer.send_interval(1000, :tick)
{:producer,
%State{
time: start_time,
debug_logging: debug_logging,
remaining_demand: 0
}}
end
@impl GenStage
def handle_demand(demand, %State{remaining_demand: remaining_demand} = state) do
handle_tick(%State{state | remaining_demand: remaining_demand + demand})
end
@impl GenStage
def handle_info(:tick, state) do
handle_tick(state)
end
def handle_info(_message, state) do
{:noreply, [], state}
end
defp handle_tick(%State{remaining_demand: 0} = state) do
{:noreply, [], state}
end
defp handle_tick(%State{remaining_demand: remaining_demand, time: time} = state)
when remaining_demand > 0 do
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
{events, new_time} =
Enum.reduce_while(
1..remaining_demand,
{[], time},
fn _, {list, time} = acc ->
new_time = NaiveDateTime.add(time, 1, :second)
case NaiveDateTime.compare(new_time, now) do
:lt ->
{:cont, {[%Event{time: new_time, catch_up: true} | list], new_time}}
:eq ->
{:cont, {[%Event{time: new_time, catch_up: false} | list], new_time}}
:gt ->
{:halt, acc}
end
end
)
events = Enum.reverse(events)
new_remaining_demand = remaining_demand - Enum.count(events)
if remaining_demand > 0 and new_remaining_demand == 0 do
log_caught_up(state)
end
{:noreply, events, %State{state | time: new_time, remaining_demand: new_remaining_demand}}
end
defp log_caught_up(%State{debug_logging: false}), do: :ok
defp log_caught_up(%State{debug_logging: true}),
do:
Logger.debug(fn ->
{"Clock Producer caught up with past times and is now running in normal time",
node: Node.self()}
end)
end