Snooze ERA5 submits when CDS in-flight ceiling is near

CDS enforces a per-user cap of 150 queued jobs. Each successful submit
adds 2 rows to era5_cds_jobs (single-level + pressure-level), so when
the current in-flight count + 2 would exceed (150 - 10 headroom), the
worker snoozes for 5 minutes instead of POSTing to CDS. Stops 'Number
of queued requests is limited to 150' rejects from burning Oban
attempts.
This commit is contained in:
Graham McIntire 2026-04-13 17:30:43 -05:00
parent 07a8db5309
commit 6842a6dd42
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 105 additions and 0 deletions

View file

@ -38,6 +38,16 @@ defmodule Microwaveprop.Workers.Era5SubmitWorker do
@poll_delay_seconds 5 * 60
# CDS enforces a server-side per-user cap of 150 in-flight jobs. Each
# successful submit adds 2 rows to era5_cds_jobs (single-level +
# pressure-level), so we stop submitting once the in-flight count is
# within `@cap_headroom` of the ceiling. Snoozing (instead of failing
# or sleeping) releases the Oban slot immediately and lets the poll
# workers drain in-flight jobs before we push more in.
@cds_ceiling 150
@cap_headroom 10
@snooze_seconds 5 * 60
@impl Oban.Worker
def backoff(%Oban.Job{attempt: attempt}) do
# The submit itself is cheap; retry fast so transient CDS/HTTP blips
@ -65,11 +75,24 @@ defmodule Microwaveprop.Workers.Era5SubmitWorker do
{:ok, :already_submitted}
cds_queue_full?() ->
in_flight = Repo.aggregate(Era5CdsJob, :count, :id)
Logger.info(
"Era5Submit: CDS in-flight cap reached (#{in_flight}/#{@cds_ceiling}) — snoozing #{year}-#{pad(month)} tile #{tile_lat},#{tile_lon}"
)
{:snooze, @snooze_seconds}
true ->
submit_and_persist(year, month, tile_lat, tile_lon)
end
end
defp cds_queue_full? do
Repo.aggregate(Era5CdsJob, :count, :id) + 2 > @cds_ceiling - @cap_headroom
end
defp submit_and_persist(year, month, tile_lat, tile_lon) do
days = Calendar.ISO.days_in_month(year, month)
area = tile_area(tile_lat, tile_lon)

View file

@ -124,6 +124,88 @@ defmodule Microwaveprop.Workers.Era5SubmitWorkerTest do
end
end
describe "CDS in-flight cap" do
# CDS rejects with "Number of queued requests is limited to 150" when
# a user has too many pending jobs. Each submit adds 2 rows to
# era5_cds_jobs (single-level + pressure-level), so we snooze when
# the in-flight count is close to the ceiling.
@cap_headroom 10
@cds_ceiling 150
test "snoozes when era5_cds_jobs count is at or above the ceiling - headroom" do
# Seed enough in-flight rows to push us over the snooze threshold.
seed_count = @cds_ceiling - @cap_headroom
rows =
for i <- 1..seed_count do
%{
id: Ecto.UUID.generate(),
year: 2014,
month: 3,
tile_lat: 32,
tile_lon: -98 - i,
single_job_id: "seed-single-#{i}",
pressure_job_id: "seed-pressure-#{i}",
submitted_at: DateTime.truncate(DateTime.utc_now(), :second),
inserted_at: DateTime.truncate(DateTime.utc_now(), :second),
updated_at: DateTime.truncate(DateTime.utc_now(), :second),
poll_count: 0
}
end
Repo.insert_all(Era5CdsJob, rows)
Req.Test.stub(Era5Client, fn _ -> raise "CDS should not be contacted when capped" end)
Oban.Testing.with_testing_mode(:manual, fn ->
assert {:snooze, seconds} = Era5SubmitWorker.perform(%Oban.Job{args: @default_args})
assert is_integer(seconds) and seconds > 0
end)
# No new era5_cds_jobs row inserted for the requested tile.
refute Repo.one(
from j in Era5CdsJob,
where: j.year == 2014 and j.month == 3 and j.tile_lat == 32 and j.tile_lon == -98
)
end
test "submits normally when in-flight count is well below the ceiling" do
# 10 in-flight — comfortably under the cap.
rows =
for i <- 1..10 do
%{
id: Ecto.UUID.generate(),
year: 2013,
month: i,
tile_lat: 40,
tile_lon: -90,
single_job_id: "warm-single-#{i}",
pressure_job_id: "warm-pressure-#{i}",
submitted_at: DateTime.truncate(DateTime.utc_now(), :second),
inserted_at: DateTime.truncate(DateTime.utc_now(), :second),
updated_at: DateTime.truncate(DateTime.utc_now(), :second),
poll_count: 0
}
end
Repo.insert_all(Era5CdsJob, rows)
Req.Test.stub(Era5Client, fn conn ->
dataset_id =
cond do
String.contains?(conn.request_path, "single-levels") -> "fresh-single"
String.contains?(conn.request_path, "pressure-levels") -> "fresh-pressure"
end
Req.Test.json(conn, %{"jobID" => dataset_id})
end)
Oban.Testing.with_testing_mode(:manual, fn ->
assert {:ok, %Era5CdsJob{}} = Era5SubmitWorker.perform(%Oban.Job{args: @default_args})
end)
end
end
describe "unique constraint" do
test "collapses duplicate (year, month, tile_lat, tile_lon) Oban enqueues" do
Oban.Testing.with_testing_mode(:manual, fn ->