Skip to content

feat(#435): read-only per-cell projection progress query on IEventDatabase#517

Merged
jeremydmiller merged 1 commit into
1.0from
fix-435-read-projection-progress
Jul 17, 2026
Merged

feat(#435): read-only per-cell projection progress query on IEventDatabase#517
jeremydmiller merged 1 commit into
1.0from
fix-435-read-projection-progress

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Addresses #435. Targets the 1.0 branch, per the issue's "v1.1 candidate" scoping — main is on 2.x, and the 1.0 line currently ships JasperFx.Events 1.36.2.

What this adds

ValueTask<ProjectionProgressRow?> IEventDatabase.ReadProjectionProgressAsync(
    string projectionName, string? tenantId, CancellationToken token);

public record ProjectionProgressRow(
    string ProjectionName, string? TenantId, long Sequence,
    string AgentStatus, DateTimeOffset? LastHeartbeat);

AllProjectionProgress already 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. IEventDatabase is 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 throwing NotSupportedException instead, matching the graceful-degradation pattern already used ten times over in IEventStore.cs on this branch.

2. The default throws rather than returning null. null is 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 AgentStatus and LastHeartbeat are 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. Sequence is clearly a progression-row value; the other two read more like daemon/tracker state. If mt_event_progression / pc_event_progression don'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: AgentStatus is typed as string per the issue, not the existing JasperFx.AgentStatus enum. 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's null survives as "no row", and the record's null-tenant semantic.

Verified by mutation: making the default return null instead of throwing fails exactly the two guard tests and leaves the other three green.

Verification

  • dotnet build jasperfx.sln — succeeded, 0 errors
  • src/EventTests — 267/267 pass on net8.0, net9.0, and net10.0

Note: the build emits pre-existing CS8785 warnings (AggregateEvolverGenerator failing with an InvalidCastException). 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 on main by #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

…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>
@jeremydmiller
jeremydmiller merged commit 5d1d62c into 1.0 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant