defmodule Microwaveprop.Propagation.FreshnessMonitor do @moduledoc """ Monitors propagation score freshness and enqueues grid worker jobs when data is stale. Checks every 5 minutes. Covers missed cron ticks, slow deploys, worker crashes, and any other gap in hourly scoring. """ use GenServer alias Microwaveprop.Propagation alias Microwaveprop.Workers.PropagationGridWorker require Logger @check_interval to_timeout(minute: 5) @stale_threshold_minutes 120 @spec start_link(keyword()) :: GenServer.on_start() | :ignore def start_link(_opts) do if Application.get_env(:microwaveprop, :start_freshness_monitor, true) do GenServer.start_link(__MODULE__, :ok, name: __MODULE__) else :ignore end end @impl true def init(:ok) do send(self(), :check) {:ok, %{}} end @impl true def handle_info(:check, state) do check_freshness() Process.send_after(self(), :check, @check_interval) {:noreply, state} end defp check_freshness do case Propagation.latest_valid_time() do nil -> Logger.info("FreshnessMonitor: no scores found, enqueuing grid worker") enqueue_if_not_queued() latest -> age = DateTime.diff(DateTime.utc_now(), latest, :minute) if age > @stale_threshold_minutes do Logger.info("FreshnessMonitor: scores are #{age}m old, enqueuing grid worker") enqueue_if_not_queued() end end end defp enqueue_if_not_queued do # Oban unique constraint on PropagationGridWorker prevents duplicates — # if a job is already available/scheduled/executing/retryable within the # last hour, insert is a no-op. Oban.insert(PropagationGridWorker.new(%{})) end end