Skip to content

test(mq,app): fit the unit budget; fail fast on an uncreatable store - #647

Merged
EricAndrechek merged 6 commits into
mainfrom
perf/unit-test-budget-main
Sep 25, 2026
Merged

EricAndrechek merged 6 commits into
mainfrom
perf/unit-test-budget-main

Conversation

@EricAndrechek

@EricAndrechek EricAndrechek commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Fixes #617. Part of #613.

internal/app and internal/mq each took 11–12 s of the 15 s unit-test budget on main under a full parallel make test-unit, and timed out when the machine was loaded. After this PR they take under 5 s each. This PR also makes NewEmbedded fail at once on a store directory it cannot create, instead of waiting out the 5 s readiness check.

What changes

Commit Change
2a494bd NewEmbedded runs MkdirAll on the store first and returns nats store: <mkdir error>. Before this, a regular file at <data_dir>/nats failed JetStream in the background, and boot reported only nats server not ready after 5 s. New test: TestNewEmbedded_AStoreItCannotCreateFailsAtOnce.
d46e273, f897394 The directory is created at 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.
98acc11 The mq tests call t.Parallel. A new mq.EmbeddedSyncAlways is true in production and is set to false only in TestMain: on macOS the fsync on every JetStream write was more than half the run. Each store lives in storeDir, which retries its removal (#442).
e441f57 internal/app's new TestMain also turns the fsync off.
668c2c4 Removes the "occupied"-directory hunk that #612's squash added to 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: the t.Parallel lines for embedded_failed_test.go and for TestEmbeddedNATS_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 AQueueThatCannotOpen conflict: 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 $G directories on a goroutine of its own, and that removal overlaps the next open. f5d8f48 has three hunks:

  • AQ-occ: an ignored occupied directory keeps streams/ non-empty during acme's failed open in SetMaxBytes_AQueueThatCannotOpen. This was added to main's squash.
  • P-globex: PacesTheRetriesOfAQueueThatCannotOpen opens globex first, so its streams keep the directory occupied.
  • App-globex: internal/app's TestNew_QueueOpenFailure "nested" subtest blocks globex rather than acme.

The perf change's version of the fix is AQ-wait: require.Eventually until the server has removed $G, and only then open globex.

These runs were on this branch, with -race and GOTOOLCHAIN=go1.26.6, on 2026-09-25 on an otherwise idle machine. "Isolated" means -run '^Name$' -count=20. "pkg" means the whole parallel internal/mq package with -count=5. "Mutation" means JetStreamMaxStore: math.MaxInt64 instead of / 2, which is the refusal the test exists to catch, at -count=5.

P-globex AQ variant AQueue isolated Paces isolated AQueue in pkg Paces in pkg other pkg fails Mutation caught
kept wait only (this PR) 20/20 pass 20/20 5/5 5/5 0 5/5 fail (caught)
kept occupied only 20/20 20/20 5/5 5/5 0 5/5 fail (caught)
kept both 0/20 20/20 0/5 5/5 — —
dropped wait only 20/20 20/20 5/5 4/5 1 5/5 caught
dropped occupied only 20/20 20/20 5/5 5/5 0 5/5 caught
dropped both 0/20 20/20 0/5 3/5 2 —
App-globex TestNew_QueueOpenFailure isolated ×20 in the internal/app package ×3
kept (main) 20/20 3/3
reverted to acme 20/20 3/3

What the runs show:

  • Both AQ fixes together always fail, 20/20. The occupied directory keeps $G from 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.
  • P-globex is what PacesTheRetries needs. 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 from AQueueThatCannotOpen, so the two findings do not conflict. P-globex is on main and this PR keeps it.
  • For 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 with MaxInt64, 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.
  • App-globex made no difference in either direction in 20 isolated runs and 3 package runs. It is left as main has it.

Timings: full parallel make test-unit

Four runs, alternating between main at 2a2b886 and this branch, with -count=1 -race -cover, on an otherwise idle machine for every run.

internal/app internal/mq whole run (DONE … in)
main (2a2b886) 11.30 / 11.37 / 11.41 / 11.45 s 12.05 / 12.18 / 12.30 / 12.38 s 12.07–12.39 s
this PR 4.71 / 4.72 / 4.73 / 4.81 s 3.42 / 3.64 / 3.64 / 3.94 s 5.07–5.27 s

An earlier baseline of main alone, under heavy load, gave internal/app 12.2–13.5 s and internal/mq 12.8–15.1 s. The 15.1 s run was already over the budget.

Verification

  • make ci passed through the shared queue (GOTOOLCHAIN=go1.26.6, the known golangci-lint toolchain workaround), including all coverage gates.
  • Pre-push reviewers were both run on HEAD 668c2c4 (opus, fresh context):

Left for later

🤖 Generated with Claude Code

https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL

EricAndrechek and others added 6 commits September 25, 2026 09:45
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
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: b1aa60d5-1b67-4dd9-99ad-41805ee44375

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added go Pull requests that update go code area/docs Documentation, site/, README area/app Process wiring (internal/app): component build, run, release labels Sep 25, 2026
@github-code-quality

github-code-quality Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Go

Go

The overall line coverage in commit 668c2c4 in the perf/unit-test-budge... branch remains at 94%, unchanged from commit 2a2b886 in the main branch.

Show a line coverage summary of the most impacted files.
File main 2a2b886 perf/unit-test-budge... 668c2c4 +/-
internal/mq/embedded.go 89% 89% 0%
internal/ingest/worker.go 97% 98% +1%

Updated September 25, 2026 22:07 UTC

@EricAndrechek
EricAndrechek marked this pull request as ready for review September 25, 2026 22:04
@EricAndrechek
EricAndrechek requested review from a team and taitelee September 25, 2026 22:04
@EricAndrechek
EricAndrechek merged commit 0cbb386 into main Sep 25, 2026
33 checks passed
@EricAndrechek
EricAndrechek deleted the perf/unit-test-budget-main branch September 25, 2026 22:25
EricAndrechek added a commit that referenced this pull request Sep 26, 2026
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>
EricAndrechek added a commit that referenced this pull request Sep 26, 2026
Brings in #618 (squash-merged parent), #619, #632, #616, #655 and #647.
Conflicts resolved by keeping main's content plus this branch's coord
changes; the AGENTS.md package count is now twenty (keyenc + coord).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
EricAndrechek added a commit that referenced this pull request Sep 26, 2026
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>
EricAndrechek added a commit that referenced this pull request Sep 26, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/app Process wiring (internal/app): component build, run, release area/docs Documentation, site/, README go Pull requests that update go code

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

test(app): internal/app unit tests use 8–17 s of the 15 s budget, time out under load

1 participant