Upload session concurrency cap Step 1: config surface for the cap and the wait

Part of #1035: upload session concurrency cap. A global cap over the population that holds object-storage staging buffers, enforced in internal/storage where the sessions are opened, with a bounded wait that ends in a 503 UNAVAILABLE rather than an unbounded park. It changes no commit protocol, no path layout, and no session row shape.

References: plan (see "Step 1"). No spec owns this control: docs/dev/storage.md is the present-tense authority for BlobStore and Session. The plan's research is pinned at 655743520da9fd935d7884db34ad074e6046013c, so re-derive any count, line, or file list before acting on it.

Type: feat | Depends on: none | MR title marker: (upload-session-concurrency-cap plan: 1/12)

Scope

Add storage.max_concurrent_upload_sessions and storage.upload_session_wait_timeout. The cap's default is derived at boot from the live GOMEMLIMIT and the effective driver chunk_size, floored at the largest number of sessions one operation holds concurrently. No enforcement yet.

Files

  • proto/artifactregistry/config/v1/config.proto (Modify): two fields on the storage message, each taking the next free field number in that message, chosen and confirmed against main at implementation time.
  • internal/config/storage.go (Modify): the two StorageConfig fields; the derivation, dividing the budget by the per-driver per-slot reservation, which is 1.2 * chunk_size + 1MiB for GCS and 2 * chunk_size + 1MiB for S3; the budget fraction and the unset-GOMEMLIMIT fallback as named constants rather than inlined in the expression; the floor and the invariant behind it; an injectable memory-limit source (a func() int64 seam mirroring the clock seams elsewhere in the tree) so the cases stay hermetic instead of mutating a process global; startup rejection of a non-positive value for either field, which is what licenses Step 2's panic; a clamp on the wait budget to a ceiling derived from rebuildTimeout, with a boot warning naming the configured value and the clamped one; and one INFO line at boot naming the derived cap, the live GOMEMLIMIT, the effective driver chunk_size, and whether the floor bound.
  • config.example.yaml (Modify): both keys under storage, each carrying the "raise it, there is no disable" note in its comment, since this is the file an operator edits.
  • docs/dev/configuration-reference.md (Modify): only what is true at this step's merge, because that file documents as-is behavior and this step enforces nothing. Both rows, each stating that a non-positive value is refused; how the default is derived at boot, including the chunk_size coupling on the storage.gcs and storage.s3 chunk_size rows; and the wait budget's relationship to single_flight_wait_timeout and rebuildTimeout, including the ceiling it is clamped to and that a value above it is clamped with a warning rather than refused. The rows that describe the control refusing work move to Step 11, which is the step that makes them true.
  • gen/** (Modify): regenerated.

Acceptance

Boot rejects 0 for either field with an error naming it. A wait budget above the rebuildTimeout-derived ceiling is clamped to it and logs a warning naming both values, asserted as the clamped value rather than as the absence of an error, which is the case that reddens if the clamp is deleted. With both omitted and GOMEMLIMIT at 400 MiB the derivation yields exactly 28 on the GCS default and 2 on the S3 default, asserted as those two numbers rather than as "different caps". A chunk_size at or above the whole budget yields the floor and boot does not panic, which is what makes the floor falsifiable, since deleting it turns that config into a boot panic on a default self-managed S3 install. The boot warning fires when the floor binds, naming the derived cap and the budget it exceeds, and does not fire when the derivation fits. An unset GOMEMLIMIT yields the 400 MiB fallback rather than a cap derived from math.MaxInt64. The boot INFO line carries the derived cap and all three inputs, asserted on the record rather than on the absence of an error.

Tests

internal/config/storage_test.go (Modify), covering both driver defaults, both rejections, the floor case, the fallback, and an explicit override surviving the derivation.

Forecast

~180 source, ~470 test, ~650 total. Over the 500-LOC ceiling by ~150 for the limit-source seam, the coupling prose, and the boot log and warning; justify it in the MR description with the LOC split by file group.