feat(npm): dispatcher + prerequisites for real handlers (S11 Step 26)
Part 1 of 2 of the npm hosted plan — Step 26: wire the format route table to the real handlers.
🗂️ Stacked MRs
Split into 2 stacked MRs to reduce review load. Each part targets the previous one (part 1 → main); review and merge bottom-up. Both are test-heavy — reviewable LoC is ~1.6k (part 1) / ~1.1k (part 2).
- feat(npm): dispatcher + prerequisites for real ... (!835 - merged) • Dzmitry (Dima) Meshcharakou • 19.2
👈 - feat(npm): wire route table to real handlers (S... (!836 - merged) • Dzmitry (Dima) Meshcharakou • 19.2
📦 What this part adds
The npm dispatcher's real-handler constructor and the three prerequisites the composition root (part 2) consumes, so that MR is a pure wiring change. Production still serves the interim 501 — the base NewDispatchHandler stays mounted; the real handlers are exercised here only through spy-based dispatch tests.
NewDispatchHandlerWithHandlers+npmRouteHandlers+buildMux— the route table injecting every per-action handler, with the sharedPUT /{package_name}route split by body shape.sniffPublishEnvelopepeeks the top-level_attachmentskey (bounded tomaxEnvelopeSniffBytes, never buffering the tarball) and reconstructs the full body; the deprecate branch re-capsr.Bodyat that bound since deprecate decodesr.Bodydirectly (publish re-wraps via its ownMaxBytesReader).NewHandleris renamedNewDispatchHandler.server.SetUploadBody— the setter symmetric withOriginalBody, so the split can re-publish the reconstructed body without truncation.NpmVersionStore.BumpLastDownloadedAt(and itsNpmPackageStoretwin) — the tarball-downloadlast_downloaded_atbump primitive.public_registry_url— an optional absolute-URLNpmConfigfield (proto, loader, validation).
Test coverage: spy-based per-route wired/unwired dispatch, the publish/deprecate body-shape split, the sniff memory bound and byte-exact reconstruction past the cap, config round-trip and validation, and datastore bump happy-path, soft-delete no-op, and argument guards.
Database Review Evidence
Queries
Note
Plans are from EXPLAIN (ANALYZE, BUFFERS) against an ephemeral
PostgreSQL 17 container (matching GL_PG_CURR_VERSION from
.gitlab-ci-other-versions.yml), with synthesized seed data rolled
back per query and the container torn down at the end of the run.
Numbers reflect moderate cardinality and do not capture
production-scale effects. See
Database review evidence
for seed sizing, methodology, and the anomalies the skill flags.
Expand each row's details for the seed shape, rendered SQL, bound args,
and raw plan.
| Method | Plan node | Index | Rows (plan / actual) | Cost | Time | Buffers (hit / read) | Partitions |
|---|---|---|---|---|---|---|---|
datastore.NpmPackageStore.BumpLastDownloadedAt |
Update → Seq Scan | n/a | 1 / 1 | 0.00..1.75 | 1.053ms | 33 / 0 | 1 |
datastore.NpmVersionStore.BumpLastDownloadedAt |
Update → Seq Scan | n/a | 1 / 1 | 0.00..1.75 | 0.641ms | 29 / 0 | 1 |
datastore.NpmPackageStore.BumpLastDownloadedAt
Summary: Plan matches the method's intent — a single-row Update targeting one namespace partition (npm_packages_p27) via Seq Scan with a (soft_deleted_at IS NULL, namespace_id, id) filter; 49 of 50 partition rows removed, 1 updated, namespace_id pruning to one of 64 partitions. Seq Scan (rather than pk_npm_packages) is cost-optimal at 50 partition rows and mirrors the decrement UPDATEs in !778 (merged); at production cardinality the planner switches to the PK index. The 1.053ms execution is dominated by a one-time FK-check-trigger plan-cache warm-up (fk_npm_packages_npm_repository_id_npm_repositories: time=0.849, first invocation in the session). No anomalies.
Seed shape: namespaces=1, repositories=1, npm_repositories=1, npm_packages=50
Rendered SQL:
UPDATE public.npm_packages
SET last_downloaded_at = NOW()
WHERE ((npm_packages.namespace_id = $1::uuid) AND (npm_packages.id = $2::uuid)) AND (npm_packages.soft_deleted_at IS NULL);Bound args: [<namespace-uuid>, <npm-package-uuid>]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Update on npm_packages (cost=0.00..1.75 rows=0 width=0) (actual time=0.089..0.089 rows=0 loops=1)
Update on npm_packages_p27 npm_packages_1
Buffers: shared hit=33
-> Seq Scan on npm_packages_p27 npm_packages_1 (cost=0.00..1.75 rows=1 width=18) (actual time=0.002..0.004 rows=1 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (namespace_id = '8a22fa59-bba7-4b9d-9339-60b7dbd2c410'::uuid) AND (id = 'b69a089c-8d0a-4f8e-854d-946e4e214579'::uuid))
Rows Removed by Filter: 49
Buffers: shared hit=1
Planning:
Buffers: shared hit=59
Planning Time: 0.195 ms
Trigger for constraint fk_npm_packages_namespace_id_namespaces on npm_packages_p27: time=0.026 calls=1
Trigger for constraint fk_npm_packages_npm_repository_id_npm_repositories on npm_packages_p27: time=0.849 calls=1
Execution Time: 1.053 msTimings: planning 0.195ms, execution 1.053ms, total 1.248ms.
datastore.NpmVersionStore.BumpLastDownloadedAt
Summary: Identical plan shape to the package-side twin — a single-row Update on the pruned npm_versions_p55 partition via Seq Scan with the (soft_deleted_at IS NULL, namespace_id, id) filter; 49 of 50 rows removed, 1 updated. Seq Scan is cost-optimal at 50 partition rows and flips to the PK index at production cardinality. Execution 0.641ms, again dominated by the one-time FK-check-trigger warm-up (fk_npm_versions_npm_package_id_npm_packages: time=0.458). No anomalies.
Seed shape: namespaces=1, repositories=1, npm_repositories=1, npm_packages=1, npm_versions=50
Rendered SQL:
UPDATE public.npm_versions
SET last_downloaded_at = NOW()
WHERE ((npm_versions.namespace_id = $1::uuid) AND (npm_versions.id = $2::uuid)) AND (npm_versions.soft_deleted_at IS NULL);Bound args: [<namespace-uuid>, <npm-version-uuid>]
Plan (EXPLAIN (ANALYZE, BUFFERS) output):
Update on npm_versions (cost=0.00..1.75 rows=0 width=0) (actual time=0.073..0.073 rows=0 loops=1)
Update on npm_versions_p55 npm_versions_1
Buffers: shared hit=29
-> Seq Scan on npm_versions_p55 npm_versions_1 (cost=0.00..1.75 rows=1 width=18) (actual time=0.003..0.005 rows=1 loops=1)
Filter: ((soft_deleted_at IS NULL) AND (namespace_id = 'e5566c14-d379-40ff-86bb-2c6d333998da'::uuid) AND (id = 'fedb5265-8c5b-492d-921b-1231604ee27d'::uuid))
Rows Removed by Filter: 49
Buffers: shared hit=1
Planning:
Buffers: shared hit=279
Planning Time: 0.512 ms
Trigger for constraint fk_npm_versions_namespace_id_namespaces on npm_versions_p55: time=0.016 calls=1
Trigger for constraint fk_npm_versions_npm_package_id_npm_packages on npm_versions_p55: time=0.458 calls=1
Execution Time: 0.641 msTimings: planning 0.512ms, execution 0.641ms, total 1.153ms.
Query notes:
- Both bumps are single-row PK-keyed
UPDATEs scoped to one namespace partition (SET last_downloaded_at = NOW()); theSeq Scanis the planner's cost-optimal choice at 50 seeded partition rows and switches topk_npm_packages/pk_npm_versionsat production cardinality — no index gap. - Execution times include a one-time FK-check-trigger plan-cache warm-up on first invocation in the session (
time=0.849package /time=0.458version); steady-state cost is the sub-0.01msSeq Scan → Updateon the pruned partition.
📚 References
- Plan: https://gitlab.com/gitlab-org/ops/artifact-registry/-/blob/main/docs/plans/2026-05-11-npm-hosted.md (Step 26)
- Spec: https://gitlab.com/gitlab-org/ops/artifact-registry/-/blob/main/docs/specs/S11-npm-hosted.md
Related to #238 (closed)