Repository navigation
JasperFx 2.56.0
An async-daemon reliability release, plus provenance for the Event Model.
⚠️ Behaviour change: Event Model merge precedence
EventModelDescriptor.Merge no longer resolves competing claims by registration order. Sources now sit on a three-rung ladder of authority and the higher rung wins:
Declared < Derived (from code) < Observed (in production)
This inverts the previous arrangement, where WolverineEventModelSource was registered at index 0 specifically so derived roles would beat overlays. Registration order is now only a tie-breaker between sources on the same rung.
Nothing breaks on upgrade. IEventModelDefinitionSource.Provenance is a default interface member returning Declared, so every existing source ties, every tie still resolves on order, and an application that has not stamped its sources merges exactly as it did on 2.55.0. To adopt the ladder, a source overrides Provenance — one line.
Precedence is per claimed role, not wholesale: a role is claimed when a slice carries a value for it, and a source that does not claim a role never overrides one that does. Slice names, domains, trigger labels and specification links therefore keep coming from declarations and keep winning by default, because nothing else claims them.
Async daemon
A tenant agent seeded below its own committed position no longer dies permanently (#702, thanks @erdtsieck). SubscriptionAgent.Apply's Start branch threw on LastCommitted > HighWaterMark, which funnelled into ReportCriticalFailureAsync → Paused — and nothing restarts a paused agent under a node-distributed daemon, so one momentarily missing ceiling took a tenant's projection out until the next deployment. The seed is now clamped to the committed position with a warning. Nothing is replayed or skipped: loading only ever starts above LastCommitted, so the agent idles until the real mark is routed to it. PolledTenantSet also gained a reference-counted Pin/Unpin that a wholesale SetTenants cannot drop, held across an agent start's priming poll.
High water is primed exactly once, and concurrent starts wait for it (#709). HighWaterAgent.IsRunning was set before the Detect() it exists to gate, so a second concurrent StartAgentAsync skipped the priming and read Tracker.HighWaterMark while it was still 0. The flag now means "detection has completed", a failed detection leaves the agent not running instead of permanently claiming otherwise, and both start paths go through a single-flight rendezvous — so 25-way parallel agent starts prime once rather than launching 25 concurrent max(seq_id) scans. This was reachable on plain non-tenanted stores, not only under sharded tenancy.
Per-tenant catch-up and rebuild hold their tenants across the priming poll (#710). Both still used Activate, which a concurrent reconciliation could drop — leaving catch-up to skip that tenant's work in total silence. Catch-up now also distinguishes "never polled" (logged) from "polled, no events yet" (the benign skip), and PolledTenantSet.Deactivate no longer removes a pinned tenant.
Event Model
- The descriptor carries provenance (#703).
EventModelProvenance,EventModelRole, per-role attribution viaEventModelSliceDescriptor.ProvenanceFor(role)andClaimedBy, the same answer stamped onto every renderedEventModelElement, andIEventModelDefinitionSource.Provenanceso a source declares its rung once. A higher rung replaces a list rather than unioning with it — unioning derived{A, C}with observed{A, B}invents a slice emitting three events nobody claimed. - A source disagreement is now a finding, not a swallowed merge (#704). New
HotspotOrigin.SourceDisagreement, recorded whenever both sides claim a role and the merged answer drops one of them, naming the role, both claims and the rung each came from viaRole/WinningClaim/LosingClaim. New semantics on shipped machinery —HotspotDescriptoralready renders in both viewers. Purely additive: nothing is recorded when nothing is lost, so a model with no disagreements is identical to one produced on 2.55.0.
Blocks
RetryBlock<T>.ShouldRetryandOnTerminalFailure(#701). A caller can now classify a failure as terminal instead of swallowing an exception inside its own handler purely to stop the retry loop.OnTerminalFailureis the capability swallowing cannot provide — the block's owner can meter or dead-letter a give-up that is not a success. Both default to null, so existing blocks are unchanged.
Compliance suite
-
EventStoreExplorerCompliancenow requires the usage descriptor to describe registered projections (#700). It previously asserted onusage.Eventsand nothing else, so a store could fill that one list and leave every other slot onEventStoreUsageempty while passing — which is exactly what happened for several releases in JasperFx/fisher#120.⚠️ This is a new requirement on every store that runs the suite, covering anInlineregistration specifically. It will go red anywhereTryCreateUsagereturns a usage without callingProjections.Describe. Marten and Polecat both call it.
Build
- GitHub Actions pins moved off the deprecated Node 20 runtime (#699).
Full changelog: V2.55.0...V2.56.0