maven_packages tombstones reach no purger scan, the hosted sibling of #728

The gap

maven_packages carries a soft_deleted_at writer and a discovery index, and no purger scan reads it.

Table Writer Discovery index Scan level
maven_packages MavenBulkMarkers.MarkMavenPackage, internal/datastore/maven_bulk_markers.go:47 present none

The writer is production-reachable: the management API's Maven bulk delete worker calls it.

LifecycleScanStore carries eight scans on main, and none reads maven_packages: ScanTombstonedRepositories, ScanTombstonedNpmPackages, ScanTombstonedContainerImages, ScanTombstonedMavenVersions, ScanTombstonedMavenRemoteVersions, ScanTombstonedNpmVersions, ScanTombstonedNpmRemoteVersions, ScanTombstonedContainerRemoteManifests.

MavenPackageReaper exists at internal/datastore/lifecycle_reap_maven.go:301, and the repository walk is its only caller.

The mark does not cascade

markMavenPackageTx marks the package row alone. It reads the live version count through CountLiveVersions for the accounting delta and marks none of those versions:

// internal/datastore/maven_bulk_markers.go — markMavenPackageTx
res, err := instrumentExec(ctx, queryMavenPackagesUpdateMark,
	softDeleteMavenPackageStmt(namespaceID, mavenPackageID), tx)
...
hiddenVersions, err = m.versions.CountLiveVersions(ctx, tx, namespaceID, mavenPackageID)

So ScanTombstonedMavenVersions finds nothing under the mark, and the upward leg from a version has no tombstoned row to start from. The whole subtree stays: every maven_versions row, every maven_files row, and every one of their blob_storage_attachments rows.

ADR-025 reclaims a blob when its attachment count reaches zero, so an unreaped subtree blocks reclamation with no error and no metric.

Relationship to #728

This is the hosted-Maven sibling of #728, which covers the same shape for npm_remote_packages, container_remote_images and maven_remote_packages. #728's title and table are scoped to remote cache tables and do not name maven_packages. The S20-A spec's Follow-ups entry for that obligation names only the three remote tables as well.

internal/lifecycle/purge_integration_test.go:530-534 is the one place in the tree that lists all four together:

maven_packages, maven_remote_packages, npm_remote_packages and container_remote_images each carry a discovery index and no scan level, and are reached by walking down from a repository tombstone instead.

The walk is not a route today

That test's "reached by walking down from a repository tombstone" is true of the design and not of main. ScanTombstonedRepositories requires repositories.soft_deleted_at IS NOT NULL, and the only writer of that column, RepositoryStore.SoftDelete, has test callers only. The destructive repository DELETE that writes it is plan step 18, still open.

So while step 18 is unmerged, a Maven package tombstone is reachable by nothing at all. Once step 18 merges, it is reachable only when someone also destroys the whole repository.

What closes it

Either of two shapes, and choosing between them is part of the work:

  1. A maven_packages scan level, in the shape ScanTombstonedNpmPackages uses, plus a downward walk from the package tombstone, because the mark leaves the versions live. This mirrors what #728 asks for on the remote side.
  2. Cascade the mark, so MarkMavenPackage tombstones the versions beneath it and the existing ScanTombstonedMavenVersions level reaps them. This changes what the accounting delta counts, so it needs S22's emit contract checked against it.

npm's hosted path already answers this differently: SoftDeleteNpmPackage has a cascade and ScanTombstonedNpmPackages is a scan level, so npm has both halves and Maven has neither.

Acceptance criteria

  1. A maven_packages row tombstoned through the management API's bulk delete is reaped without a repository tombstone.
  2. Its maven_versions, maven_files and blob_storage_attachments rows are gone after the reap, so a blob the package held reaches zero attachments.
  3. Whichever shape is chosen, the counter deltas the reap emits agree with what the mark reported hidden.

👋 Triage note (automated): This issue has been classified as type::bug. The maven_packages tombstone purger scan is missing — a production-reachable code path marks rows as soft-deleted but no lifecycle scan ever picks them up, meaning the subtree (versions, files, blob attachments) is never reaped and blob reclamation is silently blocked. This is clearly a functional defect rather than a feature request or maintenance task.

The following labels have been confirmed/applied:

  • type::bug
  • Category:Artifact Registry
  • devops::package
  • group::package registry

If this classification doesn't look right to you, please feel free to update the labels and let us know!

Edited by Triage and Label issues