ERA5 poll worker: snooze on transient HTTP errors instead of discarding
A brief CDS network blip on 2026-04-15 returned HTTP 5xx from check_status
for multiple concurrently-polling workers. The old `{:error, _}` branches
consumed attempts, max_attempts=10 burned out in seconds, and 13 rows
were discarded with no cleanup — orphaned with no active poll worker,
including two rows that had both CDS legs done and were ready to decode.
Result: zero era5_profiles ever inserted despite 59 tile-months in flight.
Transient HTTP errors are now a `{:snooze, 60}` so a CDS outage pauses
polling instead of killing it. Terminal states (:failed, :rejected,
:not_found, :stuck) still drive the fail/resubmit paths.
This commit is contained in:
parent
b889761a8e
commit
662d7232e5
2 changed files with 63 additions and 2 deletions
|
|
@ -32,6 +32,12 @@ defmodule Microwaveprop.Workers.Era5PollWorker do
|
|||
# so 5 minutes strikes a balance between responsiveness and noise.
|
||||
@snooze_seconds 5 * 60
|
||||
|
||||
# Snooze delay after a transient CDS HTTP error (5xx, network blip).
|
||||
# Shorter than the normal poll interval so recovery is fast, but a
|
||||
# snooze instead of an attempt so a sustained outage doesn't burn
|
||||
# through max_attempts and orphan the row.
|
||||
@error_snooze_seconds 60
|
||||
|
||||
# A CDS job that's been in 'accepted' state for more than this is stuck
|
||||
# in the server's queue and almost certainly won't run. Observed in
|
||||
# prod: pressure-level completes in ~90 min, but single-level
|
||||
|
|
@ -102,10 +108,12 @@ defmodule Microwaveprop.Workers.Era5PollWorker do
|
|||
fail_row(row, "pressure-level job failed: #{reason}")
|
||||
|
||||
{{:error, reason}, _} ->
|
||||
{:error, "single-level check_status error: #{reason}"}
|
||||
Logger.warning("Era5Poll: transient single-level check_status error (snoozing): #{reason}")
|
||||
{:snooze, @error_snooze_seconds}
|
||||
|
||||
{_, {:error, reason}} ->
|
||||
{:error, "pressure-level check_status error: #{reason}"}
|
||||
Logger.warning("Era5Poll: transient pressure-level check_status error (snoozing): #{reason}")
|
||||
{:snooze, @error_snooze_seconds}
|
||||
|
||||
_both_running_or_one_done ->
|
||||
bump_poll_count(row)
|
||||
|
|
|
|||
|
|
@ -247,6 +247,59 @@ defmodule Microwaveprop.Workers.Era5PollWorkerTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "perform/1 — transient CDS HTTP error" do
|
||||
# Observed in prod 2026-04-15: a brief CDS network blip returned HTTP
|
||||
# 5xx on check_status. The old {:error, _} branches consumed attempts
|
||||
# and max_attempts=10 burned out in seconds, so 13 rows became
|
||||
# orphaned (discarded with no cleanup, no active poll worker). The
|
||||
# fix: treat HTTP errors as transient — snooze without counting an
|
||||
# attempt. Terminal states (:failed, :rejected, :not_found) still
|
||||
# drive the fail/resubmit paths.
|
||||
test "snoozes and keeps the row when single-level check_status returns a transient HTTP error" do
|
||||
row = insert_cds_job()
|
||||
|
||||
Req.Test.stub(Era5Client, fn conn ->
|
||||
cond do
|
||||
conn.method == "GET" and String.contains?(conn.request_path, "cds-single-123") ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(503, ~s({"error":"service unavailable"}))
|
||||
|
||||
conn.method == "GET" and String.contains?(conn.request_path, "cds-pressure-456") ->
|
||||
Req.Test.json(conn, %{"status" => "running"})
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:snooze, snooze_s} =
|
||||
Era5PollWorker.perform(%Oban.Job{args: %{"era5_cds_job_id" => row.id}})
|
||||
|
||||
assert snooze_s > 0
|
||||
assert Repo.get(Era5CdsJob, row.id)
|
||||
end
|
||||
|
||||
test "snoozes and keeps the row when pressure-level check_status returns a transient HTTP error" do
|
||||
row = insert_cds_job()
|
||||
|
||||
Req.Test.stub(Era5Client, fn conn ->
|
||||
cond do
|
||||
conn.method == "GET" and String.contains?(conn.request_path, "cds-single-123") ->
|
||||
Req.Test.json(conn, %{"status" => "running"})
|
||||
|
||||
conn.method == "GET" and String.contains?(conn.request_path, "cds-pressure-456") ->
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(502, ~s({"error":"bad gateway"}))
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:snooze, snooze_s} =
|
||||
Era5PollWorker.perform(%Oban.Job{args: %{"era5_cds_job_id" => row.id}})
|
||||
|
||||
assert snooze_s > 0
|
||||
assert Repo.get(Era5CdsJob, row.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1 — failure" do
|
||||
test "returns {:error, _}, deletes the row, and deletes both CDS jobs when a leg fails" do
|
||||
row = insert_cds_job()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue