defmodule Belay.Clock do @moduledoc """ Injectable time source. Everything time-dependent in the engine (backoff, lease expiry, rate windows, await deadlines, cron slots) reads through this, so tests advance a `Belay.Clock.Sim` instead of sleeping. """ @callback now(term()) :: DateTime.t() @doc "Read the current time from a clock module or `{module, argument}` pair." @spec now(module() | {module(), term()}) :: DateTime.t() def now({mod, arg}), do: mod.now(arg) def now(mod) when is_atom(mod), do: mod.now(nil) end defmodule Belay.Clock.System do @moduledoc "Real time." @behaviour Belay.Clock @impl true def now(_), do: DateTime.utc_now() end defmodule Belay.Clock.Sim do @moduledoc "A settable, advanceable clock for deterministic tests." @behaviour Belay.Clock @doc "Start a simulated clock at `start`." @spec start_link(DateTime.t()) :: Agent.on_start() def start_link(start \\ ~U[2026-01-01 00:00:00.000000Z]) do Agent.start_link(fn -> start end) end @impl true def now(pid), do: Agent.get(pid, & &1) @doc "Advance the simulated clock by a number of seconds." @spec advance(Agent.agent(), number()) :: :ok def advance(pid, seconds) do Agent.update(pid, &DateTime.add(&1, round(seconds * 1_000_000), :microsecond)) end @doc "Set the simulated clock to an exact value." @spec set(Agent.agent(), DateTime.t()) :: :ok def set(pid, %DateTime{} = at), do: Agent.update(pid, fn _ -> at end) end