Single-target remote DELETE routes emit no counter delta: mavenRemotePackageEvict and its five siblings discard the composers' amounts
What is wrong
Six single-target management-API DELETE route arms evict one cached npm or Maven row and emit no storage-accounting counter delta.
Each arm calls a datastore composer that reports the amount a delta needs, and then discards that amount with _.
While no part-2 merge request is merged, an eviction through one of these six routes moves the row and moves no counter.
repositories.artifacts_count then reads too high after a package or version eviction, and repositories.size_bytes reads too high after a file eviction.
Both values stay high until a reconciliation pass recomputes the scope.
Once the emit lands at all six arms, each committed eviction moves its own delta on the next drain tick of the buffered counter pipeline.
The bulk delete workers for the same two formats already take these amounts. So the same eviction moves a counter through the bulk route and moves none through the single-target route.
The six arms
Measured at 710c99957cc99358cf3f1f330fab054c2d892915.
| Format | Symbol | Declared at | Discards the amount at |
|---|---|---|---|
| Maven | mavenRemotePackageEvict |
internal/managementapi/package_delete.go:174 |
internal/managementapi/package_delete.go:202 |
| npm | npmRemotePackageEvict |
internal/managementapi/package_delete.go:306 |
internal/managementapi/package_delete.go:330 |
| Maven | mavenRemoteVersionEvict |
internal/managementapi/version_delete.go:208 |
internal/managementapi/version_delete.go:236 |
| npm | npmRemoteVersionEvict |
internal/managementapi/version_delete.go:331 |
internal/managementapi/version_delete.go:355 |
| Maven | mavenRemoteFileEvict |
internal/managementapi/file_delete.go:266 |
internal/managementapi/file_delete.go:312 |
| npm | npmRemoteFileEvict |
internal/managementapi/file_delete.go:450 |
internal/managementapi/file_delete.go:492 |
The four package and file arms discard an amount.
The two version arms discard applied, which gates the emit rather than sizing it, because a version mark hides exactly the row it names.
What each arm owes, and what it must not move
The delta set splits by level, not by format. npm and Maven owe the same amounts at the same three levels.
| Level | What the write does | repositories.artifacts_count |
repositories.size_bytes |
namespace_statistics.components_count |
|---|---|---|---|---|
| Package | UPDATE ... SET soft_deleted_at |
-N, where N is the count of live cached versions the mark hid |
Must not move | Must not move |
| Version | UPDATE ... SET soft_deleted_at |
-1 |
Must not move | Must not move |
| File | hard DELETE of the row and its attachment |
Must not move | -S, and only when the transaction removed the repository's last reference to that blob |
Must not move |
So the asymmetry for npm and Maven is not one asymmetry.
At a mark, artifacts_count moves and size_bytes must not.
At a file hard delete, size_bytes moves and artifacts_count must not.
This differs from the Container arm.
The Container remote eviction hard-deletes no file row, so it owes Δartifacts alone, and #775 (closed) carries that table.
Evidence for the table
internal/managementapi/artifact_write.go declares the two shapes and writes them:
deleteCounterTargetTombstoneat line 181 covers a package or version mark. Its doc states that it movesartifacts_countalone, and that the footprint counters settle at the purger's reap.deleteCounterTargetFileat line 191 covers the Maven and npm file hard-deletes. Its doc states that no artifact count moves, and that repository bytes free exactly when the delete removed the repository's last reference to the blob.emitDeleteCountersat line 244 writes those two arms, and it emits no namespace delta for either one.deleteCounters.needsDispatchat line 225 drops a zero-amount emit before it spends a dispatch slot. So a still-referenced blob and an idempotent re-mark cost nothing.
The two bulk arms that !2003 (merged) delivered pass exactly these amounts, and their comments carry the reason:
BulkDeleteNpmWorker.evictRemotePackageatinternal/managementapi/bulk_npm_worker_remote.go:391. Its comment reads: "components_count and size_bytes both stay put: the mark leaves every counted row for the reap, and the size recompute reads soft_deleted_at at no level."BulkDeleteNpmWorker.evictRemoteVersionatinternal/managementapi/bulk_npm_worker_remote.go:509.BulkDeleteNpmWorker.evictRemoteFileatinternal/managementapi/bulk_npm_worker_remote.go:589.MavenBulkWorker.remoteOpsatinternal/managementapi/bulk_maven_worker.go:594passes the same pair through its three write wrappers. Its comment reads: "components_count waits for the reap: its count is physical."
Where the amounts come from
The composers report them already, and their doc comments name the caller obligation:
MavenRemoteEvictor.EvictMavenRemotePackageatinternal/datastore/maven_remote_eviction.go:57— "applied and hiddenVersions feed the caller's storage-accounting emit, counted inside the mark's transaction so the purger cannot take the decrement."NpmRemoteEvictor.EvictNpmRemotePackageatinternal/datastore/npm_remote_eviction.go:65— the same sentence.MavenRemoteEvictor.DeleteMavenRemoteFileatinternal/datastore/maven_remote_eviction.go:143— "freedSizeBytes is the blob's size when this transaction removed the repository's last reference, probed over recomputeMavenRemoteFilesSizeStmt's row set."NpmRemoteEvictor.DeleteNpmRemoteFileatinternal/datastore/npm_remote_eviction.go:146— the same shape, over the npm union.MavenRemoteEvictor.EvictMavenRemoteVersionatinternal/datastore/maven_remote_eviction.go:104andNpmRemoteEvictor.EvictNpmRemoteVersionatinternal/datastore/npm_remote_eviction.go:107reportappliedalone, by the same reasoning.
No new dependency is needed at the route arms.
All six already hold a CounterEmitter:
Deps.Countersatinternal/managementapi/handler.go:538reaches the two Maven arms that takeDeps, atpackage_delete.go:174andversion_delete.go:208.npmPackageDeleteStores.countersatinternal/managementapi/package_delete.go:52.versionDeleteStores.countersatinternal/managementapi/version_delete.go:91.fileDeleteStores.countersatinternal/managementapi/file_delete.go:82, which serves the Maven and the npm file arm.
emitDeleteCountersDetached at internal/managementapi/artifact_write.go:300 is the helper the other single-target routes use.
Ordering the emit
The hosted arms fix the shape, and the remote arms take it.
mavenHostedPackageDelete writes the 202 first, returns when the composer reports no applied mark, and emits after that (internal/managementapi/package_delete.go:156-171).
A remote arm owes the same three steps.
A remote eviction emits no usage-data event at any level.
This work adds none.
MavenBulkWorker.remoteOps states the reason at internal/managementapi/bulk_maven_worker.go:642-644: an eviction removes a cached copy of an upstream's artifact, so no usage event is owed.
Relationship to #775 (closed) and !2003 (merged)
- #775 (closed) — #775 (closed). This is the parent record for the whole gap. Its Scope section gives the npm and Maven arms to !2003 (merged) and keeps the Container remote eviction arm for itself.
- !2003 (merged) — !2003 (merged). It widened the six datastore composers, added the two last-reference probes, and wired the two bulk delete workers. It merged on 2026-08-27, and it named itself part 1 of two.
- This issue is that stated part 2. It takes the six single-target route arms and their tests, and nothing else.
!2003 (merged)'s merged description states the remainder:
This is part 1 of two
Part 1 widens the composers, the two probes, and the two bulk delete workers.
The six single-target remote
DELETEroutes still emit no counter at all:
internal/managementapi/package_delete.go:202(Maven) and:330(npm)internal/managementapi/version_delete.go:236(Maven) and:355(npm)internal/managementapi/file_delete.go:312(Maven) and:492(npm)Each of those call sites takes the new return values and discards them with
_. A run against the live service confirmed it. All six routes answered202, the target rows changed, and no counter moved after two drain intervals.Part 2 adds the emit at those six routes, with their tests. Part 2 cannot land first, because the composers report no amount until part 1 lands.
This branch also removes the last in-code pointer to the issue.
git grep -- '#775' -- '*.go'returns nothing at the tip, againstinternal/managementapi/bulk_maven_worker.go:595at the merge base. This section is therefore the only remaining pointer to the work part 2 owes.
The six line numbers that quote names are unchanged at 710c99957cc99358cf3f1f330fab054c2d892915.
One correction to #775 (closed), which this issue records rather than fixes. The "Merge order: this issue outlives !2003 (merged)" section of #775 (closed) says that what remains after !2003 (merged) merges is the Container arm alone. !2003 (merged)'s own merged description names the six single-target npm and Maven arms as remaining too. !2003 (merged) is the later document and it is specific about which call sites it did not touch, so this issue follows !2003 (merged). Issue #775 (closed) keeps the Container arm, and this issue keeps the six.
Documentation this work owes
docs/dev/storage-accounting.mdlines 1872 and 1873 point at work item 775 and say that the Maven and npm remote eviction arms emit noΔartifacts. That sentence speaks for those arms as a whole. Once the six route arms emit, the page needs the narrower statement. !2003 (merged) states that this page belongs to part 2, and it did not change it.docs/testing/e2e/maven.md:141anddocs/testing/e2e/npm.md:199each carry the sentence "The single-target remoteDELETEroutes move no counter, and reconciliation is what corrects those rows." Both sentences become false when the emit lands, so both rows need the new outcome.docs/testing/e2e/maven.md:196promises oneartifact_registry_artifact_deletedevent per committed hosted delete, and states that the remote leg emits none at the bulk job and at the single-target routes alike. That row needs no change, because this work adds no usage event.
Tests this work owes
Model them on the two bulk tables !2003 (merged) added, narrowed to a route arm.
At 710c99957cc99358cf3f1f330fab054c2d892915, internal/managementapi/artifact_delete_counters_test.go carries no remote arm, so every case below is new:
- An applied package eviction emits one repository delta
(-N, 0), for each format. - An applied version eviction emits one repository delta
(-1, 0), for each format. - A file eviction that removed a row emits
(0, -S)on the repository's last reference to the blob, for each format. - A still-referenced blob emits nothing, and not a zero-valued delta.
- An idempotent re-mark emits nothing, and a file eviction that found the row already gone emits nothing.
- No arm emits a namespace delta at any level.
- A mark emits no size delta, and a file eviction emits no artifact delta.
- A request that answers
404moves no counter.
Cover all six arms. The two package arms and the two file arms carry different amounts from the two version arms, and each arm has its own route.
Related to #775 (closed)
This is a bot message