feat(npm): bufferedUpdate goroutine helper for interim buffered columns
Related issue: #142 (closed)
📦 What
Implements Step 24 of the npm local plan: the interim bufferedUpdate goroutine helper that mutating npm handlers will call to bump npm_packages.* and repositories.* counter columns off the request path.
bufferedUpdate(ctx, label, fn) runs fn in a fire-and-forget goroutine with defer recover(), so a failed or panicking counter UPDATE is logged and metered but never surfaces to the client — honouring S11's "a failed emission must not fail the request" contract. Each outcome is metered on gitlab_artifact_registry_npm_buffered_counter_updates_total{column, result} (result enum: ok, panic).
This is a deliberately throwaway helper (TODO(buffered)): it has no caller yet and will be replaced by the cross-format buffered-write subsystem (S12 OQ-16 / S23 storage accounting). Backpressure, in-flight bounds, shutdown drain, rate-limiting, and persistent buffering are out of scope per the plan.
✅ Spec coverage
Spec: S11 — Interim implementation of buffered columns
| Behaviour | Test |
|---|---|
Happy path: result=ok, column side-effect observable after the goroutine completes |
TestBufferedUpdate_HappyPath |
Failed fn (non-panic error) logged but not surfaced; counts result=ok |
TestBufferedUpdate_FnError |
Worker panic recovered, process stays up, result=panic increments |
TestBufferedUpdate_PanicSafety |
column argument partitions the metric into distinct series |
TestBufferedUpdate_DistinctColumnsDistinctSeries |
| Detached worker runs on a live context even when the request ctx is canceled | TestBufferedUpdate_DetachesRequestCancellation |
| Metric reaches the scrape path once registered with the LabKit registry | TestRegisterMetrics_ExposedViaGatherer |
🔍 Reviewer notes
Two files fall outside Step 24's declared Files: list — both intentional, surfaced by validation for conscious sign-off:
go.mod: promotesgithub.com/prometheus/client_golangfrom indirect to direct (required —buffered.goimportsprometheusand the test importsprometheus/testutil) and pulls ingithub.com/kylelemons/godebugas an indirect transitive ofprometheus/testutil. Mechanical fallout of the new import, not a scope leak.docs/plans/2026-05-11-npm-local.md: drops the straynpm_buffered_counter_update_duration_secondsfrom Step 22's metrics catalog — a drift correction (this helper emits no duration histogram; latency instrumentation belongs to the future cross-format subsystem). Ships here under the validate-step drift carve-out.
The metric registers against the server's LabKit registry via RegisterMetrics(reg) (the composition root wires it when the first caller lands), not the global default registry that LabKit never scrapes.
🧪 Testing
go test -race ./internal/format/npm/...— pass (6 tests)golangci-lint run— 0 issuesgo vet,gofmt -l, full pre-commit hook chain — clean