Revisit defaultMaxSessionLifetime once the upload session concurrency cap gates sessions

What

Rule on whether defaultMaxSessionLifetime remains the right bound on an upload session's total life once the upload session concurrency cap gates upload sessions, and whether the value needs a configuration surface.

Why

internal/storage/pg_session.go declares defaultMaxSessionLifetime as 6 * 24 * time.Hour at ccd1fe9cb, and PgBlobStore.sessionExpiry clamps every expires_at restamp at created_at + maxSessionLifetime. internal/storage/session_expiry.md carries the two-layer design: expires_at is the ADR-004 inactivity deadline, restamped by sizeBytesCASStmt whenever the compare-and-swap persists bytes the session did not have before, and this lifetime is the backstop restamping cannot push past.

Six days was chosen for resumability, at a time when an idle session cost an upload_sessions row and its staging bytes. The concurrency cap changes that cost. Each PATCH opens a fresh per-request budget — the route's own read timeout, 1h for container.upload_read_timeout and maven.upload_read_timeout and 30m for npm publish (defaultPublishReadTimeout), all at ccd1fe9cb — so a client trickling chunks sequentially re-occupies its place for as long as it keeps sending, bounded only by the session lifetime.

While Step 11 of the concurrency cap plan (#1241 (closed)) is unmerged, that costs a row and staging bytes and nothing more. Once it merges, each of those requests holds a unit of the admission gate, so one such client occupies one of the derived units — 28 at the GCS default, per the plan's ### The derived cap at the defaults — for up to six days, while the shed arms answer 503 to everyone else. That is the interaction this issue exists to price.

What a ruling looks like

Either outcome closes this:

  • The value stands. Record the reason here. No code change.
  • The value changes, or gains an operator knob. That is more than a constant edit: maxSessionLifetime is settable only through WithMaxSessionLifetime today, config.example.yaml carries no session keys, and the config proto names upload_session_ttl only to record that the field was removed from the S12 surface. Adding one touches proto/artifactregistry/config/**, internal/config/** and config.example.yaml, so guardrail 14 requires docs/dev/configuration-reference.md in the same MR, documenting as-is behavior only.

Evidence worth gathering before deciding: the age distribution of real upload_sessions rows. If no session has ever survived beyond minutes, six days is academic; if resumable pushes genuinely span hours, the interaction above is worth pricing against the derived cap.

Out of scope

  • The cap itself, and its wiring — #1035 and its step tasks own that.
  • expires_at's inactivity semantics, which ADR-004 fixes.

Related to #1254 and #1035.