defmodule Microwaveprop.Workers.PskrCalibrationWorker do @moduledoc """ Hourly cron that builds `Pskr.CalibrationSample` rows by joining PSKR spot density with HRRR atmospheric state and SWPC space weather at a given hour. Default behaviour fires for the previous full hour — by HH:25 UTC the HRRR cycle has analysis available and PSKR's hourly aggregator has flushed its accumulator. Pass `"hour_utc"` in the args map to rerun a specific hour: Microwaveprop.Workers.PskrCalibrationWorker.new(%{"hour_utc" => "2026-05-04T18:00:00Z"}) The sampler is idempotent (UPSERT on `(hour_utc, band, midpoint_lat, midpoint_lon)`) so reruns are safe. Lives on the `:backfill_enqueue` queue rather than spawning a new one — the workload is small (~1 query window, in-memory join, bulk upsert) and cluster-wide single-slot is plenty for one fire per hour. """ use Oban.Worker, queue: :backfill_enqueue, max_attempts: 3, unique: [ period: 3600, states: [:available, :scheduled, :executing, :retryable] ] alias Microwaveprop.Pskr.CalibrationSampler require Logger @impl Oban.Worker def perform(%Oban.Job{args: args}) do hour = parse_hour(args) Logger.info("PskrCalibrationWorker: building samples for #{hour}") count = CalibrationSampler.build_for_hour(hour) Logger.info("PskrCalibrationWorker: upserted #{count} samples for #{hour}") :ok end defp parse_hour(%{"hour_utc" => iso}) when is_binary(iso) do {:ok, dt, _} = DateTime.from_iso8601(iso) dt end defp parse_hour(_) do # Default: the previous fully-completed hour. DateTime.utc_now() |> DateTime.add(-3600, :second) |> Map.put(:minute, 0) |> Map.put(:second, 0) |> Map.put(:microsecond, {0, 0}) end end