Repository navigation
Demo-readiness docs, plus the before twin's consumer shutdown fix that CI surfaced - #14
Merged
Merged
Conversation
CLAUDE.md: mode paragraph (no new slices, no bumps, what sessions are for now), slice slate marked built, schedule fallbacks marked history, file-inventory rule states the final counts, critter-stack-audit.md listed, display-name open item annotated as the only one left. README: fourth features-table row for the RabbitMQ extension under a heading a skimmer sees, slice 004 row and current whole-twin counts in the inventory table, repo tour lists tests/LayerCake.Slices.Tests and docs/critter-stack-audit.md. Build log: this pass, plus the two manual checks since slice 004 (CI green on main; demo page clicked through against the broker path on both twins).
…ounded CI on the docs PR failed with an ObjectDisposedException in a test class cleanup while every test passed. Root cause: under Alba, WebApplicationFactory stops a host whose own Run() is already stopping, so NotifyBakerConsumer.StopAsync ran twice concurrently and the second call disposed a channel the first was already disposing. A probe that disposed the host mid-drain also showed a graceful shutdown dropping an in-flight baker task (cancelled through the stopping token, nacked without requeue) and acking on a closed connection. NotifyBakerConsumer: StopAsync runs one shared shutdown task; shutdown cancels the consumer, waits for the delivery in flight, closes the channel, all under a 5 s cap that falls back to abort with a warning; the handler no longer takes the stopping token. RabbitMqConnection: close (bounded) before dispose, since disposing an open connection stalled 25 s on the client's abort path. Suite green six times in a row on the final code. Counts re-run: before twin 2,142 raw / 1,776 non-blank; inventory, README, CLAUDE.md, build log updated.
CI rejected the first shutdown fix with a TaskCanceledException from RabbitMQ.Client's Connection.CloseAsync after a 30 s stall. In client 7.2.2 a graceful connection close floors its timeout at 30 s and throws on expiry, and the graceful channel close added in the first round left the connection's main loop unfinished under load, which is the documented cause of that stall. Consumer: still idempotent StopAsync, no stopping token in the handler, cancel the consumer and await the in-flight delivery under a 5 s cap; then abort the channel through DisposeAsync instead of closing it. Connection: AbortAsync(5 s), bounded and non-throwing, before DisposeAsync. Eight stressed runs (two suites at once) with zero failures at about 15 s each, against 43 s and 73 s with failures for the rejected version. Counts re-run: before twin 2,140 raw / 1,775 non-blank; inventory, README, CLAUDE.md, build log updated.
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.
Three commits. The first is the doc-only pass from the 2026-09-05 handoff. The second and third fix the CI flake this PR's own first run surfaced; the third supersedes the second's teardown shape after CI rejected it. Erik reads before merge; judgment calls flagged.
Commits 2 and 3: before twin consumer shutdown (the CI flake)
CI on commit 1 failed with
[Test Class Cleanup Failure (BeforeTwinCoupons)] System.ObjectDisposedExceptionwhile all 54 + 15 tests passed. Never seen onmain, six local runs clean. A throwaway probe (boot the before twin against the compose broker with a pre-loaded queue, dispose it mid-drain) reproduced the exact exception plus two more faults in the same path:StopAsyncruns twice, concurrently, on the same hosted service under Alba.WebApplicationFactory.DisposeAsyncstops a host whose ownRun()is already stopping (both call stacks captured). The oldStopAsyncdisposed the channel on each call; the second call hit RabbitMQ.Client's disposed-object check. That is the CI exception.Commit 2 fixed those but added graceful channel and connection closes. CI rejected it with
TaskCanceledExceptionafter a 30 s stall, reproduced locally by running two contract suites at once. The stack pointed at RabbitMQ.Client'sConnection.CloseAsync. Two facts from the client's 7.2.2 source: a graceful connection close floors any timeout below 30 s up to 30 s and throws on expiry, and a connection whose main loop never finishes stalls on its channel 0 for 5 s plus 20 s, which is the stall I had measured. The graceful channel close was what left the loop unfinished under load.Final shape (
NotifyBakerConsumer.cs,RabbitMqConnection.cs; no other files):StopAsyncis idempotent: one shared shutdown task behind a lock.DisposeAsync. No graceful channel close.AbortAsync(5 s)before it is disposed.Verification: eight stressed runs (two suites concurrently, four rounds) with zero cleanup failures, zero cap warnings, zero dropped deliveries, each about 15 s wall clock, against 43 s and 73 s with failures for commit 2 under the same load; three unstressed whole-solution runs green. The probe was deleted, not committed. The after twin needs nothing: Wolverine owns its shutdown.
Counts re-run by the inventory's method: before twin 2,140 raw / 1,775 non-blank (was 2,061 / 1,705; two files edited, none created). After twin unchanged.
docs/file-inventory.md, the README table, and the CLAUDE.md numbers are updated; the build log has both rounds in full.Judgment call 3: the fix adds 79 lines to the before twin, all shutdown hygiene. That is honest weight a layered codebase carries for owning a raw consumer, and the slide numbers move with it. The alternative was a test-side workaround, which I did not want to do to the proof.
Commit 1: CLAUDE.md and README refresh
docs/critter-stack-audit.mdadded to "Where detail lives".tests/LayerCake.Slices.Testsand the audit doc; zero em dashes.The two manual checks from the handoff (in the build log)
mainafter PR Slice 004: NotifyBaker over RabbitMQ on both twins #13: green, 1m23s.src/frontend/index.htmlopened from disk in the installed Chrome and driven through its own buttons by a Playwright script (the Claude-in-Chrome extension could not inject into any page this session). Both twins: reachable, BDAY10 valid, publish 201, order priced 82.00 / 8.20 / 73.80 on both, baker row highlighted within about 250 ms on both, no page errors; bothlayercake-*-baker-tasksqueues at 1 consumer, 0 ready, 2 published, 2 acked afterwards.docker compose down -vbefore a dry run restores the clean seed.Noted, not changed
.github/workflows/ci.ymlcomment still says only "No PostgreSQL service"; the suite now also starts RabbitMQ via Testcontainers.postgres:17andrabbitmq:4on the demo machine.