Stop writing a periodic per-shard heartbeat nobody reads (#622) - #628
Merged
Conversation
jeremydmiller
changed the base branch from
gh/621-extended-progression-lifecycle
to
main
August 4, 2026 12:11
EnableExtendedProgressionTracking cost one pooled connection and one transaction per database per node every 5 seconds -- ~37 connection acquisitions/sec/node on a 512-shard-database deployment, to keep 6-12 rows per database current, writing into the same rows the progress writer updates. It made a production web app unresponsive (marten#5167). The audit found nothing reads any of it. Not JasperFx (the only read of LastHeartbeat anywhere is the writer checking its own input; the CLI status grid renders sequence and high-water only). Not CritterWatch, the sole intended consumer, which gets agent status and heartbeats by reflecting off in-memory objects and drops the extended columns it already receives. And its AgentDown alert fires at 60s on a 30s tick fed by a 15s poll -- a 5s write is a 12x oversample of something nothing downstream can observe below 15s. Split the periodic beat from the transitional status: - ExtendedProgressionWriter drops non-transition publications by default, outright rather than queueing them, so a stale beat can't ride along on the next transition either. - Started/Paused/Stopped writes are unchanged -- rare, and the data the "durable across a crash" story was actually about. - HeartbeatWriteInterval defaults to TimeSpan.Zero (off) and is now reachable from application code through DaemonSettings.ExtendedProgressionHeartbeatInterval. Before this it was a hardcoded 5 seconds that no knob could reach, which is what made the cost impossible to opt out of. A positive value restores the old behavior at that cadence -- a compatibility hatch, not a recommendation. Also corrects the comment at the Path B tenant high-water heartbeat, which claimed the beat "surfaces the same beat on the live Tracker (and the ExtendedProgression columns)". It never reached the columns: OnNext drops HighWaterMark and AllProjections states, as its own test pins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fuk1GybEEmohFmboJuM4Po
jeremydmiller
force-pushed
the
gh/622-heartbeat-cost-model
branch
from
August 4, 2026 12:11
650b15b to
b887137
Compare
This was referenced Aug 5, 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 #622. Rebased onto
mainnow that #625 (the writer-accumulation fix) has merged — #621 is the bug that multiplied this cost without bound; this is the cost model that remains after it.What the audit found
EnableExtendedProgressionTrackingcost one pooled connection and one transaction per database per node every 5 seconds — ~37 connection acquisitions/sec/node on a 512-shard-database deployment, to keep 6–12 rows per database current, writing into the same rows the progress writer updates. It made a production web app unresponsive (marten#5167).And nothing reads it. Not JasperFx — the only read of
LastHeartbeatanywhere is the writer checking its own input before writing it, and the CLI daemon status grid renders sequence and high-water only. Not CritterWatch, the sole intended consumer, which obtains agent status and heartbeats by reflecting off in-memory objects (CurrentAgents(), the tracker'sShardState) and drops the extended columns it already receives over the wire. ItsAgentDownalert fires atheartbeatAge > 60s, on a 30s tick, fed by a 15s poll — a 5s write is a 12× oversample of something nothing downstream can observe below 15s resolution.The change
Proposal items 1–3 from the issue.
1. Stop the periodic beat.
ExtendedProgressionWriterdrops non-transition publications by default — outright, rather than queueing them, so a stale beat can't ride along on the next transition write either.2. Keep the transitional columns.
Started/Paused/Stoppedwrites are untouched: rare, and the data the "durable across a crash" story was actually about.3. Make the interval configurable.
HeartbeatWriteIntervalnow defaults toTimeSpan.Zero(off) and is reachable from application code through the newDaemonSettings.ExtendedProgressionHeartbeatInterval. Before this it was a hardcoded 5 seconds that noDaemonSettingsknob could reach — the field was private on the daemon, exposed by no property, and the only code that set it was the class's own unit test. A positive value restores the old behavior at that cadence; that is the compatibility hatch, not the recommended shape.The coalescing/batching machinery is unchanged and still fully tested — it just has to be asked for now.
Also
Corrects the comment at the Path B tenant high-water heartbeat (
JasperFxAsyncDaemon.cs), which claimed the beat "surfaces the same beat on the live Tracker (and the ExtendedProgression columns)". It never reached the columns —OnNextdropsHighWaterMarkandAllProjectionsstates, asskips_high_water_mark_and_all_projections_statespins. The issue asked for this regardless of the outcome.What is deliberately not done
NodeHeartbeat, #837). Nothing to add here.pause_reason,running_on_node). Left alone — they're free now that they're only written on transition.Tests
ExtendedProgressionWriterTests: new coverage for the default (no periodic write even after 5 minutes of publications), transitions still written with the beat off and not carrying a dropped heartbeat along, and a positive interval restoring the beat. The existing periodic-beat tests now opt in explicitly, which is the honest statement of where that behavior lives.ExtendedProgressionHeartbeatIntervalTests: end-to-end through a real daemon + shared tracker, provingDaemonSettings.ExtendedProgressionHeartbeatIntervalreaches the writer — null andTimeSpan.Zeroare off, a positive value is on, transitions persist regardless.Full
EventTestssuite passes (693).🤖 Generated with Claude Code
https://claude.ai/code/session_01Fuk1GybEEmohFmboJuM4Po