diff --git a/lib/microwaveprop/propagation/freshness_monitor.ex b/lib/microwaveprop/propagation/freshness_monitor.ex index 91e08280..1faf7db8 100644 --- a/lib/microwaveprop/propagation/freshness_monitor.ex +++ b/lib/microwaveprop/propagation/freshness_monitor.ex @@ -54,9 +54,11 @@ defmodule Microwaveprop.Propagation.FreshnessMonitor do 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. + # PropagationGridWorker declares `unique:` over a 1-hour window on + # the seed args (`%{}`), so repeated 5-minute ticks during a long + # outage collapse into a single seed job — not one stacked chain + # per tick. `Oban.insert` returns `{:ok, job}` either way; the + # `conflict?` field on the returned job distinguishes the two. Oban.insert(PropagationGridWorker.new(%{})) end end diff --git a/lib/microwaveprop/workers/propagation_grid_worker.ex b/lib/microwaveprop/workers/propagation_grid_worker.ex index 22104fb1..e94db331 100644 --- a/lib/microwaveprop/workers/propagation_grid_worker.ex +++ b/lib/microwaveprop/workers/propagation_grid_worker.ex @@ -24,7 +24,16 @@ defmodule Microwaveprop.Workers.PropagationGridWorker do # (e.g., a rolling deploy that kills a mid-flight chain step) # don't exhaust the chain's retry budget and discard the whole # run. Legitimate scoring errors still give up after 5 attempts. - max_attempts: 5 + max_attempts: 5, + # Deduplicate identical jobs across a 1-hour window. Uniqueness + # is over the full args set, so the seed (`%{}`) collapses with + # itself — FreshnessMonitor's 5-minute ticks during a long outage + # no longer stack 24 redundant f00-f18 chains — while chain steps + # with distinct `run_time` + `forecast_hour` args remain distinct + # from each other and from the seed. The period is the same as + # the hourly cron interval so successive hourly cron fires move + # past the window naturally. + unique: [period: 3600, states: [:available, :scheduled, :executing, :retryable]] alias Microwaveprop.Commercial alias Microwaveprop.Propagation diff --git a/test/microwaveprop/propagation/freshness_monitor_test.exs b/test/microwaveprop/propagation/freshness_monitor_test.exs index 414011b2..78280f4c 100644 --- a/test/microwaveprop/propagation/freshness_monitor_test.exs +++ b/test/microwaveprop/propagation/freshness_monitor_test.exs @@ -95,11 +95,12 @@ defmodule Microwaveprop.Propagation.FreshnessMonitorTest do end) end - test "repeated ticks on a persistently stale grid each enqueue another job" do - # Characterization note: the module comment claims an Oban unique - # constraint on PropagationGridWorker suppresses duplicates, but - # the worker currently declares no `unique:` option. Each tick - # therefore inserts a fresh job. + test "repeated ticks on a persistently stale grid are deduplicated into a single job" do + # PropagationGridWorker declares `unique:` on the seed args (`%{}`), + # so a stale-check tick that runs while an earlier seed is still + # available/scheduled/executing/retryable is a no-op. Without this, + # a long outage stacks one expensive HRRR f00-f18 chain per 5-min + # tick (e.g. a 2-hour outage = 24 redundant seed jobs). ScoresFile.write!(@band_mhz, minutes_ago(180), @sample_scores) Oban.Testing.with_testing_mode(:manual, fn -> @@ -107,7 +108,7 @@ defmodule Microwaveprop.Propagation.FreshnessMonitorTest do FreshnessMonitor.handle_info(:check, %{}) FreshnessMonitor.handle_info(:check, %{}) - assert 3 == length(all_enqueued(worker: PropagationGridWorker)) + assert 1 == length(all_enqueued(worker: PropagationGridWorker)) end) end