fix(datastore): report the repository and freed bytes from the package reap
Summary
MavenPackageReaper.Reap now fills ReapTotals.RepositoryID and ReapTotals.SizeBytes, so a hosted Maven package purge decrements repositories.size_bytes at the reap instead of leaving the column high until the next reconciliation pass.
The cause.
The arm's three-leg walk — the maven_versions rows with their files, the version-less files, then the package row — freed rows whose bytes the column still counted, and the arm reported neither field: it dropped the digests reapMavenFilePage handed back and made no resolveReapRepository call, so emitChunkCounters issued the component delta alone.
The upload side raises the column (dispatchHandler.emitCommittedCounters in internal/format/maven/upload_emit.go), so the drift ran one direction only.
The change.
reapRepositoryRouteMavenPackageand its resolve statement, walkingmaven_packagestomaven_repositories— the npm package route's shape.EXPLAIN (ANALYZE, BUFFERS)against the migrated schema: two primary-key index scans (maven_packages_p38_pkey,maven_repositories_p38_pkey), each pruned to one hash partition bynamespace_id, nested loop, 4 shared-buffer hits, ~1 ms.- The resolve runs before the legs, while the package chain stands; a repository walk's row declines the figure (
TombstoneRow.SkipFreedBytes), so that route pays for neither probe statement. - One
sumFreedRepositoryBytesprobe over the digests of both legs, positioned ahead of every return: the version leg's page alone at the not-drained exit, both legs' pages before the package delete. A digest carried by a file of both legs reaches the probe twice; theDISTINCTinside the statement counts it once. - The
maven_versions/maven_packageslevel order inDefaultPurgeLevelsis recorded as unordered: the walk drains the versions itself (no foreign key orders the pair), and both arms report the figure (no counter does either) — unlike the npm pair, whose order is load-bearing. - The storage-accounting doc, the S20-A spec's gap entries, and the e2e counters row stop recording this arm as one that computes no byte figure.
Merge order.
Stacked on !2040 (merged) (the maven_packages scan level and the walk this fix builds on); this MR targets that branch and retargets to main when it lands.
!2040 (merged) itself rebases onto S20-A step 20b per that plan's merge order, so this branch inherits that rebase.
The two new error wraps (resolve and sum failures) are reachable only through statement failures and follow the family's untested-by-convention status: no arm in this family — npm version, npm package, maven version — tests its equivalent wrap.
Governing ADRs
ADR-007 — the figure is the repository-scoped one ADR-007 defines, deduplicated within the repository; the survivor statement walks maven_files through maven_package_id, the route RepositoryReconcileStore.RecomputeSizeBytes takes, so a version-less file counts; the emit pair (SizeBytes != 0 && RepositoryID != uuid.Nil) is what the counter contract makes emittable.
Checked against ADR-004 (the entity-count table bounds the probe-cost reasoning) and ADR-025 (blob reclamation unchanged).
No deviation.
Testing
- A new byte-report suite,
TestMavenPackageReaper_Reap_ReportsTheRepositoryScopedBytes, covers the arm the way the npm package arm's suite covers its own: the survivor pair (a digest another package's file still carries frees nothing; one nothing carries frees its blob's bytes, through the version-less leg), the cross-repository copy, the arm's own duplicate shape (a version-ful and a version-less file at one digest, counted once), and the version-leg early exit, where the page fills the budget and the figure is over what that page freed.TestArtifactReapers_Reap_NameNoRepositoryOnADrainedSubtreegains the maven package case, pinninguuid.Nilon a gone chain. - The pinned zero flips: the drained-whole case asserts the figure over blobs seeded at non-zero sizes and the repository read before the reap; four whole-struct assertions gain the repository; the no-version zero becomes a measured one.
The red runs were demonstrated against the branch's parent before the implementation landed (the two
test(...)commits). - The conservation drive flips to the size half: the chunks' summed
SizeBytesequals the blobs the package held. - The guard suite's statements table gains the new resolve as a positive hit.
- Full local runs against a postgres carrying the CI service options (
max_locks_per_transaction=1024):internal/datastore/migrations,internal/datastore/schemas, andinternal/lifecyclesuites fully green; theinternal/datastoresuite ran 45 minutes with zero test failures and timed out midTestNpmPackageStore_CascadeSoftDeleteNpmPackage's 5000-row fixture, a test this branch does not touch — CI'stest:integration:datastoreshards that suite and carries the full gate. The default testcontainers lock table 53200s on cascading cleanup deletes locally, which is why these runs useARTIFACT_REGISTRY_DATABASE_TEST_DSNagainst a service-shaped postgres. - Lint:
golangci-lint run --build-tags=integration internal/datastore/... internal/lifecycle/... --max-same-issues=0 --max-issues-per-linter=0 --uniq-by-line=falsereports nothing on the touched lines;check-comment-capspasses.
Post-deploy monitoring
No additional operational monitoring required: the change adds a counter decrement at an existing emit site on a background worker.
A wrong figure surfaces as repositories.size_bytes drift against the hourly reconciliation recompute — the same signal that surfaced the gap — and the purge worker's existing purge_outcome metric carries any statement failure the new probe could raise.
Closes #1012 (closed)