Skip to content

test(e2e): api, ingest and sweeper in separate processes - #645

Closed
EricAndrechek wants to merge 142 commits into
mainfrom
integration/distributed
Closed

EricAndrechek wants to merge 142 commits into
mainfrom
integration/distributed

Conversation

@EricAndrechek

@EricAndrechek EricAndrechek commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Part of #613.

Integration proof. Do not merge this PR directly. It proves that every #613 stack works together on top of #612 (mq-tenant-streams), and it adds C2, the multi-process integration test. The stacks merge individually after #583. Each fix made here is listed below with the PR it belongs to, so it can be backported there.

Base: main. This branch was built on #612 (mq-tenant-streams @ f5d8f48). #612 was squash-merged into main as 2a2b886 while the work was under way, and its branch was deleted, so the PR targets main. 49a3b01 merges origin/main with the tree unchanged (-s ours), because this branch already held all of #612. The squash had one extra hunk: an "occupied" directory in TestEmbeddedNATS_SetMaxBytes_AQueueThatCannotOpen. It is deliberately left out. #617's perf branch fixes the same race another way, by waiting for the failed open's cleanup, and the two together fail: with the hunk the test failed (measured), without it 5/5 runs passed. Whoever lands #617 on main must drop or reconcile that hunk.

What is merged (merge commits only)

Stack tip PR Carries
fix/config-yaml-zero #632 #631 fix (on main)
fix/discovery-retry-jitter #616 C3 (on main)
fix/query-error-classes #627 A #619, A2
fix/mq-external-close-and-shrink #644 D1 #623, D2 #624, D3 #636, D4 #639, plus C1 #622, B1 #615, G1 #618
feat/cache-redis-wiring #630 E1 #614, E2 #621, E3 #626, E4, G1
fix/mutation-pipes-uncached #634 on E1
feat/dedupe-retention #633 F1 #625, F2 #629, F4
feat/dedupe-dynamodb-wiring #635 F3 #628, F5, G1
feat/coord-nats-kv @ 8a9af4b B2 NATS KV leases
perf/unit-test-budget (local, 7 commits on 5666252) #617 the unit-test budget fix

Conflicts resolved in the merge commits

  • fix(config): keep an explicit false/0/"" from config.yaml #632 × G1/C1/D4, internal/config/config.go: kept defaults() beside C1's roles code; the Cache struct had moved to backends.go.
  • D1 × F2, internal/api/ingest.go: D1's mq.ErrUnavailable branch now sits in F2's publishFailed as an uncertain failure. It commits the records before k, lets k's claim lapse, releases the rest, and answers 503 with Retry-After: 5. internal/mq/mq.go keeps both ErrUnavailable and WithIdempotencyKey. api.md, AGENTS.md and architecture.md say the same, and describe the duplicate window as 2m embedded or the partition's duplicate_window under nats.
  • E × D: Warnings blocks combined, .testcoverage.yml excludes unioned, the Makefile integration target runs ./internal/cache/... beside natsspike, go mod tidy.
  • F5 × D and B2: kept the MQNATS case in wireMQ, and NATSTopology gets both DedupeLease and CoordBucket. F5 carries an older G1, so its criss-cross doc conflicts were resolved by hand.
  • Docs and CHANGELOG: merged so that each side's edits are applied. For the one-line paragraphs, I checked that each stack's edits against its base equal the result's edits against ours.

Integration fixes, and where each belongs

Commit Fix Backport to
ddf0101 ingest_outage_test.go on #612's broker API (NewEmbedded(dir), SetMaxBytes, DeadLetterCounts(ctx, tenant.Default, "")) #619 (A)
a08a6d6 #632's defaults() for roles, every *.backend and mq.nats; zeros Validate refuses are pinned as refusals; the docs test reads durations, lists and named strings; the mq.nats TLS doc row is split #618 (G1), #622 (C1), #639 (D4)
eaf69ef cache.redis defaults in defaultCacheRedis(); compress_min_bytes 0 means never again (the -1 sentinel is gone, a negative value is refused) #630 (E4)
00e165c deployment.md's shared-cache section: #386 is fixed, #394 is what remains whichever of #630 and #634 merges second
d2ce776 the window test pins ErrUnavailable as uncertain (503, Retry-After: 5, k lapses) whichever of #629 (F2) and #623 (D1) merges second
6a69c03 bug: ExternalNATS.publish overwrote the Nats-Msg-Id that WithIdempotencyKey sets, so an uncertain retry was stored twice under nats (measured). New mqtest case IdempotencyKeyStoresOnce; embedded and ExternalNATS both pass it the case to #623 (D1), the fix to #636 (D3)
cc53a6a dedupe defaults in defaultDedupe(); config.embeddedDuplicateWindow pinned to mq.EmbeddedDuplicateWindow by a test (config stays a leaf); F5's fixtures gain the required dedupe.retention #635 (F5)
09f2d3d verifier: every partition's duplicate_window must be at least dedupe.lease; warn at boot and on reload when a served tenant's finite dedupe.retention is under the window #639 (D4), once #633 and #635 are below it
c696b2c deployment.md: what nats and dynamodb share; retention reaches DynamoDB TTL the F5/D4 pair and the F4/F5 pair, whichever of each merges second
5666252 CHANGELOG Unreleased as one list integration only
713b782 deployment.md "One Deployment per role": four shared backends whichever of #630, #635 and B2 merges last
9c1d252 C2, below C2's own PR
e830f34 four integration tests name roles and backends in hand-built configs (C1/G1 require it) query_errors_test.go → #627; shared-cache tests → #630; dedupe_dynamodb_app_test.go → #635
d002146 pure move of the shared-backend wiring into wire_{nats,dynamodb,ops}.go, excluded from the e2e gate only (e2e measured 59.1% before it) wire_nats.go → #639 and B2; wire_dynamodb.go → #635; wire_ops.go → #622
f10b9de wireCoord's nats case becomes wireNATSCoord in wire_nats.go, for the e2e gate (59.9781% after d002146), no behaviour change B2
468b4fe C2 review fix: TestMain removes the binary C2 builds (it leaked 80–90 MB into $TMPDIR per run, measured) C2's own PR, with 9c1d252
8c6057e docs review, round 1: the MQ section's "embedded only", ignored sub-blocks (a dedupe.dynamodb block under pebble is ignored silently), boot-config blocks and secrets, retention's 2m being the embedded window, the config.yaml roles comment, CHANGELOG lines configuration.mdx embedded line → #639; ignored-block sentence and warnings list → B2; boot-config list → whichever of #630/#635/B2 merges last; retention lines → #633; config.yaml → B2
d6b4d4b docs review, round 2: the retention refusal names 2m; a split needs a shared cache only when api and ingest run in separate processes; getting-started.md: only read pipes are cached settings-directory.mdx → #633; deployment.md → B2; getting-started.md → #634

#617 perf branch (perf/unit-test-budget, merged in e3d83c7):

Commits Backport to
2a19d7e, 3d959b9, 3c8a93b the standalone fail-fast store fix (#612, or a PR against main)
96e2112, bd059f2 #612
8776b4d #635
09d5c14 #639

C2: tests/integration/roles_test.go

The test builds the real binary (with coverage when the suite collects it) and runs it as separate OS processes, configured by WH_* only: A and E roles=api, B and C roles=ingest, D roles=sweeper. The backends are an operator-provisioned NATS (the shipped Helm values and nack manifests, the wh_coord bucket included), Redis, dynamodb-local, and the suite's ClickHouse. Measured, over two local runs and in make ci:

  • 50 rows ingested through A land in ClickHouse exactly once and stay that way.
  • A's shared-cache fill is invalidated by B's and C's inserts: a MISS carrying the new rows, inside the 10s TTL floor.
  • SSE clients on A and on E both see every event.
  • One id sent to A and E at once is accepted exactly once, and later repeats are duplicates.
  • After D is killed with SIGKILL, the sweeper lease moves to a restarted D 16.2s later (15s lease duration).

TestRoles_BootRefusesWhatTheBackendsCannotServe drives boot rules 1–5 through the binary: all 9 cases pass.

Flake verdict

TestEmbeddedNATS_PacesTheRetriesOfAQueueThatCannotOpen was fixed by #612's f5d8f48 (measured). It passes 10/10 isolated, -count=10 and -count=30 on #612's tip, and 10/10 plus -count=10 on the merged tree. No patch is needed.

Evidence

make ci (queued, GOTOOLCHAIN=go1.26.6) is green on d6b4d4b, whose tree is identical to the HEAD, 49a3b01; it was also green on f10b9de and 8c6057e.

Suite Result Coverage Floor
unit 3017 tests, 1 skipped 91.9% 80
integration 284 tests, including C2 62.0% 20
e2e (Go) SDK suite 67 passed, 3 skipped 60.0690% (3830/6376) 60
Go total 94.5% 80
ts-total 237 unit + 81 e2e tests 82.28% 50
  • The e2e margin is thin, and it moves with timing: 60.0376% on 8c6057e, 60.0690% on f10b9de and d6b4d4b. Before d002146 and f10b9de it was 59.1% and then 59.9781% (both measured). Those two commits move the shared-backend wiring, which e2e never boots, into wire_{nats,dynamodb,ops}.go, excluded from the e2e per-suite gate only. That follows the precedent of external.go and dynamodb.go. The merged total still counts them. The floor was not lowered.
  • test(app): internal/app unit tests use 8–17 s of the 15 s budget, time out under load #617: with perf/unit-test-budget merged, the unit suite passes its 15s-per-package budget. Before it, internal/app and internal/mq measured 18–19s in the parallel run.
  • Integration flakes: B2's reported base flake, TestExternalNATS_Recheck, did not fail in any of the runs above.

Reviewers

Both reviewers ran on opus, in fresh context, scoped to the integration-only commits plus the D1×F2 resolution in 605a6c6, not the stacks' own deltas.

Notes from the code reviewer, not findings:

  • wavehouse mq manifests has no --dedupe-lease flag, so a lease over 2m under nats fails the new rule against the generated 2m window. Boot names the fix, and publish_timeout has the same gap.
  • A go test -timeout panic would orphan C2's child processes. There is no portable parent-death signal to prevent it.

Integration suite timeout (CI run 36140286117)

Cause. tests/integration hit make test-integration's -timeout 240s (panic: test timed out after 4m0s, package 254 s). All 17 reported failures were fallout from that panic: they were parallel tests paused at === PAUSE or subtests that had just started, and gotestsum reported each one as (unknown). The combined stacks run their tests in series: C2 took 45 s, the NATS end-to-end test 13 s, the shared-cache tests 18 s, the coord tests 7 s and the DynamoDB app test 6 s. They sat alongside the existing own-container outage tests (29 s and 16 s). On top of that, C2 ran go build -cover -coverpkg=./... ./cmd/wavehouse inside the timed window. A cold build costs about 120 CPU-seconds, which is most of a minute on a 4-vCPU runner whose build cache holds no non-race cover objects. Locally, go test -json put the package's m.Run time at 203 s against the 240 s budget.

Fix (the first preference: make the tests parallel and keep the budget; no assertion changed, and the coverage gates are untouched):

  • TestMain builds the TestRoles_* binary in a goroutine while the containers start, and waits for it before m.Run. The -timeout alarm starts in m.Run, so the build is no longer charged to it.
  • Every test that brings up its own ClickHouse, app.New, processes or backends calls t.Parallel(). Go resumes parallel top-level tests only after every sequential one has finished, so the tests that call t.Setenv (TestDynamoDBDedupe_TwoInstancesShareSeenIDs, the OTel tests) stay sequential and never overlap them.
  • The NATS and Redis containers now wait for the listening port as well as the log line (the pattern startClickHouse already uses). With several containers starting at once, "Server is ready" returned before the host port accepted connections, and a local make ci failed with nats: no servers available for connection.
  • The two Redis breaker tests in internal/cache (TestRedis_ServerStopsAnswering, TestRedis_CloseDeliversPastAnOpenBreaker) write their setup fill through a default-timeout client. In CI run 36159801619, the second test's setup Lookup hit context deadline exceeded on the client tuned to a 100 ms timeout. That client now serves only the paused phase, and no assertion changed. The failure ran alongside tests/integration's TestMain build, which may have been the load (inferred).

I did not split the suite into another package or CI job: once the tests run in parallel, the package fits one invocation with margin, and a split would need a second TestMain with its own containers. I did not raise the timeout either.

Timings (m.Run, -race -coverpkg=./..., local): 203 s before, 102 s after. make ci's tests/integration package time, setup included, was 108–111 s over three runs. On CI, before the fix m.Run panicked at 4m0s and the package ran 4m14s, in a 5m09s job (run 36140286117). After it, the package took 3m13s in run 36159801619 and 2m51s in the all-green run 36164495404, both including setup and the build. The Integration tests job took 4m06s.

Which stack each change belongs to, for backporting one stack at a time:

Also found: a separate flake that exists on main, TestDedupeDynamo_Conformance/a_failed_reserve_leaves_nothing_claimed. It failed once in three local runs, and #648 tracks it. The likely cause is inferred, not verified: a put cancelled mid-flight can land after the rollback's conditional delete. This PR does not change it.

Left to later PRs

  • Backporting each fix above to its own stack.
  • D5 (partition claiming), C4 (membership), C5 (partitioned ingest), E5 (near-cache): as the design says.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL

taitelee and others added 30 commits September 24, 2026 17:49
mq.backend, cache.backend, dedupe.backend and coord.backend select each
layer's implementation; only today's in-process one exists per layer and
it is the default. Validate refuses an unknown value, internal/app picks
the implementation in one switch per layer, data_dir is probed only when
a selected backend keeps state there, and boot logs Config.Warnings.

Part of #613.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
RetryRefresh slept exactly 2s * 2^n capped at 60s, so instances retrying
against one recovering ClickHouse fired in lockstep. Each sleep is now
drawn uniformly below the backoff (full jitter), via an injectable
retryDelay so the tests pin the backoff sequence without wall-clock sleeps.

Closes #141. Part of #613.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Cache.Get/Set becomes Lookup(ctx, tenant, sha, deps) -> (Entry,
Snapshot, error) and Set(ctx, Snapshot, value, ttl): a fill is filed
under the versions read before its query ran, so a bump landing
mid-query orphans it instead of re-homing pre-write rows (#382).

Every query key folds the tenant version, so InvalidateTenant now
orphans pipe results too. A Lookup naming another tenant's namespace is
ErrForeignDependency. Set errors only on backend failure.

Adds internal/testutil/cachetest, the backend-agnostic conformance
suite LocalCache runs and the Redis backend will.

Fixes #382. Part of #613.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
New internal/coord: Coordinator/TryAcquire/Term with a fencing Token,
Done/Err and Resign; RunElected for leader loops; Local, the in-process
implementation; and coordtest.Conformance, the suite every backend runs.
The sweeper now runs through RunElected under the "sweeper" lease, over a
Local coordinator that wireCoord opens until coord.backend lands.

Part of #613.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
A handoff overlap cannot lose ClickHouse data (every sweep stops at the
ack floor) but can trim SSE replay history when the holders' settings
views differ. Also lists coord/ in development.md's package tree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
…ENTS.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
mqtest.Run states the mq.Broker contract as behavior, through the
interfaces alone, so the external-NATS backend (#613) runs the same cases
as the embedded one; mqtest.Caps covers the places where their semantics
legitimately differ. The embedded broker passes it. The suite found that a
durable deleted on several tenants' queues could report on failed more
than once; fixed.

The interface comments now allow a partition as the delivery unit, a
CreateConsumer that finds rather than creates, an operator-owned
retention, and zero dead-letter counts without a per-tenant queue.
mq.ErrUnavailable is new, and the ingest handler answers it with 503 and
Retry-After: 5.

Part of #613.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
A failed batch insert went through row-by-row isolation whatever the
failure, so a down, overloaded or read-only ClickHouse parked every row
on the DLQ. chconn.Classify now classes the failure first: only a row
ClickHouse rejects is isolated and dead-lettered; an unavailable, denied
or unjudged failure is handed back with a delayed nak under a per-pool
backoff, including when ClickHouse goes away mid-isolation.

Part of #613.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
internal/mq's unit tests already take ~10s of their 15s budget under
load, and the suite pushed them over. The embedded run moves to
mqtest/embedded_test.go and ends delivery by closing the broker, so it
needs no hook into mq's internals; the exactly-once failed report gets a
deterministic test in internal/mq. The api.md rows for ErrUnavailable say
that no backend returns it yet.

Part of #613.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Replace CheckAndMark with a two-phase Reserve -> Commit | Release contract
with a lease on the pending claim, keyed by (tenant, table, id) under a
versioned layout, and add dedupetest, the conformance suite every backend
runs.

- Pebble claims under a sharded in-memory lock, so concurrent requests with
  one id publish it once (#390).
- Ingest reserves after encoding, publishes, then commits, releasing the id
  when the publish fails, so a retried 503 is published, not dropped (#384's
  loss; F2 closes the uncertain-publish window). An id held by another
  request answers 503 with the lease as Retry-After.
- The same id in two tables is two ids (#222); an explicit null id is a
  missing id (#370).

BREAKING: the key layout changes, so ids seen before the upgrade are
accepted once more.

Part of #613.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
The JetStream layout an external NATS must provide for mq.backend: nats
(epic #613, D2): N interest-retention ingest partitions each with the
wh-ingest durable, a limits history stream sourcing them, and one DLQ.
A verifier checks a live server against the spec and returns every
finding at once; await retries it while the operator's CRs roll out.
`wavehouse mq manifests` renders the same spec as nack CRs, and
deployments/nats ships its N=4 output (golden-tested) plus Helm values
whose wavehouse user permissions are test-pinned and used verbatim by
the fixture the verifier tests run against.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Review round 1. A read-only table (or one with too many parts or
mutations) tripped the whole pool's breaker, and a healthy neighbour's
success reopened it on every flush — the backoff never escalated and
the logs flapped. chconn.TableScoped now routes those codes to a
per-(pool, table) backoff. While a probe is out, arriving rows are
handed back with a floored delay instead of cycling through the worker.
Docs: the query handlers do not use Classify yet; list NakWithDelay in
the mq surface; complete the Denied list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deletes the durable on one tenant's queue, drains the report, then on the
next: the real path, rather than calling fail by hand. The replay polls in
mqtest pause between attempts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
…lease

Also document the upgrade, the SDK's new 503 cause, and the release on a
failed publish.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
@github-actions github-actions Bot added go Pull requests that update go code area/observability Metrics, logs, traces, health, profiling area/api HTTP handlers, routing, middleware area/ingest Ingest pipeline (Bento, batching, DLQ) area/query Structured query AST, SQL builder area/cache Local / shared / tiered caching area/dedupe Deduplication (Pebble, ScyllaDB) area/sdk TypeScript SDK (clients/ts/) area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release area/app Process wiring (internal/app): component build, run, release labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

📚 Docs preview is live → https://d2f7b401-wavehouse-docs.wave-rf.workers.dev

  • Commit — 1914eac: docs: neutral tags in the DynamoDB example; neutral benchmark wording
  • Author — @EricAndrechek, Claude Opus 5.5 (1M context)
  • Committed — 2026-09-25 13:07 (UTC-04:00)
  • Deployed — 2026-09-25 13:18 EDT

… first

The combined #613 stacks pushed tests/integration past make
test-integration's 240s -timeout on a GitHub runner (run 36140286117:
panic at 4m0s, 17 "failures" that were all tests still paused).

- TestMain starts the C2 binary build alongside the containers and waits
  for it before m.Run, so a cold cover build (~120 CPU-s) no longer
  counts against -timeout, which starts at m.Run.
- Tests that bring up their own ClickHouse, processes or backends call
  t.Parallel: C2, the NATS end-to-end and coord tests, the shared-cache
  tests, and the two own-container outage tests. Go runs them only after
  every sequential test, so shared-state tests never overlap them.

Local m.Run time 203s -> 109s; no assertion changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
EricAndrechek and others added 6 commits September 25, 2026 11:44
…nary

Review follow-up: TestQueryErrors_ClickHouseDown (own ClickHouse and
app) and TestNestedDirectory_PerTenantPoolsAndDiscovery (own app, own
databases) fit the rule the package comment states, so they are
parallel too. The docs name the TestRoles_* tests as the binary's users.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
…he log

With the backend tests parallel, make ci failed TestRoles_SeparateProcesses
and TestCoordNATS_OneSweeperAcrossReplicas with "nats: no servers
available for connection": the "Server is ready" wait returned before
Docker forwarded the host port while several containers started at
once. Wait for the listening port as well, as startClickHouse does, and
do the same for Redis.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
CI run 36159801619 failed TestRedis_CloseDeliversPastAnOpenBreaker at
its first Lookup ("context deadline exceeded"): the setup fill ran on the
client tuned to a 100ms timeout, a threshold of 1 and a one-hour breaker,
so one slow round trip on a busy runner made the test unpassable. Both
breaker tests now seed through a default-timeout client under the same
prefix; the tuned client serves only the paused phase, and every
assertion is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
@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 1914eac in the integration/distribu... branch is 93%. The line coverage in commit 2a2b886 in the main branch is 94%.

Show a line coverage summary of the most impacted files.
File main 2a2b886 integration/distribu... 1914eac +/-
internal/mq/nat...est/natstest.go 0% 74% +74%
internal/mq/external.go 0% 84% +84%
internal/cache/redis.go 0% 90% +90%
internal/mq/lease.go 0% 90% +90%
internal/coord/...st/coordtest.go 0% 93% +93%
internal/mq/nats_topology.go 0% 94% +94%
internal/dedupe/dynamodb.go 0% 95% +95%
internal/mq/mqtest/cases.go 0% 96% +96%
internal/mq/nats_manifests.go 0% 96% +96%
internal/config/backends.go 0% 100% +100%

Updated September 25, 2026 17:18 UTC

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL
EricAndrechek added a commit that referenced this pull request Sep 25, 2026
…647)

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 #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 #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):
- `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's `defaultDirPerms` and `wireMQ`'s error
path. No docs page needed a change.
- Because of #454, the hook wrote the markers against the main
checkout's HEAD, not this branch's HEAD. No marker was written by hand.

## Left for later

- `internal/testutil.NewEmbeddedMQ`, used by the `internal/ingest` and
`internal/api` tests, 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.
- 8776b4d (#635) and 09d5c14 (#639) from the perf branch belong to
their own stacks and are not included here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
EricAndrechek added a commit that referenced this pull request Sep 29, 2026
Same reason as wire_nats.go, and the same move #645 made: the e2e
binary runs every role, so wireOpsAuth and wireOpsHTTP were statements
the e2e gate counts but can never reach. Pure move, excluded from the
e2e gate only; the unit and merged totals still count them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyrXjhR7iDg33paioLQHFq
@EricAndrechek

Copy link
Copy Markdown
Member Author

Superseded: every stack it integrated has now merged to main, the NATS stack last in #624 (6fa9723). Its C2 multi-process test landed with #622/#624. Closing without merging, as planned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api HTTP handlers, routing, middleware area/app Process wiring (internal/app): component build, run, release area/cache Local / shared / tiered caching area/dedupe Deduplication (Pebble, ScyllaDB) area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release area/ingest Ingest pipeline (Bento, batching, DLQ) area/observability Metrics, logs, traces, health, profiling area/query Structured query AST, SQL builder area/sdk TypeScript SDK (clients/ts/) dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation go Pull requests that update go code

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants