Releases: JasperFx/wolverine
Release list
Wolverine 6.26.0
Upgrade note
This release moves the Critter Stack dependencies forward together:
| package | from | to |
|---|---|---|
| JasperFx, JasperFx.Events (+ both source generators) | 2.46.0 | 2.47.0 |
| Marten, Marten.AspNetCore, Marten.Newtonsoft | 9.22.6 | 9.23.0 |
| Polecat | [5.7.0,6.0.0) (resolving to 5.7.0) |
[5.12.0,6.0.0) |
Polecat users get the larger jump of the two: the old range pin resolved to its 5.7.0 floor, so this is 5.7.0 → 5.12.0 in practice.
If you reference JasperFx.Events.SourceGenerator explicitly and reference Polecat, you may hit CS0433 ("the type <X>Evolver exists in both <YourAssembly> and <YourAssembly>"). The generator ships both bundled inside the Polecat nupkg and as a standalone package; when the two copies are the same version they load as two analyzer instances and each emits every projection's Evolver dispatcher. Polecat 5.12.0 bundles 2.47.0, which is what this release pins, so the pair now matches. The fix is to drop one instance — see PolecatTests.csproj for the DropDuplicateBundledEventSourceGenerator target we use, which keeps the explicitly-pinned generator and removes the bundled duplicate.
Two new packages
WolverineFx.DataAnnotationsValidation and WolverineFx.FluentValidation.Grpc are published for the first time in this release. Both were documented as installable but had never actually shipped — every version returned BlobNotFound from nuget.org — because each declared a PackageId while being absent from the packaging list. If you followed the Data Annotations validation or gRPC error details docs and found the package missing, it exists now. A build-time check keeps the two lists from drifting apart again. (#3905, #3909)
Fixes
A [WriteAggregate]-only chain now reports IsTransactional correctly. The Marten and Polecat aggregate handler workflows appended a SaveChangesAsync postprocessor but never set IChain.IsTransactional, so a chain reported having no transactional middleware while its generated code committed. The disagreement was visible to IHttpPolicy authors, who had no reliable signal for whether a chain would commit — the workaround was an unused IDocumentSession parameter on every such endpoint, purely to flip the detection. Thanks to @esond for the report and the fix. (#3893, #3901)
Outgoing batches no longer serialize eagerly. OutgoingMessageBatch built its contiguous byte[] in its constructor whether or not anything read it. (#3906)
Aggregate handler workflow unification (#3907, increment one)
Wolverine has carried two near-identical copies of the aggregate handler workflow — one in Wolverine.Marten, one in Wolverine.Polecat — and improvements had been landing on one copy at a time. This release starts implementing it once, in core.
Nothing is retired and no public API changes. [WriteAggregate], [ReadAggregate], [AggregateHandler], [ConsistentAggregate], Events, MartenOps/PolecatOps and the rest of each integration's surface stay exactly where they are.
What landed:
- The drift between the two copies is reconciled, taking whichever side was correct rather than merging mechanically. The substantive one:
AggregateHandling.DetermineVersionMembernow returnsMemberInfo?— Marten's copy used a null-forgiving operator that was masking a real null fromIAggregateVersioning.VersionMember. Polecat regains AOT annotations it had dropped. Two reflectively-closed codegen frames widen aclassconstraint tonotnull, matchingIEventStream<T>'s own declaration; the narrower form threw for a struct aggregate instead of generating the same correct code. IEventSourcingFrameProvider, the store seam — deliberately a sibling ofIPersistenceFrameProviderrather than new members on it, so stores without event sourcing never grow no-op aggregate members.- The first shared mechanism moved into
Wolverine.Persistence.EventSourcing: the event-capture frames andDetermineEventCaptureHandling, all written purely againstJasperFx.Events'IEventStream<T>. - A reflection test enforcing that no core type shares a simple name with a public type in a
Wolverine.<Store>integration, so aCS0104ambiguity for users is caught by a failing test instead.
The bulk extraction continues in #3911. Also in this release: #3908, which pins what [Storage(typeof(...))] actually promises against a Marten ancillary store.
Upstream halves of this work: JasperFx/jasperfx#648, JasperFx/marten#5221, JasperFx/polecat#453.
Wolverine 6.25.5
6.25.5 supersedes the never-published 6.25.4 (its tag and release were retired), so the first two fixes below make their first NuGet appearance here.
Fixes
PostgreSQL dead-letter and outgoing counts are exact for small tables (GH-3885)
The PostgreSQL message-store counts for the dead-letter and outgoing tables now report exact numbers for small tables instead of the estimate that could read as zero right after activity. First staged for 6.25.4; this is its first published release.
The durable inbox routes by endpoint for sticky handlers (GH-3886)
Durable inbox recovery now routes each envelope by its owning endpoint, so sticky-handler ([StickyHandler] / endpoint-scoped) messages recovered from the inbox execute on the endpoint they were received on rather than falling back to the default route. Also first staged for 6.25.4.
Never export empty metrics snapshots + idle-tenant eviction (#3891)
Wolverine no longer exports metrics snapshots that contain no data, and per-tenant metric state for tenants that have gone idle is evicted after a configurable number of cycles via WolverineOptions.Metrics.TenantIdleEvictionCycles. This is the upstream half of CritterWatch#963 — at very high tenant counts, idle tenants no longer pin memory or pad every export.
Agents that exhaust node-local auto-restarts are released to a capable peer (GH-3888, #3896)
When a stalled agent uses up its node-local auto-restart budget, the node now releases the agent so a capable peer can pick it up, instead of retrying forever on the same node. A capability embargo prevents the agent from bouncing straight back to the node that just failed it.
Short-circuiting Before + Finally middleware no longer NREs (GH-3892, #3895)
Middleware that combines a short-circuiting Before method with a Finally method no longer produces a NullReferenceException at codegen time — and Finally now runs on the short-circuit path, as the middleware contract promises.
Saga diagnostics tolerate an unprovisioned saga table (GH-3887, #3894)
DatabaseSagaStoreDiagnostics.ReadSagaAsync / ListSagaInstancesAsync treat a missing saga table (Postgres 42P01 / SQL Server 208) as null / empty rather than surfacing a raw undefined-table error. A declared-but-never-persisted saga is a legitimate state, since AddSagaType is optional.
Polecat TransportSchemaName is honored (GH-3884, #3897)
PolecatIntegration.TransportSchemaName is now actually applied — previously the setting was inert and the transport tables always landed in the default schema.
Improvements
The stalled-agent auto-restart path is testable (#3890)
The auto-restart path now runs on TimeProvider, making it deterministic under test — with coverage added. Thanks @erdtsieck!
Message types can be exempted from partitioned processing (GH-3899, #3902)
MessagePartitioning.ExemptFromPartitionedProcessing<T>() exempts a message type from partitioned (GroupId-keyed) processing — exempt types ride the endpoint's normal parallelism while partitioned types keep strict per-group ordering.
Batched members' DeliverBy expiry is enforced again (GH-3898, #3903)
Expired members are shed at batch assembly with the normal discard observability, and a whole-batch backstop expires batches whose every member has lapsed.
The sharded execution block deserializes in parallel with ordered emission (GH-3900, #3904)
The sharded execution block's decompress/deserialize stage now runs N-wide while preserving per-group FIFO byte-for-byte.
Dependencies
- JasperFx / JasperFx.Events 2.39.5 -> 2.46.0 — includes the high-water-detection memory fix for very high tenant counts (#644, the 2,000+ tenant OOM).
- Weasel 9.23.0 -> 9.24.0 — parallel
db-apply/db-assertacross physical databases (weasel#431/#442), per-fingerprint schema stamp keying (weasel#439), SQL ServerCREATE DATABASEpostcondition (weasel#415). - Marten 9.22.2 -> 9.22.6 — every-matching-masking-rule fix (marten#5199) plus the compliance-suite enrollments.
Wolverine 6.25.3
A silent data-plane bug for anyone combining Marten with a database-backed transport. Found from a user's minimal reproduction against CritterWatch.
What's Changed
- GH-3883: the Marten integration no longer clobbers an explicit transport schema by @jeremydmiller in 694bf51f7
IntegrateWithWolverine() registers MartenIntegration as an IWolverineExtension, so its Configure() runs at host build — after an inline UsePostgresqlPersistenceAndTransport(..., transportSchema: ...) in the same options lambda. It then stamped its own schema names onto the shared PostgreSQL transport unconditionally, so the integration's defaults silently overwrote whatever the caller asked for.
The failure lands on the data plane rather than at startup, which is what makes it expensive to diagnose. A host without Marten honours the configured schema and publishes to {configured}.wolverine_queue_x; a Marten-backed consumer listens on wolverine_queues.wolverine_queue_x. Auto-provision creates both tables happily, nothing is logged on either side, and no message is ever delivered — the publisher's rows just accumulate in a table nobody polls:
myapp_queues | wolverine_queue_orders <- publishers write here
wolverine_queues | wolverine_queue_orders <- the Marten-backed host listens here
TransportSchemaName now records whether it was explicitly assigned and is stamped onto the transport only then; MessageStorageSchemaName is stamped only when non-empty. Both currently-working cases are unchanged — an explicitly-set Marten knob still wins, and a host that configures neither still lands on wolverine_queues.
If you have been running Marten alongside UsePostgresqlPersistenceAndTransport with a custom transportSchema, check for a duplicate wolverine_queue_* table under wolverine_queues — that is undelivered mail, and it becomes reachable once you upgrade.
Known related gap
PolecatIntegration.TransportSchemaName is declared and documented but never applied — the mirror-image problem (inert rather than over-eager), so an explicit value there is silently ignored. Its sibling MessageStorageSchemaName is wired correctly. Tracked as #3884, not addressed in this release.
Full Changelog: V6.25.2...V6.25.3
Wolverine 6.25.2
All related to CritterWatch
What's Changed
- GH-3882: opt-in Buffered mode for global partitioned topology slots by @jeremydmiller in 5ef50f8ab
GlobalPartitionedMessageTopology.SetExternalTopology force-set EndpointMode.Durable on every external slot and companion local queue after the user's configure callback ran, with no way to opt out. For lossy, re-reported traffic — telemetry being the motivating case — that store-and-forwards every envelope through the application's own message store.
opts.MessagePartitioning.GlobalPartitioned(topology =>
{
topology.UseShardedRabbitQueues("telemetry", 5);
topology.Mode(EndpointMode.BufferedInMemory); // new — default stays Durable
});The mode applies to the external slots and their companion local queues, and is order-independent (it may be set before or after the transport-specific UseSharded*Queues call). EndpointMode.Inline is rejected — partitioned slots depend on the external-listener-to-companion-queue bridge that inline endpoints bypass.
Full Changelog: V6.25.1...V6.25.2
Wolverine 6.25.1
All related to CritterWatch
What's Changed
- #3877: make agent distribution aware of cross-family node load by @jeremydmiller in #3879
- #3878: pin and document that blanket durability policies skip system endpoints by @jeremydmiller in #3880
Full Changelog: V6.25.0...V6.25.1
Wolverine 6.25.0
Couple bugs, one new API meant for CritterWatch
What's Changed
- #3867 a batched handler participates in partitioned sequential processing by @jeremydmiller in #3868
- #3869: latch endpoint-originated causation reporting by @jeremydmiller in #3872
- GlobalPartitionedMessageTopology.Except() (#3867 follow-up) by @jeremydmiller in #3873
- #3871: apply endpoint policies to broker system endpoints by @jeremydmiller in #3874
- #3870: route the durable inbox to a handler's enrolled DbContext store by @jeremydmiller in #3875
- #3871 follow-up: narrow the system endpoint compile to Rabbit MQ by @jeremydmiller in #3876
Full Changelog: V6.24.10...V6.25.0
Wolverine 6.24.9
This is mostly about CritterWatch uncovered issues with very high volumes of messaging via SQS and making the back pressure detection a bit more sophisticated
What's Changed
- Batching pipelines: back-pressure sees their depth, faulted receivers rebuild, OOM can't fault the block (CritterWatch#942) by @jeremydmiller in #3864
Full Changelog: V6.24.8...V6.24.9
Wolverine 6.24.8
Bug fix release. Four durability and multi-tenancy fixes, all with regression coverage.
Fixes
#3856 — Dormant inbox rows for a durable local queue were never recovered (#3857)
PublishToPartitionedLocalMessaging() marks every slot ListenerScope.Exclusive, and the GH-3590 carve-out then handed inbox recovery to a loop that is never constructed for a local queue — a local queue never gets a ListeningAgent at all. Envelopes sat at status='Incoming', owner_id=0 indefinitely, surviving rolling deploys. Both guards implementing that hand-off now ask a single Endpoint.IsSingleNodeListener predicate, which LocalQueue answers false. Reported by @erdtsieck.
#3815 — forEveryDatabase visited the main database twice (#3858)
MultiTenantedMessageStore.ActiveDatabases() yields Main first, so on any multi-tenanted configuration the Oracle, PostgreSQL and MySQL queues counted the main database twice — GetAttributesAsync() reported a queue depth of 2 for a single row. Schema checks and purges also ran twice. SqlServer and Sqlite were already correct.
#3859 — MySQL multi-tenanted queues shared one physical table (#3861)
A MySQL schema is a database, so the single TransportSchemaName resolved every tenant to the same queue table: no isolation, and counts that multiplied by the tenant count instead of summing. Queue tables now resolve inside each tenant's own database. Single-database hosts are unaffected.
#3860 — MySQL database-per-tenant storage had no isolation (#3862)
The same root cause in the message stores: every tenant store received the one configured schema name, so inbox, outbox, dead letter, node and saga tables were shared across all tenants. Each tenant's database is now its own schema.
Upgrading
MySQL database-per-tenant users only. Before this release your tenant envelope rows all lived in the single configured schema. After upgrading, each tenant reads from its own database instead — drain or copy across any in-flight envelopes still sitting in the old shared tables before you upgrade. No other provider or configuration is affected.
Full changelog: V6.24.7...V6.24.8
Wolverine 6.24.10
Small bug fix release: queue endpoints addressed only by Uri on the database-backed transports (SQL Server, PostgreSQL, SQLite, MySQL) now sanitize the queue name the same way the fluent API does, so a name like sqlserver://my-service-control no longer produces invalid wolverine_queue_* table DDL from the dash. This was uncovered by CritterWatch's systemControlUri usage in the field.
What's Changed
- Database queue Uris sanitize the queue name like the fluent API does by @jeremydmiller in #3865
Full Changelog: V6.24.9...V6.24.10
Wolverine 6.24.7
This is a fix release. Its centre of gravity is agent assignment: a leader that re-decided the same placements every cycle, and — hidden underneath that churn — a serial stop path that made every rebalance far slower than it needed to be.
Agent assignment converges much faster
#3852 — the leader re-decided placements it had already made. The GH-3698 pending-assignment ledger armed on a ReassignAgent but could never apply one: an agent being moved is still listed in its source node's persisted ActiveAgents, so the guard that skips agents with a known original node skipped every reassignment. GH-3698 closed this hole for first-time placement and left it open for moves.
On a 512-database / 5-node / ~8,700-agent cluster that reproduced as 3,468 decisions every cycle against a frozen snapshot, indefinitely — matching the ~45,000 decisions over six minutes reported from production. It converged in spite of itself, because the batched command carries set-based value equality and the dispatcher collapses an identical re-emitted batch while its lane is busy, so it read as benign. The telemetry was not deduplicated at all: AssignmentsChanged fires before batching, so every one of those decisions wrote an AssignmentChanged node record.
The churn was concealing a second defect. StartAgents got bounded parallelism back in GH-3604 — a 50-agent chunk started one at a time was seconds of dead wall-clock that blew the reply window. The stop side is the same shape and never got it: a plain foreach, so at AgentStartBatchSize = 50 an entire chunk's stop cost ran in series before a single start could cascade. It survived only because the per-cycle churn was trickling agents onto the destination alongside the batch. Fixing the churn exposed it.
Measured against the 512-database reproduction:
| 6.24.6 | ledger fix only | 6.24.7 | |
|---|---|---|---|
| work reaching fresh nodes | 38.0s | 74.1s | 14.0s |
| full convergence | 176.3s | 176.3s | 31.1s |
Net 5.7x faster to converge than 6.24.6, not merely quieter.
#3850 — the cached node-number release is now bounded by a high-water mark, so a newcomer's messages cannot be released by a stale cache. Follow-up to GH-3846.
Node-number lookups happen once per node instead of once per database (#3847, thanks @erdtsieck) — a real saving on multi-database deployments, where the old shape scaled with the shard count.
Durability/projection affinity now reports whether it engaged
#3785 shipped in 6.24.5: a shard database's durability agent follows that database's event-subscription agents, so the database attracts one node's connection pool instead of two.
That join is deliberately fail-silent — a miss falls back to the even spread, because a miss is never wrong, only not-better. The problem is diagnostic: a join that never fires because the two descriptor pipelines spell the same database differently looks exactly like the feature working, minus the benefit. Verifying it meant joining pg_stat_activity against the assignment table on a live cluster.
It now says so directly, once, when the numbers change:
Durability/projection database affinity (GH-3785) co-located 446 of 446 durability agents
with their database's event subscription agents across 446 databases
and escalates to a warning in the one unambiguous case — projection agents present, database-bearing durability agents present, zero matched. On a multi-database store that is a spelling divergence, not a coincidence. An application with no projections has nothing to follow and stays quiet.
Transport and listener fixes
#3832 — a deliberately paused listener now reports the distinct ListeningStatus.Paused instead of being indistinguishable from back-pressure TooBusy. The contract now matches what the code actually does.
#3842 — RabbitMqListener.CreateAsync no longer dereferences a null Channel when the agent is disposed mid-startup.
Testing and build
- #3799 — Pulsar tests share one digest-pinned broker per job rather than starting a heavy container per worker process on an unpinned
:latest, which used to hang silently when Docker ran out of memory. - #3800 — the CloudEvents compliance harness carries an exception type name rather than an
Exception, so dead-lettering by exception type can actually be tested;ErrorCausingMessagenever round-tripped through System.Text.Json. - #3839 / #3841 — the solution builds every project, including two shipping packages that previously compiled only during
Pack, and the Polecat incident-service sample (whose tests had not compiled since April, with nothing noticing).
Full changelog: V6.24.6...V6.24.7