chore(maven): shed a refused upload-session open as 503 (upload-session-concurrency-cap plan: 5/12)

What

The Maven artifact PUT answered 500 internal_server_error for every refused upload-session open, saturation included. A refused-because-saturated open now answers 503 with service_unavailable, its canonical detail, and a jittered Retry-After. Every other refused open keeps the 500.

writeSessionOpenError classifies at the call site rather than in a mapper, because internal/format/maven/upload.go handed the raw error to writeStorageError, which writes 500 unconditionally with no classification.

Why

This is Step 5 of the upload-session concurrency cap. storage.ErrUploadSessionsBusy landed in !2408 (merged) with no handler arm on the Maven deploy route, so a shed would surface as a server fault and tell the client nothing about when to come back.

Nothing changes on main today. The gate's Acquire is unwired until Step 11, so this arm is unreachable and no request can reach the 503 yet.

The Retry-After window

The header renders 28 to 42 seconds: a base of 35 s with a one-fifth half-width drawn around it.

The base is priced on the precedent inlineBuildTimeoutRetryAfter sets (rebuildTimeout plus five seconds) rather than reusing that constant, whose doc prices it against a rebuild worker's lifetime. It is held as a literal, so a reprice of the rebuild cannot move a client-visible retry. The comment says so, because a comment claiming the derivation would go stale in silence.

The window matches the npm publish arm of the same plan, so a reader can carry the number between the two packages. Tests assert bounds rather than an exact value, per the plan's rule for every rendering step, and also assert a render below the base. Without that second assertion the bounds admit a one-sided draw whole, and a revert to base + rand.N(jitter) would pass green.

Spec

docs/specs/S10-maven-hosted.md now records the 503 at all four sites that enumerate this route's statuses: the PUT response-status table, the Step 4c failure-modes summary, the format-specific error-code catalog, and Error Cases.

The plan's Step 5 file list does not name the spec. It is here because a reader of any one of those tables would otherwise conclude the route cannot answer 503. The GET table keeps its 500: the gate is consulted when a session opens, and only PUT opens one.

Test plan

  • TestStreamAndCommitPrimary_RefusedOpenAnswersItsCause pairs the two refusals a session open takes. The sentinel gets 503, service_unavailable and a Retry-After; every other error keeps 500, internal_server_error and advertises no Retry-After. Both arms assert the cause stays out of the body, and that request_id echoes the real correlation ID.
  • TestStreamAndCommitPrimary_ShedRetryAfterCarriesTheJitter checks the spread reaches the header, and that at least one render falls below the base.
  • TestStreamAndCommitPrimary_ShedStillBoundsTheDrainWindow keeps the shed on the refused-open return, where the unread body's drain bound is armed.
  • TestMavenDeadlineOpenFailStore_NeedsAnError reaches the refusal store's own guard, which no case that sets err can.

Each new guard was mutation-checked: dropping the jitter, reverting to the one-sided draw, and widening the sentinel narrowing each redden a case.

Guardrails

  • Merge order. Depends on !2408 (merged), which is merged. Step 11 depends on this step and edits the same file for its defer release(), so this lands first.
  • e2e scenarios. None added. The plan states that Steps 1 to 10 add no scenario, because none of them changes observable behavior, and that Step 11 adds the concurrent-push-past-the-cap scenario. That is where the client's reaction to the 503 gets established rather than assumed.
  • Conformance. Not run. The arm is unreachable until Step 11 wires the acquire, so no conformance case can reach it.
  • Size. 276 reviewable LOC across 6 files: 50 source, 221 test, 5 docs. Under the 500 ceiling.
  • ADRs. ADR-003 is the governing one and is met rather than deviated from. ADR-004's per-user and per-namespace upload-session limits are a different control from this global memory bound.

Stack

Step MR State
2. admission gate primitive and shed sentinel !2408 (merged) merged
5. Maven publish shed arm this MR open

Steps 1, 3, 4, and 6 to 12 have no MR open yet. This step depends only on Step 2, so those do not block it.

Related to #1035

Merge request reports

Loading
Loading