docs(specs): add S28 garbage collection spec
Why
S28 is the garbage-collection platform foundation: format-agnostic reclamation of storage that is no longer referenced or has expired. Every format reduces to the same blob layer, so reclamation is one shared mechanism, not per-format cleanup. This spec is the mechanism contract: the per-namespace online deferred model, the coordination protocol that prevents premature deletion (permanent data loss), and the gc:* job family on River (S27 / S27-A). The model and its rationale are recorded in ADR-025, merged via Add Artifact Registry ADR 025: Garbage Collection (gitlab-com/content-sites/handbook!20362 - merged) • Hayley Swimelar.
What reviewers should know
- Correctness rests on one serialization point: a
FOR UPDATElock on theblob_storage_blobsrow, shared by the push and dedup-skip paths and the GC delete path, with a canonical multi-row lock order. Both sides are S28 deliverables: the push-side lock lands in the OCI handlers in Phase 3, not as an external prerequisite. - Acceptance criteria are phased. Phases 1-2 (capture, discard) cannot lose data. The irreversible object delete (Phase 3) sits behind a hard entry gate, including a numeric
review_delayvalidation read from the capture-to-re-reference histogram the no-delete phases already emit, and a bound on the dedup-skip re-upload hold. - GC computes no reachability and deletes no manifest. The attachment count is the reference count, S12's cascade decides which config and layer attachments a manifest delete removes, and GC reclaims what that leaves at zero. ADR-025 records materialized manifest-to-layer edges as a rejected alternative.
- Two points diverge from the resolved design by operator decision: namespace purge is deferred entirely (no platform caller exists), and the admin status read carries queue health only (byte accounting is S22's domain).
- Three of the five Open Questions are resolved or corroborated against container-registry production data (details in a separate internal note). The AR-native
review_delayre-validation remains a Phase 3 gate. The delete-attempt marker stays open, recorded in both this spec and ADR-025.
Test plan
- markdownlint and Vale: 0 errors.
- Both mermaid diagrams render.
- Read the spec against ADR-025, ADR-010, ADR-008, ADR-011, ADR-002, ADR-022, and S06 / S12 / S27 / S27-A for consistency.
Spec status is Approved. Merging this MR is the approval signal, per the spec-MR guardrail.
Context for LLM reviewers
Design rationale and rejected alternatives:
- App-level capture (in
BlobStorageAttachmentStoreplus a new forbidigo ban) over database triggers: a missed capture is a recoverable leak the reconcile scan collects, not data loss, so observability and testability win. Triggers remain the flip if formats add un-funneled delete paths. - No reachability in GC: the attachment count is the reference count, so GC needs no manifest-to-layer edges and parses no manifest. ADR-025's Alternatives section records materialized edges as rejected (an edge table may still earn a place as a format-layer optimization, which is the OCI format's decision, not GC's). Earlier drafts of this spec adopted materialized edges. The merged ADR and the current spec both drop them, so treat any materialized-edges claim as stale.
FOR UPDATEon the blob row over an advisory lock: the row is the durable invariant, so the lock has referential meaning and reuses the in-treelockManifestRowidiom.
Reconciliation state:
- The spec is reconciled with the merged ADR-025, synced in-tree at
docs/adr/025_garbage_collection.md. The ADR says "set aside" for the state this spec names quarantine. They are the same state, internal to GC's queue and unrelated to the product's artifact-quarantine capability. - The dedup-skip re-upload hold bound is a Phase 3 entry-gate criterion in both documents, not a follow-up.
Non-goals (deliberately out of scope):
- S20 soft-delete and restore, and S29 policy evaluation: S28 reclaims what they leave unreferenced and defines the boundary only.
- S22 storage byte accounting: the admin read reports queue health, not live or pending byte totals.
- Namespace purge: deferred entirely until a platform namespace-deletion flow exists.