fix(managementapi): reset tags_count in the npm bulk whole-package cascade

Why

The npm protocol unpublish paths reset npm_packages.tags_count on the tombstone they commit. The bulk whole-package cascade (NpmPackageManagementDeleter, reached through the npm bulk worker) dispatches no reset. The npm bulk routes merged with feat(managementapi): serve the npm bulk-delete ... (!1631 - merged) • Hayley Swimelar • 19.4, so the divergent path is reachable end to end: until this fix lands, a bulk whole-package delete leaves tags_count stale where the unpublish zeroes it. Raised by vespian_gl on feat(managementapi): serve the npm arm of the v... (!1535 - merged) • Hayley Swimelar • 19.3.

The worker now dispatches the reset the way the unpublish arm does: post-commit through bufferedCounterUpdate, keyed off the composer's marked report so only a committed tombstone settles, ahead of the artifact-deleted emission so a tracker fault cannot skip it.

The single-package management DELETE route (npmPackageDelete, merged to main after this branch was cut) observes the same marked report and dispatches no reset, so that route stays divergent after this MR. Closing it is follow-up work mirroring this arm's pattern, and the tracking issue below stays open until it lands.

Test plan

  • Unit (internal/managementapi/bulk_npm_worker_settlement_internal_test.go): the reset dispatches once per committed tombstone with the batch's namespace and the entry's package id, never for a marked=false entry, before the event emission, and its failure does not fail the batch.
  • Integration (TestIntegration_BulkDeleteNpmWorker_PackagesTagsCountReset): a whole-package bulk delete through the real River-driven worker zeroes tags_count on both drift shapes. A stored 5 past 2 real tags rejects decrement-instead-of-reset, and a drifted 3 with zero tags rejects a removed-tags-gated reset. A resubmit of the drained batch settles nothing. Written first: at the test-only commit both rows fail against the unfixed cascade.
  • go test is green on internal/managementapi, internal/datastore, and cmd/artifact-registry, the npm bulk integration family passes, and the uncapped --build-tags=integration lint reports nothing in the touched files.

No e2e catalog change: the npm bulk-delete scenarios ride the delete-family catalog pass tracked by E2e lifecycle catalogs: one delete-family pass ... (#638) • Unassigned.

Context for LLM agents

Design rationale:

  • The dispatch rides the NpmPackageTagsCountSettler seam and TagsCounts dep the npm versions bulk worker landed on main, rather than a reset-only seam of its own: this branch predated that merge, and the rebase adopted main's shape instead of keeping a strict-subset duplicate interface. The packages arm calls only the reset, because a whole-package cascade never decrements. The settler's doc names both dispatch shapes, per-pass for versions and per-entry for packages.
  • The dispatch is per entry and takes bufferedCounterUpdateMaxInFlight's documented shed-rate arm for worker-side callers. Each reset targets its own npm_packages row, so there is nothing to aggregate per batch, and the arithmetic is what carries the choice: each entry pays a full multi-statement cascade transaction against one point UPDATE by primary key, so in-flight stays far under the 64-slot cap and a 500-row delete_all page sheds nothing. A shed dispatch increments the dropped result label, a series scrapes expose now that the dist-tag delete wired managementapi.RegisterMetrics at the composition root.
  • The Phase 4 plan's Step 36 correction mandates per-batch aggregation for the versions arm of this same worker. The packages arm's per-entry dispatch coexists with that on purpose: per-package resets have no aggregate delta to carry, so the two settlement shapes in one worker are deliberate, not drift.
  • A reset failure is best-effort, matching the unpublish arm: the batch does not fail, and a River retry re-answers marked=false and never re-dispatches. A stale counter on a tombstone is the accepted cost on both surfaces.

Non-goals:

  • The single-package management DELETE arm's missing reset is pre-existing on main and out of scope here. Its fix is the same marked-gated dispatch in npmPackageDelete, landing as follow-up work with its own tests, together with the composer-comment and ResetNpmPackageTagsCount caller-enumeration true-ups.
  • versions_count stays untouched on the tombstone: per ADR-007 it counts soft-deleted versions, and only a hard delete moves it.
  • No constructor nil-guard for the new dep: NewBulkDeleteNpmWorker validates no field, and TestNewBulkDeleteNpmDeps_WiresEverySeam is the established completeness guard for this struct.

Related to #656

Edited by Hayley Swimelar

Merge request reports

Loading
Loading