feat(#537): wire the extended progression write path into the async daemon - #538
Merged
Merged
Conversation
…aemon Extended progression tracking (ExtendedProgressionEnabled) created the schema columns (heartbeat, agent_status, pause_reason, running_on_node) and a faithful read surface, but no daemon path ever persisted them — the write half was built and never connected (#519). This adds the daemon-level wiring so both Marten and Polecat can implement the store-side write: - IEventDatabase.WriteExtendedProgressionAsync(ShardState, CancellationToken): the store-side persistence hook, default-implemented as a graceful no-op so existing stores compile and degrade silently until they implement it. The contract forbids touching progression (last_seq_id) from this path so a telemetry write can never race a batch commit into losing progress. - IEventStore.ExtendedProgressionEnabled (default false): store-level gate, read live per publication so runtime opt-in (e.g. CritterWatch's forced enable) is honored. - ExtendedProgressionWriter: a ShardStateTracker observer the daemon subscribes at construction. Status transitions (Started/Paused/Stopped + pause_reason) write immediately; heartbeat ticks and batch-commit publications are throttled to one write per 5s per shard. Writes are serialized on a background block and are strictly best-effort: a failing store write is logged at debug and can never fail or stall the shard, and a slow database can never back up the tracker's publication loop. Carries the assigned node number through to running_on_node when a distribution layer stamped it. The heartbeat write path is deliberately NOT coupled to progress advance — a paused or stalled shard is exactly when the persisted heartbeat/status matters most (the CritterWatch node-down / HWM-frozen alerting case, CritterWatch#750). Closes #537 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 20, 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.
Closes #537. Companion Marten filing: JasperFx/marten#4981; surfaced by JasperFx/CritterWatch#750.
What
Extended progression tracking (
ExtendedProgressionEnabled) created the schema columns (heartbeat,agent_status,pause_reason,running_on_node) and a faithful read surface, but no daemon path ever persisted them — "built and never connected" (#519). This PR adds the daemon-level write path, flowing through the store abstraction so Marten and Polecat implement only the store-side write.Design
IEventDatabase.WriteExtendedProgressionAsync(ShardState, CancellationToken)— the store-side persistence hook, default-implemented as a graceful no-op (the established pattern), so every existing store compiles and degrades silently until it implements the write. The documented contract forbids touching progression (last_seq_id-equivalents) from this path, so a telemetry write can never race a concurrent batch commit into losing or regressing progress.IEventStore.ExtendedProgressionEnabled(defaultfalse) — store-level gate, read live per publication so a runtime flip (e.g. CritterWatch's forced enable of the DI-registeredIEventStoreInstrumentation) is honored.ExtendedProgressionWriter— aShardStateTrackerobserver subscribed byJasperFxAsyncDaemonat construction (the same shape asStoreUriStampingObserver/SkippedEventsCountObserver):Started/Paused+pause_reason/Stopped) write immediately — a paused or stalled shard is exactly when persisted status matters most (the CritterWatch node-down / HWM-frozen alerting case).AssignedNodeNumberthrough torunning_on_nodewhen a distribution layer stamped it.Test evidence
New
EventTests.Daemon.ExtendedProgressionWriterTests(9 tests): transitions write through; heartbeats throttle per shard (FakeTimeProvider); zero writes when the store hasn't opted in; HighWaterMark/AllProjections and telemetry-less publications skipped; a throwing database write is swallowed and subsequent writes continue; node-number carry; and an end-to-end pass through a realShardStateTracker+SubscriptionAgent(start → Running write with heartbeat, stop → Stopped write).Full local runs green: EventTests 562/562, EventStoreTests 72/72, CoreTests 470/470.
Clean-room end-to-end (with the companion Marten branch implementing the store write): the preserved #537 repro (plain
AddMarten().AddAsyncDaemon(Solo), one async projection, flag forced via the DIIEventStoreInstrumentation) on locally-packed JasperFx.Events2.31.0-dev.1+ Marten9.17.0-dev.1:Previously (2.29.0/2.30.1/2.30.2 × Marten 9.16.x): every one of those columns NULL at all times.
Store-side follow-ups
MartenDatabase.WriteExtendedProgressionAsyncas an UPDATE-only decoration ofmt_event_progression+ theIEventStore.ExtendedProgressionEnabledoverride — companion PR on JasperFx/marten referencing #4981.🤖 Generated with Claude Code