feat(#435): read-only per-cell projection progress query on IEventDatabase#517
Merged
Merged
Conversation
…abase Adds IEventDatabase.ReadProjectionProgressAsync(projectionName, tenantId, token) so monitoring tools can read the progression-row state for a single (projection, tenant) cell without spinning up an IProjectionDaemon. AllProjectionProgress already works but returns every row across every projection x tenant pair, leaving the caller to filter. For a per-cell UI polling loop (e.g. 1Hz against a single visible batch) the targeted query is materially cheaper. Shipped as a default interface member that throws NotSupportedException, not as the abstract member the issue's snippet implies. IEventDatabase is implemented out-of-tree by Marten and Polecat, so an abstract member would break every implementor on a maintenance line the moment it shipped. This matches the graceful-degradation pattern already used ten times over in IEventStore on this branch. The default throws rather than returning null on purpose: null is the documented "no row for this pair yet" answer, so a store that simply has not implemented the query must not borrow it and report a live cell as absent. Targets the 1.0 line because the issue scopes this as a v1.1 candidate; main is on 2.x. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 17, 2026
jeremydmiller
added a commit
that referenced
this pull request
Jul 17, 2026
…abase (#518) Adds IEventDatabase.ReadProjectionProgressAsync(projectionName, tenantId, token) so monitoring tools can read the progression-row state for a single (projection, tenant) cell without spinning up an IProjectionDaemon. AllProjectionProgress already works but returns every row across every projection x tenant pair, leaving the caller to filter. For a per-cell UI polling loop (e.g. 1Hz against a single visible batch) the targeted query is materially cheaper. Shipped as a default interface member that throws NotSupportedException, not as the abstract member the issue's snippet implies. IEventDatabase is implemented out-of-tree by Marten and Polecat, so an abstract member would break every implementor the moment it shipped. This matches the graceful-degradation pattern already used throughout this interface (#356, #407, #450, #473). The default throws rather than returning null on purpose: null is the documented "no row for this pair yet" answer, so a store that simply has not implemented the query must not borrow it and report a live cell as absent. ProjectionProgressRow is deliberately placed in the JasperFx.Events namespace, identical to the 1.0 port in #517, so Marten and Polecat can implement the override source-identically against both lines. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
Jul 17, 2026
…ersists it (#519) AgentStatus was typed non-nullable string, but no event store can populate it today. Both Marten and Polecat create an agent_status column on the progression table and read it back, yet neither has a code path that writes one: - Marten ships mt_mark_event_progression_extended(), whose SQL does write agent_status/pause_reason/running_on_node — but the only reference to it is EventGraph.FeatureSchema.cs, which registers it as a SystemFunction so the DDL is created. Nothing invokes it. - Polecat has MarkExtendedProjectionProgress, which likewise writes the columns and is constructed only from tests (polecat#323). So agent_status reads NULL in both stores, and an implementer of ReadProjectionProgressAsync would have to invent a value to satisfy the type. Make it string? so a store with nothing to report can say so — symmetric with LastHeartbeat, already nullable and documented as "null when the store does not track a heartbeat for it". Caught before release: #518/#517 are merged but ProjectionProgressRow has not shipped (latest JasperFx.Events is 2.27.0), so this costs nothing now. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Addresses #435. Targets the
1.0branch, per the issue's "v1.1 candidate" scoping —mainis on 2.x, and the 1.0 line currently ships JasperFx.Events 1.36.2.What this adds
AllProjectionProgressalready works, but returns every row across every projection × tenant pair and leaves the caller to filter. For a per-cell UI polling loop (1Hz against a single visible batch), the targeted query is materially cheaper.Two decisions worth a reviewer's attention
1. Default interface member, not an abstract one. The issue's snippet shows the method declared bodiless — i.e. abstract.
IEventDatabaseis implemented out-of-tree by Marten and Polecat, so shipping it abstract would break every implementor the moment a 1.x package released. It's a default member throwingNotSupportedExceptioninstead, matching the graceful-degradation pattern already used ten times over inIEventStore.cson this branch.2. The default throws rather than returning
null.nullis the documented "no row for this pair yet" answer. If an unimplemented store borrowed it, a monitoring UI would render a running projection as having no progress — a silently wrong reading rather than a loud unsupported one. Same reasoning as #503.Open question for the companion PRs (worth your eyes)
I could not verify that
AgentStatusandLastHeartbeatare actually available from the progression tables — I don't have the Marten/Polecat schemas in this repo, and the issue doesn't cite columns for them.Sequenceis clearly a progression-row value; the other two read more like daemon/tracker state. Ifmt_event_progression/pc_event_progressiondon't carry them, the companion PRs will have to either join something else or leave them stubbed, which would make the record's shape misleading. Flagging rather than guessing — the record shape is easy to trim now and awkward to trim after it ships in a 1.x package.Relatedly:
AgentStatusis typed asstringper the issue, not the existingJasperFx.AgentStatusenum. That tolerates store-specific states outside Running/Stopped/Paused, but if you'd rather have the enum, now is the cheap moment.Test coverage
src/EventTests/Daemon/ReadProjectionProgressDefaultsTests.cs— 5 tests: the default throws (both store-global and tenant-scoped), an implementing store can return a row, an implementing store'snullsurvives as "no row", and the record's null-tenant semantic.Verified by mutation: making the default return
nullinstead of throwing fails exactly the two guard tests and leaves the other three green.Verification
dotnet build jasperfx.sln— succeeded, 0 errorssrc/EventTests— 267/267 pass on net8.0, net9.0, and net10.0Note: the build emits pre-existing
CS8785warnings (AggregateEvolverGeneratorfailing with anInvalidCastException). Not from this change — I confirmed it reproduces on a clean 1.0 checkout with my changes stashed. It looks like the evolver cast bug fixed onmainby #505 and never backported to 1.0; may be worth a separate backport.Status note
The issue is de-prioritized — CritterWatch#309 is paused until after CritterWatch 1.0, and you noted "feel free to scope / land if it's convenient on your side independently; we'll consume when we're ready." This PR is that: the abstraction plus its degradation contract, with no consumer coupling. The Marten/Polecat overrides remain downstream work.
🤖 Generated with Claude Code