Skip to content

feat(#537): wire the extended progression write path into the async daemon - #538

Merged
jeremydmiller merged 2 commits into
mainfrom
fix/537-extended-progression-writes
Jul 20, 2026
Merged

feat(#537): wire the extended progression write path into the async daemon#538
jeremydmiller merged 2 commits into
mainfrom
fix/537-extended-progression-writes

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

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 (default false) — store-level gate, read live per publication so a runtime flip (e.g. CritterWatch's forced enable of the DI-registered IEventStoreInstrumentation) is honored.
  • ExtendedProgressionWriter — a ShardStateTracker observer subscribed by JasperFxAsyncDaemon at construction (the same shape as StoreUriStampingObserver / SkippedEventsCountObserver):
    • Agent status transitions (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).
    • Heartbeat ticks (the agents' existing ~10s timer) and per-batch-commit publications are throttled to one write per 5s per shard, so the heartbeat column advances on every tick without turning every batch commit into an extra progression-table write. The heartbeat write path is deliberately not coupled to progress advance.
    • Writes are serialized on a background block and strictly best-effort: a throwing store write is logged at debug and never fails or stalls the shard; a slow database never backs up the tracker's publication loop.
    • Carries AssignedNodeNumber through to running_on_node when 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 real ShardStateTracker + 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 DI IEventStoreInstrumentation) on locally-packed JasperFx.Events 2.31.0-dev.1 + Marten 9.17.0-dev.1:

--- running, t+15s ---  Counters:All  seq=47   heartbeat=4:50:40 PM  agent_status=Running  pause_reason=NULL
--- running, t+30s ---  Counters:All  seq=97   heartbeat=4:50:55 PM  agent_status=Running  pause_reason=NULL
--- running, t+45s ---  Counters:All  seq=146  heartbeat=4:51:10 PM  agent_status=Running  pause_reason=NULL
=== stopping host ===
--- after host stop --- Counters:All  seq=146  heartbeat=4:51:15 PM  agent_status=Stopped

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

  • Marten (required for end-to-end): MartenDatabase.WriteExtendedProgressionAsync as an UPDATE-only decoration of mt_event_progression + the IEventStore.ExtendedProgressionEnabled override — companion PR on JasperFx/marten referencing #4981.
  • Polecat: inherits the graceful no-op until it implements the write (polecat#323 remains the tracker; its heartbeat-on-progress-advance path is unaffected).

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits July 20, 2026 11:52
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant