Repository navigation
DaemonTests on Bobcat 1.0.0-alpha.1: rendered daemon specs and diagnosed catch-up waits (bobcat#562) - #5690
Merged
Conversation
… catch-up waits (bobcat#562) Bobcat, Bobcat.Xunit, Bobcat.Generators and Bobcat.Supervisor move from 0.27.3 / 0.60.4 to 1.0.0-alpha.1, one BobcatVersion for all four. Bobcat.Xunit 1.0 requires xunit.v3 4.x on Microsoft.Testing.Platform 2.5.1, which cannot share a test project with the 3.x the rest of the repo uses, so DaemonTests and DaemonTests.ManualOnly take 4.0.2 through VersionOverride and nothing else moves. CoreTests drops a ProjectReference to DaemonTests it only used for a transitive Lamar, which would otherwise have dragged it onto xunit 4. The build takes JasperFx 2.90.2 for the supervisor; Marten's own JasperFx floor is untouched. - DaemonContext's step helpers use [Given]/[When]/[Then]; [BobcatScenario] goes (Bobcat.Xunit records every [BobcatFeature] class). - New WaitForCatchUp / WaitForShard helpers throw Bobcat's ProjectionCatchUpTimeoutException with every shard's progress on a timeout, instead of a bare TimeoutException. RebuildProjection is a step. - Rendered as specifications: build_aggregate_projection, rebuilding_an_inline_projection, when_skipping_events_in_daemon, catching_up_mode_for_projections_and_subscriptions and rebuild_mode_start_resumes_after_a_failure, with BobcatProjectAssertions on so Shouldly checks render as Then steps. - New CritterStackSpec specifications over a hosted Solo daemon, and tests pinning the forced catch-up timeout diagnosis through both CritterStackScenario and DaemonContext. Gate: ./build.sh test-daemon (net10.0, Release, --disable-test-retry) 374/374 at one lane, and 374/374 twice at two lanes. CI stays at one worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GmWSVvZAZZSYUkzyLFpi9N
This was referenced Oct 17, 2026
Closed
Member
Author
|
Linq (net9, PG15, Newtonsoft) failed at package restore, before any test ran: This is a stale nuget.org view on that one runner, not this change. Bobcat 1.0.0-alpha.1 was published and listed at 01:13 UTC, and nuget.org now returns 57 versions. Every other job restored the same Generated by Claude Code |
This was referenced Oct 17, 2026
The Daemon job failed three of the new specifications, and Bobcat's catch-up diagnosis named both causes. - 'ShipmentSummary:All' at 2 of 4: the rebuild specification shared a host with the others. JasperFx's rebuild stops the shard's continuous agent, rebuilds with dedicated agents, disposes them and never starts the continuous agent again, so every specification that ran after it on CI waited on a stopped shard. Locally it happened to run last. It now has a host of its own. Whether a hosted daemon should resume a shard after a rebuild is left as a question for review. - An undiagnosed TaskCanceledException: Marten's WaitForNonStaleProjectionDataAsync documents a TimeoutException, but a timeout that lands inside its first round trip (a cold store's first storage check, a slow connect) escaped as a TaskCanceledException, an OperationCanceledException or a MartenSchemaException/NpgsqlException wrapping one. Bobcat only diagnoses a TimeoutException. Both phases now report a cancellation by their own timeout as the TimeoutException the method promises. wait_for_non_stale_data_error_cases pins it on a cold store at 1, 20 and 100ms; without the fix all three fail (TaskCanceledException twice, MartenSchemaException once). ./build.sh test-daemon (net10.0, Release, --disable-test-retry): 377/377. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GmWSVvZAZZSYUkzyLFpi9N
This was referenced Oct 18, 2026
This was referenced Oct 20, 2026
This was referenced Oct 23, 2026
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.
JasperFx/bobcat#562: Bobcat applied to
src/DaemonTests, on Bobcat 1.0.0-alpha.1, ready for review. A comment on bobcat#562 planned to wait for 1.0.0-rc.1; Jeremy asked for it to be done now on alpha.1, so this pins 1.0.0-alpha.1 from nuget.org.#5363 had already rendered two classes on 0.27.3 through decorated
DaemonContexthelpers. Nothing in DaemonTests used the Gherkin lane, so the 0.50.0 removal (bobcat#471) cost nothing here. This PR moves that work to 1.0, routes the projection waits through Bobcat's catch-up diagnosis, addsCritterStackSpecspecifications, and renders three more daemon-behaviour classes.What changed
Packages.
Bobcat,Bobcat.Xunit,Bobcat.Generators(0.27.3) andBobcat.Supervisor(0.60.4) all move to 1.0.0-alpha.1 through a singleBobcatVersionproperty inDirectory.Packages.props.xUnit / MTP alignment. They can't line up repo-wide in this PR, so here is what was done:
VersionOverride)AssemblyInfo.csfiles useCollectionBehavior(DisableTestParallelization = true), which is a compile error on 4.x. DaemonTests now uses[assembly: Xunit.v3.Parallelization(Mode = ParallelMode.None)].ProjectReferenceto DaemonTests. It was used only for a transitive Lamar, and it would have dragged xunit 4 into CoreTests. I removed it and added Lamar directly. CoreTests builds on both TFMs and runs 826 total, 0 failed (net10.0).build/takes 2.90.2 viaVersionOverride, because otherwise NU1605 blocks the supervisor. Marten's own JasperFx floor is unchanged. Moving it is your call.dotnet teston an MTP 2.x host needsglobal.json→"test": {"runner": "Microsoft.Testing.Platform"}, which is a repo-wide switch. I didn't make it. DaemonTests runs through./build.sh test-daemon(the supervisor) or its built executable, which is how CI runs it already.Rendered specifications.
DaemonContext's helpers now use[Given]/[When]/[Then].[BobcatScenario]is gone, because Bobcat.Xunit now records every[BobcatFeature]class.<BobcatProjectAssertions>is on, so statement-level Shouldly checks in feature classes render as Then steps.when_skipping_events_in_daemon,catching_up_mode_for_projections_and_subscriptionsandrebuild_mode_start_resumes_after_a_failure, alongside the existingbuild_aggregate_projectionandrebuilding_an_inline_projection.WaitForShardwhere they are awaited;WaitForShardStatecompletes from the snapshot, per Unify aggregator lookup in ProjectionCollection to use the same aggre… #568;ShouldHaveTheSameElementsAsbecame Shouldly's orderedShouldBein the one rendered class;CritterStackSpec.Specifications/async_daemon_specifications.csspecifies three daemon behaviours: catch-up after an append, delete on cancel, and rebuild through the running daemon. They run against a hosted app withAddAsyncDaemon(DaemonMode.Solo), and every read opens with Bobcat's own catch-up step.Catch-up diagnostics. Two new
DaemonContexthelpers,WaitForCatchUp(readModel)andWaitForShard(daemon, shard, sequence, readModel). On a timeout they throwProjectionCatchUpTimeoutExceptionwith the progress list fromEventStores.ProjectionsBehindAsync, instead of a bareTimeoutException.catch_up_timeout_diagnostics.cspins that diagnosis through bothCritterStackScenarioandDaemonContext.Supervisor. The build compiles against Supervisor 1.0.0-alpha.1. Stall detection was already on for every suite: report-only at 5 minutes, with a 30s heartbeat. Across these runs it named nothing as stalled. The longest DaemonTests are the
Bug_5567hot/cold cases at about 60s each.A rendered spec
From
./DaemonTests --filter-class …, written withBOBCAT_SPEC_CONSOLE_FILE(see JasperFx/bobcat#676):A deliberately forced catch-up timeout
A throwaway spec, not committed, read a
ShipmentSummaryfrom a host with the daemon disabled andProjectionTimeout = 5s:And through
DaemonContext.WaitForShard, from the committedwaiting_on_a_shard_that_never_starts_names_where_every_shard_stood:The diagnosis also found a real bug in this PR before review. The first two full gate runs failed the new specs with
Behind: 'ShipmentSummary:All' at 0 of 4, which meant the hosted daemon had never run its shard. The cause was that the fixture cleaned event data after the Solo daemon started. That restarted the sequence underneath a shard that had already loaded its old progress, so the shard sat "ahead" of every new event. Isolated runs won that race and full runs lost it. A 3s delay before the clean reproduced it deterministically. The fixture now cleans beforeStartAsync.Two things the first CI run found (second commit)
Bobcat's diagnosis named both of them on the first CI run:
WaitForNonStaleProjectionDataAsyncnow always throws theTimeoutExceptionit documents.TaskCanceledException, anOperationCanceledException, or aMartenSchemaException/NpgsqlExceptionwrapping one.TimeoutException. So on the slower CI runner, the forced timeout incatch_up_timeout_diagnosticsshowed up as a bareTaskCanceledException.TimeoutException.wait_for_non_stale_data_error_casespins it on a cold store at 1, 20 and 100ms. Without the fix, all three fail.e7f96db), if you'd rather take it on its own.'ShipmentSummary:All' at 2 of 4.rebuildProjectionCorestops the continuous agent, rebuilds with dedicated agents and disposes them, but never starts the continuous agent again. So every spec that ran after the rebuild spec on that host waited on a stopped shard. Locally it happened to run last.RebuildProjectionAsyncis your call. I left Marten and JasperFx behaviour as is.What I ran
Local Postgres 16 (
Host=localhost;Port=5432;Database=marten_testing;Username=postgres;password=postgres), .NET SDK 10.0.112. Only the net10.0 runtime was available here, so net9.0 was built but not run../build.sh test-daemon --framework net10.0 --configuration Release --disable-test-retry(1 lane)--test-workers 2(a database per lane), twicedotnet build -c Release, DaemonTests / DaemonTests.ManualOnly / CoreTests / buildNo test was skipped or disabled, and no Retry trait was added.
For you to decide: two green local runs at two lanes saved about 2 minutes. I left CI at one worker and updated the comments in
tests.ymlandCLAUDE.mdto record the measurement. If you want it, it'sworkers: 2onTestDaemonplus a committed ledger from--update-test-ledger.Upstream Bobcat issues
Filed on JasperFx/bobcat (by the coordinating session, from drafts written here). Each links back to bobcat#562.
BOBCAT_SPEC_CONSOLE=1prints nothing when stdout is redirected (xunit.v3 4.0.2 / MTP 2.5.1). It works in a TTY and withBOBCAT_SPEC_CONSOLE_FILE, but not when stdout is piped or captured, which is the CI case. This was already filed; this PR's evidence was added there as a comment.CritterStackScenario. There's no public "wait and diagnose" helper,ProjectionCatchUpTimeoutExceptionrequires a read type even for shard waits, andAfterAsyncDaemonsCatchUpAsyncthrows a bareTimeoutException.ProjectionsBehindAsyncswallows why the store "could not say". It also doesn't document thatof Mis the highest sequence, not an event count.○as a step that never ran, so a green scenario looks partly skipped.global.jsonis needed fordotnet test.BobcatProjectAssertionson, a multi-line assertion subject is written into a generated string literal with its newline unescaped, which produces 58 compile errors in generated files. Worked around here with a local.🤖 Generated with Claude Code
https://claude.ai/code/session_01GmWSVvZAZZSYUkzyLFpi9N