test(mq,app): fit the unit budget; fail fast on an uncreatable store - #647
Conversation
A store directory the embedded server cannot create (a regular file in its place) failed JetStream in the background, and NewEmbedded only gave up after ReadyForConnections' full 5s wait, reporting "nats server not ready" instead of the cause. Create the directory first and return its error. TestNew_LateBootFailureReleasesEverything in internal/app used exactly this failure and spent 5s of its package's 15s budget on it. Part of #617. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
internal/mq's unit binary took 13s alone and 18s under a parallel `make test-unit`, against the 15s per-package budget (#617). Two costs dominated: - An fsync per JetStream write (SyncAlways), which on macOS is a full flush and was over half the run. It is now EmbeddedSyncAlways, true in production and turned off by TestMain: nothing here asserts anything across a crash. - The embedded tests ran one after another, each on its own in-process server in its own directory. They now call t.Parallel; the ones that capture the default logger stay serial, so no captured log gains another test's lines. Running in parallel exposed two races that load alone had hidden: - A store's TempDir removal racing a consumer's state file written after Close (#442): the store now lives in storeDir, the retrying removal internal/testutil and mqtest already use. - TestEmbeddedNATS_SetMaxBytes_AQueueThatCannotOpen opened globex while the server was still removing the directories acme's failed open had emptied, on a goroutine of its own. The test now waits for that removal. It cannot keep the directory occupied the way the pacing test does: a queue open first would keep the store reservation count above zero, and the test would no longer catch a store limit at the top of the int64 range (checked by mutation). Alone: 13.0s -> ~3.0s. Part of #617. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Every boot here opens its tenants' queues on the embedded broker, each open a handful of fsynced JetStream writes. TestMain turns mq.EmbeddedSyncAlways off, as internal/mq's own tests do: nothing here asserts anything across a crash. About 1.6s of the package's run. Part of #617. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Review fixes for the fail-fast store commit: nats-server creates its store directory at 0700, so NewEmbedded's MkdirAll now does too rather than widening it to group-readable, and the operator-visible change gets its CHANGELOG entry. Part of #617. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
MkdirAll succeeds on an existing directory whatever its mode, so an unwritable <data_dir>/nats still waits the 5s and reports "nats server not ready". Say so instead of claiming it. Part of #617. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
#612 kept the streams directory occupied through acme's failed open so the server would not remove it under globex's open. The parallel-tests change fixes the same race by waiting for the server to remove $G, which never happens while the occupier is there: with both, the test times out every run. Keep the wait. Part of #617. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code Coverage OverviewLanguages: Go GoThe overall line coverage in commit 668c2c4 in the Show a line coverage summary of the most impacted files.
Updated |
Brings in #647, #632, #616 and #655. One conflict, in config.go: main dropped the Cache struct's env-default tag while this branch moved Cache into backends.go; kept the move. #632 removed every env-default tag. The four *.backend fields and cache.l1_max_cost this branch declares in backends.go still carried theirs, so an explicit `backend: ""` would have become the default. Their defaults now live in defaults(); an empty backend, which Validate refuses, is pinned next to server.port's zero as a refusal; the docs test converts a documented default to a named string type such as MQBackend. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in main via feat/coord-leases: #618's squash, #619, #632, #616, #655 and #647. Per #632, the roles default moves from its env-default tag into defaults(): an explicit `roles: []` now reaches Validate (a refusedZeros entry pins it), and the doc-defaults test parses the roles cell as a comma-separated list. instance_id's documented default is *(empty)*, the value in defaults(); Load resolves it to <hostname>-<8 hex>. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
main now carries #612 and #618 as squashes, plus #632, #622, #627, #615, #619, #647, #616, #655 and #623. The merge was resolved against the pre-squash #618 head (f129d57) as its base, so main's version wins for everything this stack does not own and only the cache stack's changes (#614, #621, #626 as merged here, and this PR) are re-applied on top. Warnings keeps main's api-role gate for the cache.redis warnings too: a split's Deployments differ only in roles, so the API's cover the others'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017aS7rLrH1RKkUMem7X4ckd
Fixes #617. Part of #613.
internal/appandinternal/mqeach took 11–12 s of the 15 s unit-test budget on main under a full parallelmake test-unit, and timed out when the machine was loaded. After this PR they take under 5 s each. This PR also makesNewEmbeddedfail at once on a store directory it cannot create, instead of waiting out the 5 s readiness check.What changes
NewEmbeddedrunsMkdirAllon the store first and returnsnats store: <mkdir error>. Before this, a regular file at<data_dir>/natsfailed JetStream in the background, and boot reported onlynats server not readyafter 5 s. New test:TestNewEmbedded_AStoreItCannotCreateFailsAtOnce.0700, the same mode nats-server uses (defaultDirPerms). CHANGELOG entry under Fixed, limited to what mkdir catches: an existing directory that cannot be written to still fails the old way.mqtests callt.Parallel. A newmq.EmbeddedSyncAlwaysistruein production and is set to false only inTestMain: on macOS the fsync on every JetStream write was more than half the run. Each store lives instoreDir, which retries its removal (#442).internal/app's newTestMainalso turns the fsync off.TestEmbeddedNATS_SetMaxBytes_AQueueThatCannotOpen(see below).These are 2a19d7e, 96e2112, bd059f2, 3d959b9 and 3c8a93b from
perf/unit-test-budget, cherry-picked onto main. Two hunks from the perf branch are left out because they belong to stacks that are not on main: thet.Parallellines forembedded_failed_test.goand forTestEmbeddedNATS_Publish_IdempotencyKeyDropsARepeat. Neither test exists on main; they come from the integration tree (#645). The CHANGELOG entry was re-applied under main's### Fixed.The
AQueueThatCannotOpenconflict: measured#612 (f5d8f48) and the perf change fix the same race in different ways. The race: after a failed open, the server removes the empty
streams/and$Gdirectories on a goroutine of its own, and that removal overlaps the next open. f5d8f48 has three hunks:occupieddirectory keepsstreams/non-empty during acme's failed open inSetMaxBytes_AQueueThatCannotOpen. This was added to main's squash.PacesTheRetriesOfAQueueThatCannotOpenopens globex first, so its streams keep the directory occupied.internal/app'sTestNew_QueueOpenFailure"nested" subtest blocks globex rather than acme.The perf change's version of the fix is AQ-wait:
require.Eventuallyuntil the server has removed$G, and only then open globex.These runs were on this branch, with
-raceandGOTOOLCHAIN=go1.26.6, on 2026-09-25 on an otherwise idle machine. "Isolated" means-run '^Name$' -count=20. "pkg" means the whole parallelinternal/mqpackage with-count=5. "Mutation" meansJetStreamMaxStore: math.MaxInt64instead of/ 2, which is the refusal the test exists to catch, at-count=5.TestNew_QueueOpenFailureisolated ×20internal/apppackage ×3What the runs show:
occupieddirectory keeps$Gfrom ever being removed, so the wait for its removal times out. The finding from test(e2e): api, ingest and sweeper in separate processes #645 reproduces.PacesTheRetriesneeds. Without it, the pacing test raced in the full parallel package (1/5 and 2/5 failures), even though it passed 20/20 when run alone. This is the finding from feat(mq): coord leases on a NATS KV bucket #646. It is a different test fromAQueueThatCannotOpen, so the two findings do not conflict. P-globex is on main and this PR keeps it.AQueueThatCannotOpen, AQ-wait alone and AQ-occ alone both passed every run, and both caught the mutation in this matrix. The earlier finding, that the occupied variant still passes withMaxInt64, did not reproduce here. On main as merged (occupied only), the mutation also fails the test 5/5 (measured). I kept AQ-wait because it is the version the perf change was written and measured against. It is also what the test's comment describes: no queue is kept open, and no directory is kept around to hold the reservation count up. Dropping AQ-occ instead of AQ-wait would work equally well by these numbers.Timings: full parallel
make test-unitFour runs, alternating between main at 2a2b886 and this branch, with
-count=1 -race -cover, on an otherwise idle machine for every run.internal/appinternal/mqDONE … in)An earlier baseline of main alone, under heavy load, gave
internal/app12.2–13.5 s andinternal/mq12.8–15.1 s. The 15.1 s run was already over the budget.Verification
make cipassed through the shared queue (GOTOOLCHAIN=go1.26.6, the known golangci-lint toolchain workaround), including all coverage gates.pre-push-reviewer: ship_it, with 0 MUST, 0 SHOULD and 0 MAY findings.docs-reviewer: ship_it, with 0 findings. The CHANGELOG entry was checked against nats-server'sdefaultDirPermsandwireMQ's error path. No docs page needed a change.Left for later
internal/testutil.NewEmbeddedMQ, used by theinternal/ingestandinternal/apitests, still fsyncs on every write. Those packages are not near the budget today. The reviewer noted it as the next place to get the same speed-up.🤖 Generated with Claude Code
https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL