Skip to content

Bump Marten and WolverineFx.Marten - #54

Open
dependabot[bot] wants to merge 1 commit into
devfrom
dependabot/nuget/code/K9Crush-scaffold/K9Crush/multi-ea9b235fcc
Open

Bump Marten and WolverineFx.Marten#54
dependabot[bot] wants to merge 1 commit into
devfrom
dependabot/nuget/code/K9Crush-scaffold/K9Crush/multi-ea9b235fcc

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 11, 2026

Copy link
Copy Markdown
Contributor

Updated Marten from 9.20.1 to 9.22.6.

Release notes

Sourced from Marten's releases.

9.22.6

A fix-and-adoption release: a masking-rule fix, a broadened event-store compliance net, a CI overhaul, and the JasperFx 2.46.0 / Weasel 9.24.0 dependency adoptions.

Fixes

Every matching masking rule runs, not just the first (#​5199)

ApplyEventDataMasking used to stop at the first masking rule whose event type matched, so an event enrolled in two rules (say, one masking a name and another masking an address) only ever had the first applied. All matching rules now compose: each rule runs in registration order against the output of the previous one.

Test coverage

Compliance waves 6-8 + strong-typed identity (#​5198, #​5201, #​5203, #​5205)

Marten's event-store behavior is now enrolled in the shared JasperFx.Events.ComplianceTests suite for: stream compacting and event data masking (wave 6), projection rebuild/catch-up and dead letters (wave 7), conjoined event tenancy and subscriptions (wave 8), plus StrongTypedIdentityCompliance (#​5144). These pin Marten's behavior to the same contract Polecat and future stores are held to.

CI

One job per test project, supervised (#​5096, #​5208)

The monolithic CI test run is split into one job per test project running under Bobcat's supervisor, so a flaky suite no longer poisons the whole gate and failures name the project that produced them. Also removes stray ITestOutputHelper/debug logger injections from tests (#​5211).

Dependencies

  • JasperFx / JasperFx.Events 2.45.0 -> 2.46.0 — includes the high-water-detection memory fix for very high tenant counts (jasperfx#​644, the 2,000+ tenant OOM).
  • Weasel 9.23.2 -> 9.24.0 — parallel db-apply/db-assert across physical databases (weasel#​431/#​442), per-fingerprint schema stamp keying (weasel#​439), SQL Server CREATE DATABASE postcondition check (weasel#​415), discovery progress reporting (weasel#​432).

9.22.5

Two source-generator and test-harness fixes that both surfaced on projections built through AddProjectionWithServices, plus the JasperFx 2.42.2 adoption they ride on.

Fixes

The source generator no longer breaks a projection that takes dependencies (#​5192)

The bundled JasperFx.Events.SourceGenerator registers an EventProjection's discovered published document types (#​4166) by writing into your partial class. It used to emit a parameterless constructor to do it, which failed two ways for exactly the projections that need dependencies injected.

It broke the build outright against a primary constructor. C# requires every other constructor to chain through the primary one, so this failed with CS8862 inside the generated <T>.TypeRegistration.g.cs:

public partial class MyProjection(ILogger<MyProjection> logger) : EventProjection
{
    public override ValueTask ApplyAsync(IDocumentOperations operations, IEvent e, CancellationToken cancellation)
    {
        operations.Store(new Thing());
        return new ValueTask();
    }
}

And where it did compile, it silently did nothing. A projection registered through AddProjectionWithServices is built by the container, which calls the dependency-taking constructor — so the generated parameterless one never ran and the published types went unregistered. That also left the projection's teardown targets unregistered, so a rebuild did not wipe its documents.

Registration now rides an override of ProjectionBase.PublishedTypes(), which does not care how the instance was constructed.

Affects 9.22.3 and 9.22.4. Earlier versions discovered published types syntactically, so only an explicit ops.Store<Doc>(x) produced a registration and the far more common ops.Store(x) produced none — which meant the constructor was rarely emitted at all.

One behavior change to be aware of: the generator used to skip registration entirely when your class already had an explicit parameterless constructor, a guard that existed only because you cannot add a second one. An override has no such conflict, so those projections now get their published types registered too. That is the intended #​4166 behavior, but on upgrade it can newly provision document storage — and newly register teardown targets — for a projection that was quietly getting neither. If a projection writes into storage that must not be truncated on rebuild, set DeletePublishedTypesOnTeardown = false.

EventProjectionScenario no longer spends its wall clock asleep (#​5195, in part)

Almost none of a scenario's time was work. The harness wipes the event store and then starts the daemon, so the high-water agent's first look saw an empty store, read CaughtUp, and settled into SlowPollingTime — one second by default. Every append then raced a sleeping agent, and because the agent returns to CaughtUp after each batch drains, the cost recurred at every batch boundary. Since a boundary is how a scenario says "these appends must land in different daemon batches", the more precisely a test described its batching, the slower it got.

A scenario owns both the appends and the daemon that must notice them, so it now says so directly, through an in-process IDaemonWakeup — a semaphore release, no database round trip and no LISTEN/NOTIFY. Nothing about your store's polling configuration changes.

batch boundaries before after
1 ~1290ms ~300ms
3 ~3357ms ~815ms

A flat ~250ms per boundary remains, from a hard-coded poll delay in WaitForNonStaleDataAsync. That is the other half of #​5195 and is still open.

Dependencies

JasperFx / JasperFx.Events 2.42.2. Adopting it also enrolls Marten in the strong-typed identity event-sourcing compliance suite that landed in 2.42.0 (IComplianceStoreRegistrar.RegisterValueType<T>()), taking the shared cross-store suite to 167 passing tests against Marten.

9.22.4

What's Changed

Full Changelog: JasperFx/marten@V9.22.3...V9.22.4

9.22.3

What's Changed

Full Changelog: JasperFx/marten@V9.22.1...V9.22.3

9.22.1

Security release. Upgrade is recommended for anyone using sharded tenancy together with Events.UseTenantPartitionedEvents.

A tenant id was interpolated into a double-quoted PostgreSQL identifier without doubling an embedded double quote, so a tenant id containing one could terminate the identifier and execute additional SQL statements. This is a different class from the two advisories previously published on this repository, both of which were the single-quoted string-literal class; neither of those fixes addressed this.

You are affected only if you use sharded tenancy, have UseTenantPartitionedEvents enabled, and your application passes attacker-influenced input as a tenant id. Note that the reachable surface includes ordinary session resolution, not just administrative provisioning calls — GetTenantAsync / FindOrCreateDatabase auto-provision an unknown tenant. Applications using tenant ids from a trusted fixed set are not exploitable.

Affected versions: 9.4.0 through 9.22.0.

Full details, including remediation guidance for existing data, are in the security advisory: GHSA-3vp4-34pf-2rcw

What changed

  • PerTenantEventSequences.QuotedSequenceName escapes embedded quotes, matching quote_ident/%I so the name still resolves to the same object the quick-append function finds. Covers the create, drop, schema-apply and cleanup paths.
  • BulkEventAppender no longer builds an unquoted sequence name from a suffix read back out of the tenants table. This also fixes a functional bug: PreserveSourceSequence bulk imports previously failed with 42601 for hyphenated and GUID tenant ids under sharded tenancy.
  • ShardedTenancy validates tenant ids destined for DDL, closing a long-standing asymmetry with the DefaultTenancy provisioning path. It is a narrow denylist rather than the existing identifier allowlist, so hyphenated and GUID tenant ids keep working.

Dependency

Requires Weasel.Postgresql 9.21.1, which escapes partition bound values (JasperFx/weasel#​416). Both halves are needed; the dependency is pulled in automatically.

Credit to Barak Srour (Apiiro) for the report.

9.22.0

The partitioning feature is new, but otherwise this was all about CritterWatch improvements for a huge installation

What's Changed

Full Changelog: JasperFx/marten@V9.21.0...V9.22.0

9.21.0

Highlights

A small, low-risk release: two bug fixes reported against 9.20.x, a LINQ ordering fix, a Newtonsoft serialization fix, and a new health-check overload for Wolverine-managed daemon distribution.

[!NOTE]
There is a change to the mt_quick_append_events PostgreSQL function in this release, and applying it is NOT mandatory or required.

You do not need to patch your database, schedule a migration, or coordinate a deployment window to take 9.21.0. The client-side half of the #​5062 fix ships in the assembly, so upgrading the NuGet package alone is sufficient — 9.21.0 is correct against the function version you already have deployed.

Under the default AutoCreate.CreateOrUpdate the function is simply refreshed the next time Marten ensures event storage exists (a CREATE OR REPLACE FUNCTION, no lock on your event data). If you run AutoCreate.None with db-patch / db-apply, your next patch will contain one extra CREATE OR REPLACE FUNCTION … mt_quick_append_events statement — apply it whenever it suits your normal cadence. See the migration guide for details.

Bug Fixes

mt_quick_append_events returned {NULL} for an empty event array (#​5062, #​5088)

array_length('{}', 1) is NULL in PostgreSQL rather than 0, so calling the bulk append function with no events returned a bigint[] whose single element was NULL. Npgsql could not read that into long[], and the resulting InvalidCastException was thrown from the batch's post-processing loop — where it displaced whatever exception had actually made the append fail. Callers were left with an unrelated, non-retryable error instead of the real one; for the reporter that dead-lettered Wolverine messages which would otherwise have been retried.

Fixed on three fronts:

  • The function now COALESCEs the array length, so an empty append means what it says: zero events appended, final version unchanged.
  • The append operation no longer reads the returned array when the batch carries no events — this is what makes the fix effective without any database change.
  • The one code path in Marten that could reach the function with empty arrays (ProjectionUpdateBatch.WaitForCompletion, for an Append side effect that ended up with no events) no longer issues the call.

OrderBy against a dictionary indexer dropped the key (#​5063, #​5073)

OrderBy(x => x.SomeDictionary["key"]) generated SQL that ignored the indexer key, so the ordering was wrong (or arbitrary) rather than failing loudly.

Lazy LINQ sequences serialized as objects under Newtonsoft (#​5076, #​5080)

A document property holding a deferred-execution sequence (Select(...), Where(...) without a materializing call) was written by Newtonsoft as an iterator object rather than a JSON array, so it would not round-trip. These are now written as plain arrays.

IMessageBatch is called concurrently (#​5065, #​5085)

Not a behavior change, but a documentation fix worth flagging if you implement IMessageBatch yourself: the async daemon raises projection side effects from multiple threads at once (measured at up to 8 concurrent publishers across 10 threads for a single-stream projection catching up). The interface previously said nothing about this. An implementation that appends to an unsynchronized collection will silently drop messages — the same hazard, in a real outbox, that showed up here as a "flaky" test.

New

Provider-aware databaseFilter for the high-water health check (#​5061, #​5089)

AddMartenHighWaterHealthCheck's databaseFilter is captured at registration time, so it cannot resolve services — which makes it unable to express "the databases this node currently owns" when ownership is runtime state. That is precisely the case under Wolverine-managed daemon distribution, where agents are assigned per (database, tenant) and rebalanced over a node's lifetime.

There is now an overload whose filter receives the IServiceProvider and is re-evaluated on every probe:

Services.AddHealthChecks().AddMartenHighWaterHealthCheck(
    (services, database) => services.GetRequiredService<IWolverineRuntime>()
        .Agents.AllLocallyOwnedDatabaseIds()
        .Any(id => id.Name.EqualsIgnoreCase(database.Identifier)),
    staleThreshold: TimeSpan.FromSeconds(30),
    includeExternallyManaged: true);
 ... (truncated)

## 9.20.2

## What's Changed
* Fix NgramIndex to match NgramSearch's unaccent-aware mt_grams_vector expression by @​dat-honguyen in https://github.com/JasperFx/marten/pull/5060

## New Contributors
* @​dat-honguyen made their first contribution in https://github.com/JasperFx/marten/pull/5060

**Full Changelog**: https://github.com/JasperFx/marten/compare/V9.20.1...V9.20.2

Commits viewable in [compare view](https://github.com/JasperFx/marten/compare/V9.20.1...V9.22.6).
</details>

Updated [WolverineFx.Marten](http://github.com/jasperfx/wolverine) from 6.23.1 to 6.25.5.

<details>
<summary>Release notes</summary>

_Sourced from [WolverineFx.Marten's releases](http://github.com/jasperfx/wolverine/releases)._

## 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.

 ... (truncated)

## 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 https://github.com/JasperFx/wolverine/commit/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**: https://github.com/JasperFx/wolverine/compare/V6.25.2...V6.25.3


## 6.25.2

All related to CritterWatch

## What's Changed
* GH-3882: opt-in Buffered mode for global partitioned topology slots by @​jeremydmiller in https://github.com/JasperFx/wolverine/commit/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.

```csharp
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: JasperFx/wolverine@V6.25.1...V6.25.2

6.25.1

All related to CritterWatch

What's Changed

Full Changelog: JasperFx/wolverine@V6.25.0...V6.25.1

6.25.0

Couple bugs, one new API meant for CritterWatch

What's Changed

Full Changelog: JasperFx/wolverine@V6.24.10...V6.25.0

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

Full Changelog: JasperFx/wolverine@V6.24.9...V6.24.10

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

Full Changelog: JasperFx/wolverine@V6.24.8...V6.24.9

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.

#​3815forEveryDatabase 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: JasperFx/wolverine@V6.24.7...V6.24.8

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.

#​3842RabbitMqListener.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; ErrorCausingMessage never 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).

... (truncated)

6.24.6

A bug-fix release. The headline is a message ordering regression affecting every transport built on BatchedSender — if you rely on FIFO ordering anywhere, this release matters to you.

Highlights

Message ordering restored in BatchedSender (#​3825). BatchedSender ran its serializing stage at Environment.ProcessorCount, so envelopes reached the batching block in serialization-completion order rather than enqueue order.

This was a silent regression from the switch off TPL Dataflow. ActionBlock defaults MaxDegreeOfParallelism to 1 — ordered by default — and the Channels rewrite raised it without the ordering guarantee being restated anywhere. The block was ordered for years, then quietly wasn't. The practical effect: FIFO ordering was not honored under Azure Service Bus sessions, SQS FIFO message groups, or global partitioning, on every transport that uses BatchedSender. Nothing was lost; messages arrived out of order. Fixed by returning the stage to a degree of parallelism of 1 — everything downstream was already serial.

A second, independent defect fell out of the same investigation: TrackedSession.AllRecordsInOrder() sorted by SessionTime, which is ElapsedMilliseconds — whole milliseconds. An entire receive batch ties, and the stable sort then fell back to enumerating a Guid-keyed cache with no relation to real order. Every ordering assertion in the test suite was at the mercy of this. Records now carry a monotonic sequence number.

Back-pressure now works on the right number, and says what it is (#​3831, jasperfx#​632). A latched listener logged exactly one too busy line and then nothing — forever. An operator watching a queue grow for 40 minutes could not distinguish "still draining" from "wedged". Underneath that, the count a PartitionProcessingByGroupId endpoint latched and resumed against was wrong: the downstream block holding the backlog was invisible to it, so Count reported zero for work that was really there.

  • BackPressureAgent logs a periodic warning while a listener stays latched, carrying the queue count and the restart threshold the resume decision is made from.
  • The timer-driven check is exception-safe. A throw during an attempted resume was an unobserved ValueTask fault, and the listener silently never resumed.
  • BufferedReceiver/DurableReceiver wire the receiving block's OnError to ILogger. A terminally-faulted block freezes the queue count and permanently latches the listener; that now logs at Critical instead of vanishing to stderr.

A tenanted Azure Service Bus endpoint could not send at all (#​3826) — tenanted or untenanted. TenantedSender deliberately does not implement ISenderRequiresCallback, but callback registration did not recurse, so a BatchedSender underneath it kept a null callback and threw InvalidOperationException: This sender has not been registered. on every batch. The tenanted path now uses inline senders, matching how Redis, MQTT, and Pub/Sub already worked around this.

Oracle queue identity round-trip (#​3820). System.Uri lowercases the authority component while Oracle uppercases its queue identifiers, so ToOracleQueue() resolved a second endpoint over the same physical tables. Also fixes a dead final-attempt error handler: a when clause that included the loop counter made the descriptive exception at the bottom of the retry loop unreachable.

Behavior change worth reading

TrackedSession now completes only when all conditions are satisfied, not the first (#​3824). This is a public testing API. A tracked session configured with several expectations previously returned as soon as any one of them was met, which means some existing tests were passing vacuously. After upgrading, such a test waits for every condition — and may now fail where it previously passed. That failure is generally revealing a real gap rather than introducing one.

Other changes

  • JasperFx upgraded to 2.39.5. Beyond the block Count fix above, this carries jasperfx#​600/#​601 — the application-assembly stack walk could adopt a test-runner assembly and then scan an assembly holding none of your types — and jasperfx#​599, where DatabaseId's escaping now survives a System.Uri round trip.
  • EventSubscriptionAgentFamily.DatabaseKeyOf and TenantNeutralKeyOf are now public (#​3819).

Testing and CI

No runtime behavior changes here, but this is why the fixes above became findable. The Category=Flaky exclusion list went from 12 tagged classes to zero (#​3763) — and several of those tags turned out to have been added in the very commit that introduced the feature they test, hiding working code rather than broken code. Every CI readiness gate now fails loudly instead of warning and continuing; the Kafka gate in particular was a no-op that passed in 0.0s against a broker that would not serve metadata for another 3 seconds (#​3814). The retry ledger records why a test flaked rather than only which one (#​3787), and CIAzureServiceBus was sharded three ways on measured per-class durations (#​3790).

What's Changed

6.24.5

Note: 6.24.4 shipped on NuGet without a GitHub release, so these notes cover everything since V6.24.3.

Highlights

Multi-database projection & subscription assignment got a major reliability pass. For sharded event stores, Wolverine assigns the agents for projections and subscriptions in groups by database — so connection pools scale with the number of databases rather than nodes × databases:

  • A blue/green rollout carrying a projection version bump no longer assigns the new version's agents to nodes that cannot build them — previously the new version could never start anywhere for the whole rollout (#​3792, thanks @​erdtsieck). A split database now costs exactly one owner per version.
  • Database affinity is now a property of the database across agent families (#​3785): a shard database's durability agent follows that database's projection agents onto the same node, so the database attracts one node's connection pool instead of two. Measured on a 512-database production cluster, 73% of databases were split across two nodes, wasting ~425 connection slots. Expect a one-time wave of durability-agent reassignments on first deploy as an existing cluster converges.
  • The settled assignment state is now pinned as a fixed point — re-evaluating a converged cluster moves nothing — and a new deterministic simulation drives the real leader evaluation through the exact deploy shape of GH-3753: slow agent starts and a blue/green capability split at once.

Durable outbox to SNS/SQS FIFO destinations is fixed (#​3793): EnvelopeSerializer never round-tripped Envelope.DeduplicationId, so any envelope recovered from durable storage after an outage was re-sent without MessageDeduplicationId and rejected deterministically by a FIFO destination without content-based deduplication — retrying forever or dead-lettering. Also fixed alongside it: the circuit-resume ping could never reach a FIFO destination (a latched sender could never unlatch), and SNS sent MessageDeduplicationId to standard topics, which AWS rejects. The same fix is merged to the 5.x maintenance branch and will ship in the next 5.40.x release for .NET 8 users.

Balanced-mode host shutdown no longer hangs (#​3781): stopping a node while agent commands were queued could pay a full agent-batch reply window per queued command — measured at 17+ minutes. Now ~2 minutes on the same reproduction.

Azure Service Bus conventional routing sanitizes entity names (#​3786): a handler for an array message type (e.g. Handle(Foo[])) produced an illegal ASB entity name that broke broker startup for the whole assembly, and the real reason was lost. Names are sanitized and failures now carry the offending name.

What's Changed

Full Changelog: JasperFx/wolverine@V6.24.3...V6.24.5

6.24.3

What's Changed

New Contributors

Full Changelog: JasperFx/wolverine@V6.24.2...V6.24.3

6.24.2

What's Changed

Full Changelog: JasperFx/wolverine@V6.24.1...V6.24.2

6.24.1

Patch release. Four reported issues, all with reproductions from production clusters.

Fixes

#​3701wolverine_node_records grows without bound (#​3734)

A reporting cluster reached 36,135,221 rows / 16 GB in five days on a diagnostic table nothing on the hot path reads. Three distinct defects:

  • INodeAgentPersistence.DeleteOldNodeRecordsAsync was implemented for every relational store and never invoked outside tests.
  • The pruning that did run bounds the table by age only (NodeEventRecordExpirationTime, 5 days), which is no ceiling at all at high write rates — every one of those 36M rows was inside the window.
  • That age sweep's hourly throttle was dead. Its backing field was never assigned (the CS0649 suppression on it said so), so a full-table delete went out on every recovery cycle — every 5 seconds by default.

New Durability.NodeRecordRetention (default 10,000 rows) and Durability.NodeRecordPruningPeriod (default hourly). MultiTenantedMessageStore now delegates the trim to the main store instead of inheriting a no-op default, and Sqlite, MySQL and Oracle gained implementations they had also been missing.

#​3697 — no supported force-catch-up under Wolverine-managed event subscription distribution (#​3735)

Wolverine already implemented the coordinator-driven catch-up path, but only exposed it as a TrackActivity() stage. Adds the standalone entry point on IHost and IServiceProvider, plus <T> ancillary-store variants:

await host.PauseThenCatchUpOnMartenDaemonActivityAsync();
await host.PauseThenCatchUpOnMartenDaemonActivityAsync(CatchUpMode.AndDoNothing);
await host.PauseThenCatchUpOnMartenDaemonActivityAsync<IMyStore>();

It never calls IProjectionDaemon.CatchUpAsync — doing so under a live coordinator is what produces the ProgressionProgressOutOfOrderException and pk_mt_event_progression duplicate-key errors suites have been retrying around. Resuming the agents that already own the shards means there is only ever one writer.

#​3733 — a comma in an agent Uri voided a whole batch confirmation (#​3736)

AgentsStarted, StartAgents, AgentsStopped and StopAgents joined their Uri[] on a comma, which RFC 3986 permits unescaped in a path segment. Agent URIs embed tenant ids and projection names, so one comma shattered an agent into fragments — and because the read side built the array in a single projection, the resulting throw took out the confirmation for the entire batch. Newline is the delimiter now, and entries are parsed individually so a bad one names itself.

The comma remains the default on the wire for payloads that do not contain one, so rolling upgrades keep working in both directions.

#​3706 — RabbitMQ acks were cumulative (#​3737)

Every ack went out as BasicAckAsync(tag, multiple: true), acknowledging every lower delivery tag on the channel. That is only correct when completions happen in delivery order, and they do not with ConsumerDispatchConcurrency > 1 — acking one message silently acknowledged deliveries whose handlers were still running, and a crash at that moment lost them.

Acks are now per message. Two dead-letter paths that relied on the cumulative sweep settle themselves, most importantly the un-mappable-message branch in WorkerQueueMessageConsumer, which dead-lettered and returned without touching the delivery at all. This unblocks the planned native-ack parallel endpoint mode.

Also included

  • #​3730 — compliance coverage for a pause and node loss landing on in-flight assignments (GH-3698)
  • #​3732 — seed the departed node's inbox rows as already owned (GH-3729)

6.24.0

Two data-loss fixes — but for unusual usages

This release closes two bugs that silently destroyed data rather than failing loudly. Both are worth reading before you skip the rest of these notes.

Durable inbox rows were orphaned when a circuit breaker tripped (#​3680). DurableReceiver checked its latched flag before calling MarkReceived. The latched path still persists each envelope to the inbox as a safety net — but on an envelope that never went through MarkReceived, Status is the enum default (Outgoing) and Destination is null. Both are filter columns for inbox recovery, so the rows were written in a state no recovery sweep on any node could ever see. The null Listener also skipped the nack back to the broker, and the broker's redelivery after restart hit DuplicateIncomingEnvelopeException — which acks and drops. Net result: genuine message loss under a durable inbox any time a circuit breaker trip latched the receiver mid-flight. Measured on the circuit-breaker suite, 9 of 1,200 messages were lost per run.

Dropping one tenant from a shared partition bucket destroyed its co-tenants' data (#​3686). Found alongside #​3683. Tenant bucketing — registering several small tenants against one partition suffix so they share a physical partition — is documented and exposed through PartitionPerTenant(p => p.AllowPartitionSharing = true), and it did not work on either engine. It had no test coverage, because the doc sample demonstrating it is compile-only and never executed.

Global partitioning

Part of the GlobalPartitioning epic (#​3482).

  • Global partitioning topologies for PostgreSQL and SQL Server queues (#​3468, #​3469)
  • End-to-end sharded-processing suites for Azure Service Bus, GCP Pub/Sub, NATS, Redis Streams and Pulsar (#​3467). The scenario is lifted into Wolverine.ComplianceTests.Partitioning.ShardedProcessing, so a new transport costs one small test class
  • Native-mode design comparison and per-transport native alternatives documented (#​3481)

The new suites immediately found two real bugs:

  • NATS global partitioning had never worked at all. The topology forces EndpointMode.Durable on every slot, and a NatsEndpoint only supports Durable when JetStream-backed — so every UseShardedNatsSubjects() call threw at configuration time. The topology now enables JetStream on its own endpoints and declares a work-queue stream per shard, without which the listener died at startup on stream not found
  • Pulsar named its companion local queues off the full topic path, producing queues like global-persistent://public/default/orders1. They now use the topic's short name, matching every other transport

Multi-tenancy and persistence

  • EF Core tenant partition back-fill (#​3496). Routine migration deltas deliberately leave Weasel-managed partitions alone, so a table joining an existing managed set — a newly deployed service, or a newly mapped ITenanted entity — had no partition for any tenant registered before that table existed. IConjoinedTenantPartitions<T>.MigrateTenantPartitionsAsync() reconciles every partitioned table against the full registered tenant set, with per-table TenantPartitionResult reporting
  • Conjoined tenant partition bucketing actually works now, on both PostgreSQL and SQL Server (#​3683, and see #​3686 above)
  • Exclusive listener inbox recovery is now covered for RavenDb (#​3595) and CosmosDb (#​3596)

Transports

  • RabbitMQ: deliveries are settled against the channel they arrived on (#​3687). Acking a delivery on a torn-down channel threw a NullReferenceException
  • NATS: auto-provisioned JetStream durable consumers are filtered to their own subject (#​3676). FilterSubject was only assigned when ConsumerName was empty, so every durable consumer on a stream received every message. The fix needs a FilterSubjects multi-filter — a single filter cannot cover both {subject} and {subject}.scheduled, and a work-queue stream discards an uncovered control message
  • MQTT: the v5 authentication method name is configurable (#​3588). It was hardcoded to "OAUTH2-JWT". Azure Event Grid's custom JWT authentication requires CUSTOM-JWT, so those brokers could not be reached through Wolverine's authentication support at all. You could already set the method by hand through MqttClientOptionsBuilder.WithAuthentication(), but that gave up Wolverine's token refresh loop — the whole reason to use MqttJwtAuthenticationOptions. You no longer have to choose
  • The HTTP transport can send to a destination nobody pre-registered (#​3681, reported as ProductSupport#​34). WolverineHttpTransportClient used the endpoint's OutboundUri purely as an IHttpClientFactory client name, then posted to that client's BaseAddress — so operator commands sent back over the HTTP transport failed with An invalid request URI was provided

Performance

  • RabbitMQ consumer dispatch concurrency is now per-endpoint (#​3492). The client default of 1 was the bottleneck. Simulated handler, 2,000 msg/s offered load, 30s measured window:

    ConsumerDispatchConcurrency Throughput Transit p50
    1 (client default) 163.7/s — (nothing from the measured window was consumed before the run ended)
    5 828/s 22,871.9 ms
    20 1,999.1/s 1.486 ms (p95 2.54, p99 3.22)

    The 5.1x and 12.2x multiples understate it — at 1 and 5 the listener never catches up at all.

  • Amazon SQS batches message deletions and chunks outgoing batches on the 256KB request size limit (#​3493)

  • Azure Service Bus session listeners are no longer quadratic — the n² session loops are now n. MaxConcurrentCalls is surfaced, and a batched defer settles the original message (#​3494)

HTTP and gRPC

... (truncated)

Commits viewable in compare view.

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot cr...

Description has been truncated

Bumps Marten from 9.20.1 to 9.22.6
Bumps WolverineFx.Marten from 6.23.1 to 6.25.5

---
updated-dependencies:
- dependency-name: Marten
  dependency-version: 9.22.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: WolverineFx.Marten
  dependency-version: 6.25.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added .NET Pull requests that update .NET code dependencies Pull requests that update a dependency file labels Aug 11, 2026
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednuget/​marten@​9.20.1 ⏵ 9.22.69910090100100
Updatednuget/​wolverinefx.marten@​6.23.1 ⏵ 6.25.510010090100100

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file .NET Pull requests that update .NET code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants