test(e2e): api, ingest and sweeper in separate processes - #645
Closed
EricAndrechek wants to merge 142 commits into
Closed
EricAndrechek wants to merge 142 commits into
EricAndrechek wants to merge 142 commits into
Conversation
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
7 tasks done
|
📚 Docs preview is live → https://d2f7b401-wavehouse-docs.wave-rf.workers.dev
|
… 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
…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
Contributor
Code Coverage OverviewLanguages: Go GoThe overall line coverage in commit 1914eac in the Show a line coverage summary of the most impacted files.
Updated |
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>
This was referenced Sep 26, 2026
chore(config): pin the lease cap's window to mq.EmbeddedDuplicateWindow once #629 and #635 land
#668
Closed
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
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #613.
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 mergesorigin/mainwith the tree unchanged (-s ours), because this branch already held all of #612. The squash had one extra hunk: an "occupied" directory inTestEmbeddedNATS_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)
fix/config-yaml-zerofix/discovery-retry-jitterfix/query-error-classesfix/mq-external-close-and-shrinkfeat/cache-redis-wiringfix/mutation-pipes-uncachedfeat/dedupe-retentionfeat/dedupe-dynamodb-wiringfeat/coord-nats-kv@ 8a9af4bperf/unit-test-budget(local, 7 commits on 5666252)Conflicts resolved in the merge commits
internal/config/config.go: keptdefaults()beside C1's roles code; theCachestruct had moved tobackends.go.internal/api/ingest.go: D1'smq.ErrUnavailablebranch now sits in F2'spublishFailedas an uncertain failure. It commits the records before k, lets k's claim lapse, releases the rest, and answers503withRetry-After: 5.internal/mq/mq.gokeeps bothErrUnavailableandWithIdempotencyKey.api.md,AGENTS.mdandarchitecture.mdsay the same, and describe the duplicate window as 2m embedded or the partition'sduplicate_windowunder nats.Warningsblocks combined,.testcoverage.ymlexcludes unioned, the Makefile integration target runs./internal/cache/...beside natsspike,go mod tidy.MQNATScase inwireMQ, andNATSTopologygets bothDedupeLeaseandCoordBucket. F5 carries an older G1, so its criss-cross doc conflicts were resolved by hand.Integration fixes, and where each belongs
ingest_outage_test.goon #612's broker API (NewEmbedded(dir),SetMaxBytes,DeadLetterCounts(ctx, tenant.Default, ""))defaults()for roles, every*.backendandmq.nats; zerosValidaterefuses are pinned as refusals; the docs test reads durations, lists and named strings; themq.natsTLS doc row is splitcache.redisdefaults indefaultCacheRedis();compress_min_bytes0 means never again (the-1sentinel is gone, a negative value is refused)deployment.md's shared-cache section: #386 is fixed, #394 is what remainsRetry-After: 5, k lapses)ExternalNATS.publishoverwrote theNats-Msg-IdthatWithIdempotencyKeysets, so an uncertain retry was stored twice under nats (measured). New mqtest caseIdempotencyKeyStoresOnce; embedded and ExternalNATS both pass itdefaultDedupe();config.embeddedDuplicateWindowpinned tomq.EmbeddedDuplicateWindowby a test (config stays a leaf); F5's fixtures gain the requireddedupe.retentionduplicate_windowmust be at leastdedupe.lease; warn at boot and on reload when a served tenant's finitededupe.retentionis under the windowdeployment.md: what nats and dynamodb share; retention reaches DynamoDB TTLdeployment.md"One Deployment per role": four shared backendsquery_errors_test.go→ #627; shared-cache tests → #630;dedupe_dynamodb_app_test.go→ #635wire_{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→ #622wireCoord's nats case becomeswireNATSCoordinwire_nats.go, for the e2e gate (59.9781% after d002146), no behaviour changeTestMainremoves the binary C2 builds (it leaked 80–90 MB into$TMPDIRper run, measured)dedupe.dynamodbblock under pebble is ignored silently), boot-config blocks and secrets, retention's 2m being the embedded window, theconfig.yamlroles comment, CHANGELOG linesconfig.yaml→ B2apiandingestrun in separate processes;getting-started.md: only read pipes are cached#617 perf branch (
perf/unit-test-budget, merged in e3d83c7):C2:
tests/integration/roles_test.goThe 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 Eroles=api, B and Croles=ingest, Droles=sweeper. The backends are an operator-provisioned NATS (the shipped Helm values and nack manifests, thewh_coordbucket included), Redis, dynamodb-local, and the suite's ClickHouse. Measured, over two local runs and inmake ci:TestRoles_BootRefusesWhatTheBackendsCannotServedrives boot rules 1–5 through the binary: all 9 cases pass.Flake verdict
TestEmbeddedNATS_PacesTheRetriesOfAQueueThatCannotOpenwas fixed by #612's f5d8f48 (measured). It passes 10/10 isolated,-count=10and-count=30on #612's tip, and 10/10 plus-count=10on 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.wire_{nats,dynamodb,ops}.go, excluded from the e2e per-suite gate only. That follows the precedent ofexternal.goanddynamodb.go. The merged total still counts them. The floor was not lowered.perf/unit-test-budgetmerged, the unit suite passes its 15s-per-package budget. Before it,internal/appandinternal/mqmeasured 18–19s in the parallel run.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.
pre-push-reviewer:scripts/skip-pre-push-review.sh. At 49a3b01, the tree-neutral merge of main, both reviewers were skipped on the record the same way.docs-reviewer:Notes from the code reviewer, not findings:
wavehouse mq manifestshas no--dedupe-leaseflag, so a lease over 2m under nats fails the new rule against the generated 2m window. Boot names the fix, andpublish_timeouthas the same gap.go test -timeoutpanic would orphan C2's child processes. There is no portable parent-death signal to prevent it.Integration suite timeout (CI run 36140286117)
Cause.
tests/integrationhitmake 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=== PAUSEor 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 rango build -cover -coverpkg=./... ./cmd/wavehouseinside 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 -jsonput the package'sm.Runtime 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):
TestMainbuilds theTestRoles_*binary in a goroutine while the containers start, and waits for it beforem.Run. The-timeoutalarm starts inm.Run, so the build is no longer charged to it.app.New, processes or backends callst.Parallel(). Go resumes parallel top-level tests only after every sequential one has finished, so the tests that callt.Setenv(TestDynamoDBDedupe_TwoInstancesShareSeenIDs, the OTel tests) stay sequential and never overlap them.startClickHousealready uses). With several containers starting at once, "Server is ready" returned before the host port accepted connections, and a localmake cifailed withnats: no servers available for connection.internal/cache(TestRedis_ServerStopsAnswering,TestRedis_CloseDeliversPastAnOpenBreaker) write their setup fill through a default-timeout client. In CI run 36159801619, the second test's setupLookuphitcontext deadline exceededon the client tuned to a 100 ms timeout. That client now serves only the paused phase, and no assertion changed. The failure ran alongsidetests/integration'sTestMainbuild, 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
TestMainwith its own containers. I did not raise the timeout either.Timings (
m.Run,-race -coverpkg=./..., local): 203 s before, 102 s after.make ci'stests/integrationpackage time, setup included, was 108–111 s over three runs. On CI, before the fixm.Runpanicked 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:
TestMainandt.ParallelonTestRoles_SeparateProcesses(TestRoles_BootRefuses…was already parallel).t.ParallelonTestNATSBackend_EndToEnd, and the port wait instartNATS.t.Parallelon bothTestCoordNATS_*tests.t.Parallelon bothTestSharedCache_*tests, and the port wait instartRedis.seedFillchange ininternal/cache/redis_integration_test.go.TestDynamoDBDedupe_TwoInstancesShareSeenIDscallst.Setenvand must stay sequential.t.ParallelonTestBootResilience_*,TestIngest_ClickHouseOutage_*,TestQueryErrors_ClickHouseDownandTestNestedDirectory_PerTenantPoolsAndDiscovery. These already used their own container or app.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
🤖 Generated with Claude Code
https://claude.ai/code/session_01EJr5tY4WQUy2sc4MbW67vL