feat(#435): read-only per-cell projection progress query on IEventDatabase (2.x)#518
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 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>
This was referenced Jul 17, 2026
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 on the 2.x line. Companion to #517, which carries the identical change to the
1.0branch (the issue is scoped a "v1.1 candidate", but the surface is wanted on both lines so Marten/Polecat can implement against whichever they target).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.Backwards compatibility
Shipped as a default interface member throwing
NotSupportedException, not as the abstract member the issue's snippet implies.IEventDatabaseis 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
nulldeliberately.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 — silently wrong beats loudly unsupported. Same reasoning as #503.Two things worth a reviewer's attention
1.
ShardStatusalready exists on 2.x and overlaps.JasperFx.Descriptors.ShardStatus(ShardName,State,ProcessedSequence,EventStoreSequence,Error) covers adjacent ground. I keptProjectionProgressRowseparate because the grains genuinely differ —ShardStatusis per-shard with no tenant dimension and is reached throughIEventStore.GetProjectionStatusesAsync(all projections, all shards), whereas this is a targeted per-(projection, tenant)read offIEventDatabase. If you'd rather reconcile the two shapes, now is the cheap moment; that's a judgement call I didn't want to make unilaterally. This overlap does not exist on the 1.0 line, which is why #517 doesn't mention it.2.
AgentStatus/LastHeartbeatsourcing is unverified. Same caveat as #517: I could not confirm these are actually available frommt_event_progression/pc_event_progression.Sequenceis plainly a progression-row value; the other two read more like daemon/tracker state. If the columns don't exist, the companion PRs will have to stub them and the record's shape becomes misleading. Flagging rather than guessing — easy to trim now, awkward after it ships.Relatedly,
AgentStatusis astringper the issue rather than the existingJasperFx.AgentStatusenum, which tolerates store-specific states outside Running/Stopped/Paused.Placement
ProjectionProgressRowsits in theJasperFx.Eventsnamespace (alongsideIEventDatabase) rather thanJasperFx.Descriptors, identical to #517, so Marten and Polecat can write the override source-identically against both the 1.x and 2.x lines. Note 2.x is already inconsistent here —ProjectionStatuslives inJasperFx.DescriptorswhileDeadLetterShardCountlives inJasperFx.Events.Daemon— so there was no single convention to follow; cross-line consistency seemed the more useful tie-breaker.Test coverage
src/EventTests/Daemon/ReadProjectionProgressDefaultsTests.cs— 5 tests: the default throws (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 on this branch: 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— 503/503 pass on net9.0 and net10.0Follow-ups filed
jasperfx/marten— implement againstmt_event_progressionjasperfx/polecat— implement againstpc_event_progression🤖 Generated with Claude Code