npm remote tarball: a coalescing 503 answers the sweep interval, not S13's Retry-After: 1

The gap

A coalescing failure on the npm remote tarball route answers Retry-After derived from health_check.scheduled_interval. The two metadata routes answer 1 on the same condition.

docs/specs/S13-virtual-remote-foundation.md, #### Retry-After on a coalescing failure, fixes the value at 1 for ErrLeaderCanceled and ErrSingleFlightTimeout, states it as the same value for Maven, npm and Container, and forbids substituting the sweep interval for it. The tarball route does not follow that rule.

Why the routes differ

The metadata routes reach writeRemoteProxyError (internal/format/npm/remote_read_errors.go), whose two coalescing arms pass remoteCoalescingRetryAfterSeconds.

The tarball route never gets there for those sentinels. remoteTarballBackpressure (internal/format/npm/remote_tarball.go) intercepts ErrSingleFlightTimeout, ErrLeaderCanceled and ErrFillAbandoned ahead of the shared mapping and answers through RemoteTarballHandler.writeUnavailable, which passes remoteReadPolicy.retryAfterSeconds — the sweep interval.

The split is deliberate in the sense that it is pinned, not in the sense that anything argues for it: cmd/artifact-registry/wire_npm_remote_boot_integration_test.go asserts 1 for the dist-tags follower and the interval for the tarball follower on the identical condition, with a comment recording that the tarball route writes its own 503.

What it costs

A client coalesced onto a tarball fill that loses its leader backs off by the sweep interval (300 at the 5m default) when the condition it hit clears as soon as the next request elects a fresh leader. The same client on a packument backs off by 1. Nothing about the condition is route-specific.

Decide which way to reconcile

Either is defensible and the choice is not obvious, which is why this is a separate item rather than a drive-by fix:

  • Route the three sentinels through the shared mapping so the tarball answers 1, matching S13 and the metadata routes. Note that 1 invites an immediate retry against a repository whose fill capacity is by definition saturated, which is the condition remoteTarballBackpressure exists to name.
  • Keep the interval and amend S13 to carve the tarball out, saying why a fill slot's contention is an interval-scale condition rather than a one-second one.

Documentation state

docs/specs/S15-npm-remote.md's Error Cases table scopes its coalescing row to the metadata routes and points here for the tarball, so the spec and the code agree today. Whichever way this is resolved, that row and the paragraph under it change with it.

Related to #785 (closed) Related to #778