chore(datastore): single metadata row reaper (S20-A Maven metadata purge plan: 2/5)
What
Adds an unregistered reaper for one hosted Maven package-level maven-metadata.xml
tombstone, with attachment cleanup and freed-byte reporting. It is not wired into
discovery, dispatch, or the purge worker yet — Steps 3 and 5 of the plan add those.
MavenMetadataFileReaper.Reapdeletes the exact addressed file row plus its attachment, on the caller's transaction, and reportsReapTotals{Components: 0, Rows, RepositoryID, Drained: true}. Freed-byte accounting reuses the existing hosted-Maven-family walk inlifecycle_reap_size.govia a new file-keyed route (reapRepositoryRouteMavenFile).- A missing row (already reaped by a package or repository purge) is a drained no-op, not an error, per the plan's Acceptance entry.
- A new
mavenMetadataReapAttachmentDeleteFailsgofail seam covers the attachment-delete failure rollback;.gitlab-ci.yml/.mise.tomlwire it into the lifecycle failpoint selectors. scripts/ci/check-comment-caps.shwidensis_directive()to exempt// gofail:comments, needed for the new failpoint comment to pass the comment-cap ratchet; test coverage for that widening is included.
Size justification
This diff is 779 changed lines at 029373920 (700 insertions, 79 deletions),
above the 500 LOC guideline and above the plan's own 350–500 forecast for this
step.
| Group | Files | Lines |
|---|---|---|
| Integration tests | lifecycle_reap_maven_metadata_integration_test.go, lifecycle_reap_maven_integration_test.go |
349 |
| Unit tests | lifecycle_reap_maven_metadata_test.go, lifecycle_reap_size_test.go |
86 |
| Source | lifecycle_reap_maven_metadata.go, lifecycle_reap_maven.go, lifecycle_reap_size.go, query_names.go |
200 |
| Docs | storage-accounting.md, go-testing.md |
86 |
| CI/tooling | .gitlab-ci.yml, .mise.toml, check-comment-caps.sh, check-comment-caps_test.sh |
58 |
Every figure counts changed lines (insertions plus deletions) against
origin/main, so the groups sum to the 779 above. The lifecycle_reap_maven.go
and go-testing.md lines are comment and documentation moves, not new logic:
the review rounds relocated rationale out of comment blocks the caps charge in
full.
Tests are 435 of the 779 lines (56%). The plan's Research Findings cite the precedent fix at "about 5.7 test lines per source line," and Step 2's Tests entry enumerates 11 required scenarios (live rows, version-level metadata, another file name, another namespace, shared attachments, a digest surviving elsewhere, a new live row at the same coordinate, duplicate delivery, two concurrent reapers, the file-keyed resolve guard, and the attachment-failure rollback) plus the CI wiring that backs the last one. None separates cleanly from the reaper's transaction and concurrency contract without leaving part of it unverified.
Related to #1167 (closed)
Database Review Evidence
Queries
Note
Plans are from EXPLAIN (ANALYZE, BUFFERS) against an ephemeral
PostgreSQL 17 container (matching GL_PG_CURR_VERSION from
.gitlab-ci-other-versions.yml), with synthesized seed data rolled
back per query and the container torn down at the end of the run.
Numbers reflect moderate cardinality and do not capture
production-scale effects. See
Database review evidence
for seed sizing, methodology, and the anomalies the skill flags.
Expand each row's details for the seed shape, rendered SQL, bound args,
and raw plan.
| Method | Plan node | Index | Rows (plan / actual) | Cost | Time | Buffers (hit / read) | Partitions |
|---|---|---|---|---|---|---|---|
datastore.Reap |
Delete | maven_files_p18_namespace_id_soft_deleted_at_metadata_idx |
1 / 1 | 8.15 | 0.013ms | 4 / 0 | maven_files 1/64 |
datastore.resolveReapRepository |
Limit | maven_files_p14_pkey |
1 / 1 | 10.35 | 0.016ms | 5 / 0 | maven_files 1/64, maven_packages 1/64, maven_repositories 1/64 |
datastore.Reap
Summary: Plan matches the method's intent: the exact-id, still-tombstoned
single-row DELETE prunes to one of 64 hash partitions on the bound
namespace_id and rides the metadata discovery index
(namespace_id, soft_deleted_at), with the remaining predicate terms as an
in-index filter over the one tombstoned row in the partition. Estimate and
actual agree (1 / 1) and execution is 0.013ms over 5000 seeded rows in the
partition. No anomalies.
Seed shape: namespaces=1, repositories=1, maven_repositories=1, maven_packages=1, blob_storage_blobs=1, blob_storage_attachments=1, maven_files=5000 (one namespace partition; 1 tombstoned maven-metadata.xml target + 4999 live siblings, all sharing the one attachment)
Rendered SQL:
DELETE FROM public.maven_files
WHERE ((((maven_files.namespace_id = $1::uuid) AND (maven_files.id = $2::uuid)) AND (maven_files.maven_version_id IS NULL)) AND (maven_files.file_name = $3::text)) AND (maven_files.soft_deleted_at IS NOT NULL)
RETURNING maven_files.blob_storage_attachment_id AS "maven_files.blob_storage_attachment_id",
maven_files.blob_sha256 AS "maven_files.blob_sha256";Bound args: [446aed8a-362d-72f3-8f01-a2ea48ae451e, a490e017-da81-706e-82b5-aa54d66079aa, maven-metadata.xml]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Delete on maven_files (cost=0.12..8.15 rows=1 width=10) (actual time=0.012..0.013 rows=1 loops=1)
Delete on maven_files_p18 maven_files_1
Buffers: shared hit=4
-> Index Scan using maven_files_p18_namespace_id_soft_deleted_at_metadata_idx on maven_files_p18 maven_files_1 (cost=0.12..8.15 rows=1 width=10) (actual time=0.008..0.008 rows=1 loops=1)
Index Cond: ((namespace_id = '446aed8a-362d-72f3-8f01-a2ea48ae451e'::uuid) AND (soft_deleted_at IS NOT NULL))
Filter: ((maven_version_id IS NULL) AND (id = 'a490e017-da81-706e-82b5-aa54d66079aa'::uuid) AND (file_name = 'maven-metadata.xml'::text))
Buffers: shared hit=2
Planning:
Buffers: shared hit=294
Planning Time: 0.648 ms
Execution Time: 0.175 msTimings: planning 0.648ms, execution 0.175ms, total 0.823ms.
datastore.resolveReapRepository (route reapRepositoryRouteMavenFile)
Summary: Plan matches the method's intent: the file-to-repository resolve
opens with a primary-key seek on maven_files (id, namespace_id) pruned to
one of 64 hash partitions, then walks two nested-loop joins into
maven_packages and maven_repositories, each binding the partition key and
pruning to one partition. The Seq Scan nodes on the two ancestor partitions
scan 1-row tables by seed design, so they are the optimal node, not a missing
index. Estimate and actual agree (1 / 1) and execution is 0.016ms. No
anomalies.
Seed shape: namespaces=1, repositories=1, maven_repositories=1, maven_packages=1, blob_storage_blobs=1, blob_storage_attachments=1, maven_files=5000 (one namespace partition)
Rendered SQL:
SELECT r.repository_id
FROM maven_files f
JOIN maven_packages p
ON p.id = f.maven_package_id AND p.namespace_id = f.namespace_id
JOIN maven_repositories r
ON r.id = p.maven_repository_id AND r.namespace_id = p.namespace_id
WHERE f.namespace_id = $1 AND p.namespace_id = $1 AND r.namespace_id = $1
AND f.id = $2
LIMIT 1Bound args: [0c660044-ba16-7cac-a80e-a5fdbae9097a, 6fc23c98-ca54-767e-b3b6-34d664e63582]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Limit (cost=0.28..10.35 rows=1 width=16) (actual time=0.015..0.016 rows=1 loops=1)
Buffers: shared hit=5
-> Nested Loop (cost=0.28..10.35 rows=1 width=16) (actual time=0.014..0.015 rows=1 loops=1)
Join Filter: (r.id = p.maven_repository_id)
Buffers: shared hit=5
-> Nested Loop (cost=0.28..9.33 rows=1 width=32) (actual time=0.012..0.012 rows=1 loops=1)
Join Filter: (p.id = f.maven_package_id)
Buffers: shared hit=4
-> Index Scan using maven_files_p14_pkey on maven_files_p14 f (cost=0.28..8.30 rows=1 width=32) (actual time=0.008..0.008 rows=1 loops=1)
Index Cond: ((id = '6fc23c98-ca54-767e-b3b6-34d664e63582'::uuid) AND (namespace_id = '0c660044-ba16-7cac-a80e-a5fdbae9097a'::uuid))
Buffers: shared hit=3
-> Seq Scan on maven_packages_p14 p (cost=0.00..1.01 rows=1 width=48) (actual time=0.004..0.004 rows=1 loops=1)
Filter: (namespace_id = '0c660044-ba16-7cac-a80e-a5fdbae9097a'::uuid)
Buffers: shared hit=1
-> Seq Scan on maven_repositories_p14 r (cost=0.00..1.01 rows=1 width=48) (actual time=0.002..0.002 rows=1 loops=1)
Filter: (namespace_id = '0c660044-ba16-7cac-a80e-a5fdbae9097a'::uuid)
Buffers: shared hit=1
Planning:
Buffers: shared hit=243
Planning Time: 0.844 ms
Execution Time: 0.034 msTimings: planning 0.844ms, execution 0.034ms, total 0.878ms.