The :propagation Oban queue runs with concurrency 2, so the grid
worker chain step and the 10-minute ASOS nudge can (and do) execute
in parallel. PipelineStatus.current/0 used to return only the
most-recently-started worker, which made the chip label flip to
"ASOS nudge" at every :00 / :10 tick even though HRRR was still
chugging through forecast hours behind it.
Replace the single :detail field with a list of running_detail
maps, each tagged with :grid or :asos so the chip can:
- Render one sub-line per currently-executing worker
- Sort them deterministically (grid before asos) so the eye
doesn't see rows swap
- Still override the grid entry with the forecast-hour progress
label when a propagation_pipeline_progress broadcast arrives
191 lines
5.5 KiB
Elixir
191 lines
5.5 KiB
Elixir
defmodule Microwaveprop.Propagation.PipelineStatusTest do
|
|
use Microwaveprop.DataCase, async: false
|
|
|
|
alias Microwaveprop.Propagation.PipelineStatus
|
|
alias Microwaveprop.Repo
|
|
|
|
@grid_worker "Microwaveprop.Workers.PropagationGridWorker"
|
|
@asos_worker "Microwaveprop.Workers.AsosAdjustmentWorker"
|
|
|
|
# Bypass Oban's normal insert pipeline (which executes inline in test)
|
|
# by writing directly to the Oban.Job schema. The SQL sandbox keeps
|
|
# Oban's executor from seeing these rows.
|
|
defp insert_oban_job(overrides) do
|
|
now = DateTime.utc_now()
|
|
|
|
defaults = %{
|
|
state: "available",
|
|
queue: "propagation",
|
|
worker: @grid_worker,
|
|
args: %{},
|
|
attempt: 0,
|
|
max_attempts: 3,
|
|
inserted_at: now,
|
|
scheduled_at: now
|
|
}
|
|
|
|
attrs = Map.merge(defaults, Map.new(overrides))
|
|
Repo.insert!(struct!(Oban.Job, attrs))
|
|
end
|
|
|
|
defp minutes_ago(n) do
|
|
DateTime.add(DateTime.utc_now(), -n * 60, :second)
|
|
end
|
|
|
|
describe "current/0" do
|
|
test "returns :unknown when no pipeline jobs have ever run" do
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :unknown
|
|
assert status.last_update_at == nil
|
|
assert is_binary(status.label)
|
|
end
|
|
|
|
test "returns :running with a single grid-worker detail when only PropagationGridWorker is executing" do
|
|
insert_oban_job(%{
|
|
state: "executing",
|
|
worker: @grid_worker,
|
|
attempted_at: minutes_ago(5)
|
|
})
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :running
|
|
assert status.label == "Updating propagation"
|
|
assert [%{worker: :grid, label: grid_label}] = status.details
|
|
assert grid_label =~ "HRRR"
|
|
end
|
|
|
|
test "returns :running with a single ASOS detail when only AsosAdjustmentWorker is executing" do
|
|
insert_oban_job(%{
|
|
state: "executing",
|
|
worker: @asos_worker,
|
|
attempted_at: minutes_ago(1)
|
|
})
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :running
|
|
assert status.label == "Updating propagation"
|
|
assert [%{worker: :asos, label: asos_label}] = status.details
|
|
assert asos_label =~ "ASOS"
|
|
end
|
|
|
|
test "returns :running with BOTH workers when grid + ASOS are executing at the same time" do
|
|
# Queue concurrency was bumped to 2 so the chain-step grid worker
|
|
# and the 10-minute ASOS nudge can run in parallel. The chip
|
|
# should show one sub-line per running worker, in a deterministic
|
|
# grid-before-asos order so the eye doesn't see them flicker.
|
|
insert_oban_job(%{
|
|
state: "executing",
|
|
worker: @grid_worker,
|
|
attempted_at: minutes_ago(5)
|
|
})
|
|
|
|
insert_oban_job(%{
|
|
state: "executing",
|
|
worker: @asos_worker,
|
|
attempted_at: minutes_ago(1)
|
|
})
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :running
|
|
assert status.label == "Updating propagation"
|
|
assert [grid, asos] = status.details
|
|
assert grid.worker == :grid
|
|
assert grid.label =~ "HRRR"
|
|
assert asos.worker == :asos
|
|
assert asos.label =~ "ASOS"
|
|
end
|
|
|
|
test "returns :idle with Up to date label when last grid run completed recently" do
|
|
completed = minutes_ago(15)
|
|
|
|
insert_oban_job(%{
|
|
state: "completed",
|
|
worker: @grid_worker,
|
|
attempted_at: completed,
|
|
completed_at: completed
|
|
})
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :idle
|
|
assert status.last_update_at
|
|
assert status.label =~ "Up to date"
|
|
end
|
|
|
|
test "returns :stale when last completed was more than 120 minutes ago" do
|
|
stale = minutes_ago(180)
|
|
|
|
insert_oban_job(%{
|
|
state: "completed",
|
|
worker: @grid_worker,
|
|
attempted_at: stale,
|
|
completed_at: stale
|
|
})
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :stale
|
|
assert status.label =~ "stale"
|
|
end
|
|
|
|
test "prefers :running over :idle when a job is executing even if a recent job completed" do
|
|
recent = minutes_ago(5)
|
|
|
|
insert_oban_job(%{
|
|
state: "completed",
|
|
worker: @grid_worker,
|
|
attempted_at: recent,
|
|
completed_at: recent
|
|
})
|
|
|
|
insert_oban_job(%{
|
|
state: "executing",
|
|
worker: @grid_worker,
|
|
attempted_at: minutes_ago(1)
|
|
})
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :running
|
|
end
|
|
|
|
test "ignores completed jobs from unrelated workers" do
|
|
# WeatherFetchWorker enriches individual QSOs — it's not part of
|
|
# the CONUS grid update pipeline, so its recent success should
|
|
# not make the map page show 'Up to date'.
|
|
completed = minutes_ago(5)
|
|
|
|
insert_oban_job(%{
|
|
state: "completed",
|
|
worker: "Microwaveprop.Workers.WeatherFetchWorker",
|
|
queue: "weather",
|
|
attempted_at: completed,
|
|
completed_at: completed
|
|
})
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :unknown
|
|
end
|
|
end
|
|
|
|
describe "running_detail/1" do
|
|
test "forecast hour 0 renders as 'now'" do
|
|
assert PipelineStatus.running_detail(%{forecast_hour: 0}) == "now"
|
|
end
|
|
|
|
test "positive forecast hours render as +Nh" do
|
|
assert PipelineStatus.running_detail(%{forecast_hour: 1}) == "+1h"
|
|
assert PipelineStatus.running_detail(%{forecast_hour: 18}) == "+18h"
|
|
end
|
|
|
|
test "returns nil when no forecast hour is provided" do
|
|
assert PipelineStatus.running_detail(nil) == nil
|
|
assert PipelineStatus.running_detail(%{}) == nil
|
|
end
|
|
end
|
|
end
|