Artifact Registry ADR 007: shadow table's namespaces cascade

What

Records that blob_storage_blobs_by_namespace.namespace_id references namespaces(id) with ON DELETE CASCADE, records that blob_storage_blobs deliberately takes no such reference, and corrects statements the shadow table's foreign key makes false.

  • The Option B diagram tagged namespace_id as FK with no referential action, and no prose declared one. It is now PK,FK with the action spelled out, matching how the namespace_statistics diagram already annotates its own cascade.
  • The namespace_statistics entry claimed its foreign key was "the only references namespaces(id) in this schema carrying ON DELETE CASCADE". There are now two.
  • The blob_storage_blobs diagram and entry now state that the table carries no namespaces(id) reference by design, and why. An earlier revision of this MR recorded the opposite; see Why.

Why

The cascade sits on the same side of the rule stated for repositories as the one it joins — cascade where the children are pure structure, reject where they are user data. A shadow row is a derived copy of a blob_storage_blobs row rather than user data in its own right, so a namespace delete should take it rather than be blocked by it.

A shadow row's lifetime is bounded by the shorter of two, enforced by two reapers: the AFTER DELETE trigger when garbage collection reclaims its blob_storage_blobs row, and this cascade when the namespace goes. Whichever fires first is correct, because a shadow row exists only to make a per-namespace read cheap and both events end the possibility of that read. The shadow is a read accelerator derived from blob_storage_blobs, not a replica of it, so the two are not meant to share a lifetime.

This MR reversed direction on blob_storage_blobs itself. An earlier revision added a references namespaces(id) to its diagram and described the deployed schema as behind this ADR until that reference landed. The opposite is true. blob_storage_blobs owns object_storage_key, the only handle to the stored object, and ADR-025 has garbage collection delete that object before the row it came from — so a cascade would destroy the handle while the object was still live, leaving a storage orphan that S28's own risk table lists with no remediation, recoverable only by the storage enumeration ADR-025 rejects. A blocking reference is no better: it would stall every namespace deletion behind a garbage-collection cycle that is deliberately deferred and rate-limited. The absence is therefore a decision, and S28's namespace-purge resolution already depends on it ("there is no namespace-to-blob cascade").

A namespace hard-delete therefore removes the shadow's rows and leaves the blob_storage_blobs rows they copy, until garbage collection reclaims those on its own schedule. No reader observes the gap — repositories and blob_storage_attachments both reference namespaces without a referential action, so the delete cannot proceed until every repository and attachment under the namespace is gone, and the namespace_statistics row that would drive a reconciliation pass cascades away in the same statement.

Follows handbook!20738 (the namespace_statistics cascade and partitioning exception) and handbook!20699 (the shadow table's index set).

The Artifact Registry side ships the migration that creates the table with this foreign key. docs/adr/ in that repo is synced from here by a daily job, so this is the upstream half.

Related to gitlab-org/ops/artifact-registry#515

Edited by Pawel Rozlach

Merge request reports

Loading
Loading