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 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 # Only enqueue if there isn't already a pending/running grid worker job import Ecto.Query pending = Microwaveprop.Repo.exists?( from(j in "oban_jobs", where: j.worker == "Microwaveprop.Workers.PropagationGridWorker" and j.state in ["available", "scheduled", "executing"] ) ) if !pending do Oban.insert(PropagationGridWorker.new(%{})) end end end