Skip to content

Bump WolverineFx.RabbitMQ in /demo - #268

Closed
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/nuget/demo/nuget-abaa0c783c
Closed

dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/nuget/demo/nuget-abaa0c783c

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Oct 5, 2026

Copy link
Copy Markdown
Contributor

Updated WolverineFx from 6.39.1 to 6.45.0.

Release notes

Sourced from WolverineFx's releases.

6.45.0

Wolverine 6.45.0 is mostly a community release. Seven people outside JasperFx shaped it, and the headline feature came in as a contributor's pull request with 17 tests already passing.

Idempotency keys that answer with the first response

6.44 could stop a POST running twice. It could not tell the second caller what the first one created.

[Deduplicated] refuses a repeat with a status code, which is the right answer when the client can go and fetch the resource. It is the wrong answer when the client is the one that lost the connection mid-request and has no idea what id it created. The docs said as much, and said replaying the original response was out of scope.

6.45 adds [DeduplicatedWithResponse], which stores the first response and replays it:

[DeduplicatedWithResponse(DeduplicationScope.User | DeduplicationScope.Endpoint)]
[WolverinePost("/orders")]
public static (OrderCreated, OrderPlaced) Post(PlaceOrder command) { ... }

A repeat under the same Idempotency-Key gets the original status — a 201 stays a 201 — along with the body and the Location header. The handler does not run again, and its cascaded messages are not sent again.

Three details worth knowing before you reach for it:

Scope is required, not optional. A claimed key used to be global across every endpoint, tenant and user. That was tolerable when a duplicate was merely refused; it is not tolerable when a stored response is replayed to whoever presents the key. So the attribute will not let you declare it without saying who a key belongs to, and refuses at startup if you try.

The request is compared by a hash of the bytes sent — method, path, query string and body. A repeat that differs gets 422 rather than being answered with a response that does not belong to it. The corollary: a retry has to resend the same bytes, so a client that re-serializes and reorders JSON properties will see a 422.

It is opt-in and provisions its own table. opts.Durability.EnableDeduplicatedResponses = true, a separate wolverine_deduplicated_responses table, and nothing changes for an application that does not ask for it. PostgreSQL, SQL Server, MySQL and SQLite, which covers Marten and EF Core applications. F# endpoints are supported — [Deduplicated] still does not support them.

Three more deduplication fixes landed alongside it. Two cases where a claim outlived a request that did no work: middleware ending the request early with a success code, and a caller hanging up while the request was failing — in the second, the release itself was being cancelled by the caller's own disconnect, so the key survived for the full window. Deduplication keys also now compare the same way on every engine; they previously followed the database's default collation, so Abc and abc were one claim on SQL Server and MySQL but two on PostgreSQL and SQLite.

Thank you

  • @​uniquelau — [DeduplicatedWithResponse] itself, the two claim-leak fixes, the collation report, the EnableRelayOfUserName diagnosis, and a doc correction. Four pull requests and three issues in one release.
  • @​lahma — found that an inline InvokeAsync retry discarded the failed attempt's outgoing messages, and fixed it.
  • @​thechucklingatom — every application pointed at the same CosmosDB database joined one Wolverine cluster, so a durability agent could recover another application's envelopes into a node with no handler for them. Now each application can keep its own container.
  • @​alesdvorakcz — reported, with Query Store evidence from a failed blue/green deploy, that ReleaseIncomingAsync scanned the whole inbox on SQL Server. Measured at 6,748 logical reads before and 3 after.
  • @​faustodc — a deterministic reproduction of a message-loss window in the SQLite durable queue: the dequeue committed before the inbox row was written, so a crash mid-handler lost the message entirely.
  • @​chrisbbe — a Native AOT crash with a stack trace precise enough to find four more frames with the same defect.
  • @​jeremad — envelopes executed twice when a node started on a PostgreSQL queue backlog.

And from the core team, @​erdtsieck caught the worst regression in 6.44 — every durable-inbox message dead-lettered on partitioned Marten inboxes — and reported it with the exact SQL that QueueSqlCommand was rejecting, which is why it was fixed the same day.

Durability and reliability

  • The partitioned mark-as-handled command was two statements joined by ;, which Marten's QueueSqlCommand rejects — so every durable-inbox message was dead-lettered. (#​4736)
  • A Balanced node is registered before its transports start, so a node starting on a PostgreSQL queue backlog no longer executes envelopes twice. (#​4734)
  • The SQLite durable dequeue now moves the queue row into the incoming-envelope table in the same transaction, closing a window where a message was in neither place while its handler ran. (#​4758)
  • Ownership-release statements can use the owner index on SQL Server. A filtered index cannot serve a parameterized owner_id = @​owner, so these were clustered index scans; a blue/green deploy had been failing on the resulting command timeouts. (#​4739)
  • Two more pieces of per-attempt state survived an inline retry: a flushed context swallowed the retry's messages, and a forwarded message could not be retried at all. (#​4743)
  • A partially-initialized HttpChain could never serve a request again — a missed type lookup overwrote an already-resolved handler type with null. (#​4749)

Native AOT

... (truncated)

6.44.0

This release came overwhelmingly from outside the core team. Six people outside JasperFx reported or fixed
the work in it, including three who sent the pull request themselves, and one report that reconstructed a
production cluster failure in enough detail to find two defects nobody had suspected.

Thank you

@​alesdvorakcz (Aleš Dvořák) Found and fixed the EF Core mark-as-handled identity bug (#​4703 / GH-4701) — a fanned-out message under MessageIdentity.IdAndDestination had every destination's inbox row retired by the first handler to commit, losing the copies whose handlers had not run, with no dead letter and no log. Also reported GH-4704.
@​ayuksekkaya (Ali Yuksekkaya) Two OpenAPI accuracy PRs (#​4722, #​4724) and the ten-variant reproduction behind GH-4712 — declaratively loaded entities that were never saved.
@​thomhurst (Tom Longhurst) Found and fixed GH-4729 (#​4730): Azure Service Bus dead-letter moves passed a null propertiesToModify, so poison messages silently never reached $DeadLetterQueue. Bisected across 27 releases and identified why the existing tests were blind to it.
@​erdtsieck GH-4718 — see below. One of the best bug reports this project has received.
@​net0well GH-4699: TypeLoadMode.Static ignored DisableConventionalDiscovery().
@​alexandrefresnais GH-4700: a scheduled retry ran on a node that does not own the partition slot.

The agent control queue stall (GH-4718)

@​erdtsieck reported 5.4–6.7 MB agent commands being rejected on a sharded multi-tenant cluster — 512
databases, ~2,200 tenants, 37,000–58,000 agents — alongside agent assignment stalling for 30–60 minutes
after every node change, and explicitly marked the causal link between the two as a hypothesis rather than
a finding. It held, and the report's detail is what made both halves findable:

  • Stops were the one agent command never chunked. Starts and reassignments chunk by
    AgentStartBatchSize; stops took a destination's whole set in one envelope. A node shedding 7,592 agents
    emitted a single ~6 MB command that the destination refuses to read at all. This is also why lowering
    AgentStartBatchSize changed nothing — that knob had never touched the stop path.
  • One unreadable control queue row killed a node's entire control queue. The poller and the
    expired-row cleanup share one transaction, so the failed read rolled back the very expiry that would have
    cleared the offending row. The node then re-read it every poll and received no control message at all,
    which is why the leader reported "confirmed 0 of N" for chunk after correctly-sized chunk.
  • Both halves now name themselves in the logs — the row, its message type, its size and the node it was
    addressed to.

Fixes

  • GH-4701 / GH-4705 — mark-as-handled inside the caller's transaction now matches the whole inbox
    identity rather than the id alone, on EF Core (#​4703) and on Marten, Polecat and Fisher (#​4725). Both
    also pick up the partition-aware statement, so a redelivered row can be retired under
    EnableInboxPartitioning.
  • GH-4712 / GH-4717 — an entity loaded through [Entity], [All], [FirstOrDefault], [Queryable]
    or a query plan now enrolls the transaction. Mutating one and relying on the transactional middleware
    previously saved nothing at all, on EF Core with no diagnostic. GH-4717 closes the same gap for every
    other provider.
  • GH-4718 — see above.
  • GH-4729 — Azure Service Bus dead-letter moves no longer pass a null propertiesToModify.
  • GH-4700 — a promoted scheduled retry is settled by partition slot ownership rather than by whether
    the promoting node happens to be able to build a local circuit.
  • GH-4699 — the static handler registry honors DisableConventionalDiscovery().
  • GH-4716 — [Transactional] no longer reports a chain as transactional when no provider claimed it
    and no transaction was applied. HTTP endpoints affected by this stop advertising a 409 they cannot raise.
    ... (truncated)

6.43.0

A dependency release. The store pins had been queued behind two releases because every store's current version takes Weasel 9.36.0 as a floor, and 9.36 carried an Oracle regression. That is fixed upstream now, so all three move together.

from to
Marten (+ AspNetCore, Newtonsoft) 9.35.0 9.41.0
Polecat 5.29.0 5.33.1
Fisher 1.10.0 1.14.0
Weasel (all seven packages) 9.35.1 9.38.0

JasperFx stays at 2.76.1.

⚠️ Read this before upgrading: your next migration may emit DDL it never emitted before

This is why 6.43.0 is a minor rather than a patch. Nothing in Wolverine's own public API changed, but crossing six Weasel releases changes what a migration does:

  • A computed column whose definition changed now drops and recreates the indexes and foreign keys that depend on it (Weasel 9.36). That migration previously failed outright on SQL Server and silently dropped the index on PostgreSQL, so the new behaviour is the correct one — but it is new DDL.
  • Identity is now compared as a column difference (Weasel 9.38). A model that declares an identity column against one that lacks it will finally report drift and emit a MODIFY.
  • SQL Server 2025 users: JSON indexes were read as having no columns, so every apply emitted a DROP INDEX for them. Fixed.
  • Oracle users: descending indexes and co-migrated views never settled, and a failed migration used to strand the global advisory lock. Both fixed.

Oracle: the node tables stop drifting

Two fixes in Wolverine itself, both needed to work with Weasel 9.38.0.

Column defaults are no longer hand-escaped. Wolverine wrote every Oracle timestamp default as SYSTIMESTAMP AT TIME ZONE ''UTC'', with the quotes already doubled, because Weasel used to paste DDL verbatim into an EXECUTE IMMEDIATE '...' literal. Weasel 9.36 escapes literals itself, so both sides escaped and the executed default became AT TIME ZONE ''UTC'' — not a time zone region, and every table create failed with ORA-01882.

Identity columns are declared through AutoIncrement() rather than in the type string. wolverine_nodes.node_number and the node-record table's id were declared as NUMBER(10) GENERATED BY DEFAULT AS IDENTITY — with the clause written into the type. Oracle does not store it there: the catalog reports the type as plain NUMBER(10) and carries identity separately. The model could never equal what came back, so every migration emitted an ALTER … MODIFY that Oracle refuses with ORA-01031.

That type string was a workaround rather than an oversight — AutoIncrement() emitted the identity clause after the null constraint, which is ORA-03076, so it could not create a table at all. Fixed upstream in weasel#​667 along with two related defects: identity was never read back from ALL_TAB_COLUMNS.IDENTITY_COLUMN, and a schema containing an identity column could not be dropped at all, because the teardown tried to drop the system-generated sequence behind it.

Existing Oracle deployments should settle rather than migrate: the column is already NUMBER(10) identity in the database, and 9.38.0 now reads that back, so the model matches it.

Verification

Built pinned the way CI builds it (-c Release -f net9.0), 0 errors and 0 warnings, then 17 database suites locally across PostgreSQL, SQL Server, MySQL, Oracle and SQLite — including Marten, Polecat, Fisher, claim check and leader election on four engines. OracleTests went from 181 failing to 203 passing.

6.42.0

A release driven almost entirely by what users reported. Five of the durability fixes below came from one reporter working a multi-tenant deployment hard enough to find them, and the pattern running through them is the same: a failure that belonged to one tenant was being paid for by every tenant, or was not being reported at all.

⚠️ Read this first: a durable publish that cannot be persisted now fails the caller

GH-4662. PublishAsync/ScheduleAsync made outside a handler and outside any transaction, to a local queue made durable by UseDurableLocalQueues(), used to return without an error when the message store was unreachable. Four attempts inside ~400 ms, one line at Information, and the message was gone — not even recovered after the database came back, because no inbox row was ever written.

The cause was the shape of the retry rather than the retry itself. RetryBlock.PostAsync runs one attempt inline and posts the rest to its own worker, so the task the caller awaits completes the moment the first attempt fails. That is the right shape once an envelope is durable somewhere and recovery can find it; it is the wrong shape for the write that makes it durable, because there is no other copy.

DurableWriteRetry keeps the same 50/100/250 ms budget, awaits it inline, and lets the last exception reach the caller.

Who is affected is narrower than it sounds. If you enlist a real outbox (EF Core, Marten, DatabaseEnvelopeTransaction) nothing changes — that path never reached this code. Inside a handler nothing new escapes either; the publish is buffered and a failure is still caught, logged and recorded as a discard, just at Error with a real tracking event instead of a buried Information line. Only the un-transacted publish from outside a handler can now throw.

One tenant's outage stops costing you every tenant

  • A tenant-only inbox failure no longer pauses the listener for everyone (GH-4658), including the single-store batch path that the first fix missed (GH-4659).
  • A deferred tenant-scoped failure no longer spins. It was being redelivered with no delay, so one tenant's outage turned into a hot loop. There is now a per-tenant write brake.
  • An envelope whose completion could not be written no longer stays owned by the live node until that node stops (GH-4664) — the completion retries until the store returns.
  • PostgreSQL: the envelope tables' timestamp default is now a true UTC instant (GH-4663). It depended on the session's time zone, so InboxStaleTime handed back envelopes that were still running. ⚠️ Changing a column default does not migrate already-deployed tables — see the upgrade notes.
  • Documented what actually happens to a durable listener when one tenant's database is down (GH-4660).

Partitioning

A scheduled message no longer runs on a node that does not own the slot (GH-4673). Under global partitioning a scheduled envelope took the local shortcut regardless of slot ownership, so two nodes could process the same group id concurrently — the exact thing partitioning exists to prevent. A promoted scheduled envelope also no longer still reports its status as Scheduled.

HTTP

Stream large multipart uploads through a MultipartReader endpoint parameter, contributed by @​erdtsieck (#​4674) — no buffering the whole upload to handle it. F# code generation emits the new frame too, and the F# codegen coverage is now gated against a committed baseline so a new unsupported frame cannot slip in unnoticed.

Diagnostics, metrics and operations

  • Dead letters from local queues are counted again. Every local:// destination was being classified as System traffic, so a user's own local queue never reached the per-type/per-tenant DeadLetters metric (GH-4665).
  • Scheduled-message listings carry the envelope's tenant (GH-4666).
  • BrokerResource.Check no longer reports a thrown check as a missing resource (GH-4693), which was hiding the real cause behind "Missing known broker resources".
  • A projection agent that can never start is no longer retried forever (GH-4676), using ShardStartException.Reason.
  • SQLite: SqliteNodePersistence honors the table prefix (GH-4668); node shutdown failed outright on a prefixed store.
  • SQL Server tenant stores start against a named-instance connection string (GH-4685).

Dependencies

  • JasperFx 2.76.1 — carries the named-instance fix above.
  • Weasel stays at 9.35.1. The upgrade to 9.37.0 was prepared and deliberately held back: it regresses Oracle, where a node_number column that is already GENERATED BY DEFAULT AS IDENTITY is read back as drift, so every migration emits an ALTER ... MODIFY that Oracle refuses. It will ship once that is fixed upstream.

Thank you

@​framos-varajo Reported five of the durability issues above — GH-4658, 4659, 4662, 4663 and 4664 — with repros. Most of this release is theirs.
@​erdtsieck (Anne Erdtsieck-Wijnen) Contributed the MultipartReader streaming upload support in #​4674
@​alexandrefresnais Reported the global-partitioning scheduled-message ownership bug, GH-4673
@​frankvdb7 Reported the SQL Server named-instance startup failure, GH-4685
... (truncated)

6.41.0

28 commits since V6.40.0. Minor rather than patch because of the new EF Core and topology features below — and because one change is breaking.

⚠️ Breaking change

AutoApplyTransactions() now fails the chain build when two persistence providers can own it (#​4631). A handler taking both a DbContext and a Marten/Polecat/Fisher IDocumentSession matched two providers, and AutoApplyTransactions applied neither — no transaction support, no warning, no log line, and whatever the un-owned store buffered was discarded when the scope closed. [Transactional] on the same handler took the first ordered provider instead, so the document session was never saved. Both outcomes were silent and the handler reported success.

This stops an application that starts today. One that writes through two stores from a single handler has been losing one of those writes all along; it will now refuse to build until the handler says which store owns the transaction. Add [Transactional(typeof(X))] or [Storage(typeof(X))], or [NonTransactional] to opt out. Two-provider atomicity remains unsupported — the designated store commits, the other does not. Saga chains are exempt.

Entity Framework Core

  • Lightweight mode has a real outbox (#​4630). Cascaded messages from a Lightweight message handler now enlist in the same SaveChangesAsync() as the business write, and PublishDomainEventsFromEntityFrameworkCore() — inert in Lightweight before — runs on every path.
  • Declarative EfCoreOps set-based operations (#​4629), which force the eager transaction they need rather than failing quietly.
  • Scraped domain-event envelopes are flushed before the Eager commit (#​4628).
  • Storage actions and conjoined tenancy mean what they say (#​4611, #​4612, #​4613). Declaring a return type as Update<T> or Store<T> rather than IStorageAction<T> used to silently save nothing.
  • The conjoined tenant filter composes with your own query filter (#​4632) instead of replacing it — EF 9 discarded the second filter, EF 10 throws.
  • Four coverage gaps closed (#​4635), including the first tests for a non-flat EF model.

Multi-tenancy

  • Envelope.TenantId is normalised through TenantIdStyle (#​4640). MessageContext.TenantId always applied the style and Envelope.TenantId never did, so under ForceLowerCase a handler saw "RED" on the envelope beside "red" on the context. Polecat and Fisher read the envelope, so they wrote the raw casing — invisible on SQL Server's case-insensitive collation, and on Fisher's BINARY collation the row was unreachable.
  • A conjoined tenancy battery that runs against Marten, Polecat and Fisher (#​4634) — which is what exposed the above.
  • Fisher's OutboxedSessionFactory resolves the Main store lazily (#​4633).

Transports and topology

  • A partitioned topology can declare its own grouping rules, additively rather than authoritatively (#​4616) — thanks @​erdtsieck.
  • Google Pub/Sub shares one subscription across nodes by default (#​4615), rather than a subscription per node.
  • Kafka replay imitates the endpoint that really listens (#​4620), taking the replay group from a ListenToKafkaTopics group and stamping the topic as Destination — thanks @​lahma.

Durability and correctness

  • EnqueueDirectlyAsync retires the inbox row it forwards (#​4645). A delayed message sent from a handler to a database-backed queue that another node owns was silently destroyed: the forwarding node left an Incoming row behind, and the owning node's anti-duplicate probe then deleted the queue row it had just written. Nothing logged, nothing dead-lettered. Reported with a full four-step reconstruction by @​smoqmilus.
  • EndpointCollection._listeners is no longer a plain Dictionary (#​4646). A failover handing one node several exclusive listener agents at once had up to ten threads writing it while every send read it; a Dictionary corrupted mid-resize is poisoned permanently, and the node never routed again until the pod was replaced. Also reported by @​smoqmilus.
  • Messages no longer inherit a stale ambient Activity. Recurring occurrences (#​4647), inline receivers (#​4649) and the agent plumbing loops (#​4650) were parenting every message under whatever Activity.Current happened to be live when the agent or listener started.
  • A handler chain resolves its pre-generated type by full name (#​4625) — thanks @​skoging.
  • Batching honors a batch handler registered with AddMessageHandler.

Also

  • Upgraded to Weasel 9.35.1, finishing the batched-query registration (#​4624).
  • Fixed a CritterWatch link in the RabbitMQ dead letter queues guide — thanks @​jakobt.

Contributors

Thanks to @​erdtsieck, @​jakobt, @​lahma, @​skoging and @​smoqmilus.

6.40.0

51 commits since V6.39.1. A minor release rather than a patch, because this wave carries new capability and not only fixes.

New capability

  • Capacity-aware agent assignment (#​3959) — agent distribution now accounts for node capacity rather than treating every node as interchangeable. Landed as @​mlh758's #​4297, then hardened in #​4596 and #​4598 to require a real load monitor, stop a shed draining the cluster, and hold pins across every distribution path. Note that the node-load advertisement is PostgreSQL-only until #​4593 lands for the other stores.
  • External table transport for Oracle (#​4482) — implemented by @​Trasvi in #​4558, with follow-ups in #​4574, #​4577 and #​4578. The shared ExternalTableTransportCompliance suite now holds Oracle, PostgreSQL, SQL Server, MySQL and Sqlite to the same bar.
  • A deduplication claim now rides the business transaction on Marten (#​4505), Polecat (#​4570) and Fisher (#​4571). The claim is enlisted in the same unit of work as the handler's own writes instead of being committed on a connection of its own and compensated with a release. A refusal that never commits therefore never claims the id, and there is no release left to mis-order.

Behaviour changes

  • An unknown tenant id answers 404 ProblemDetails instead of 500 (#​4516).
  • Concurrency failures can be mapped to 409 with a one-line opt-in (#​4512).
  • MissingNamedConnectionStringsException stays an InvalidOperationException (#​4527), and every named connection string is now validated in one pass at startup.
  • Wolverine's Kafka error handler composes with the user's instead of replacing it (#​4522).

Exception message wave

#​4511–#​4532 — 18 PRs making the framework's exceptions name a remedy rather than only a symptom: saga failures, handler discovery, SNS/Rabbit MQ/Azure Service Bus/Redis/SignalR configuration problems, oversized messages, unreadable HTTP bodies, and the transports' previously message-less exceptions.

Fixes

  • Stop reading steady-state throughput as a stuck recovery batch, and detect a genuinely stuck outbox from the head of the queue (#​4499).
  • Sweep expired handled envelopes on Cosmos DB, and make DeleteAllHandledAsync work (#​4509).
  • Bound SQLite's two reaps (#​4567).
  • Only ever write ASP.NET Core's endpoint data sources from the composition thread (#​4500).
  • Grandfather a group-affinity candidate per member rather than per partition (#​4562) — @​erdtsieck's #​4563, with blue/green Polecat coverage in #​4576.
  • Declare a module's ancillary store once per namespace (#​4507) — @​uniquelau's #​4508.
  • Apply an ISendMyself published from a projection side effect, on all three stores (#​4556) — built on @​erdtsieck's #​4557.
  • Reject conflicting sending and listening modes on shared listeners (#​4059) — built on @​tmorejon's #​4506.
  • Say "disabled tenant" when that is what happened, and actually refuse one (#​4586).
  • Stop the Redis protocol version header surviving a round trip (#​4595).
  • Do not treat a sticky-bound listen-only endpoint as a local send target (#​4510).
  • Scope Polecat's and Fisher's duplicate-message Discard() to the inbox table (#​4565).

Dependencies

Unchanged from 6.39.1 — Marten 9.35.0, Polecat 5.29.0, Fisher 1.10.0, Weasel 9.32.0, JasperFx 2.74.0. This release ships exactly what its CI has been green against; the pin bump follows separately.

Contributors

Thank you to everyone who contributed code to this release:

Commits viewable in compare view.

Updated WolverineFx.RabbitMQ from 6.39.1 to 6.45.0.

Release notes

Sourced from WolverineFx.RabbitMQ's releases.

6.45.0

Wolverine 6.45.0 is mostly a community release. Seven people outside JasperFx shaped it, and the headline feature came in as a contributor's pull request with 17 tests already passing.

Idempotency keys that answer with the first response

6.44 could stop a POST running twice. It could not tell the second caller what the first one created.

[Deduplicated] refuses a repeat with a status code, which is the right answer when the client can go and fetch the resource. It is the wrong answer when the client is the one that lost the connection mid-request and has no idea what id it created. The docs said as much, and said replaying the original response was out of scope.

6.45 adds [DeduplicatedWithResponse], which stores the first response and replays it:

[DeduplicatedWithResponse(DeduplicationScope.User | DeduplicationScope.Endpoint)]
[WolverinePost("/orders")]
public static (OrderCreated, OrderPlaced) Post(PlaceOrder command) { ... }

A repeat under the same Idempotency-Key gets the original status — a 201 stays a 201 — along with the body and the Location header. The handler does not run again, and its cascaded messages are not sent again.

Three details worth knowing before you reach for it:

Scope is required, not optional. A claimed key used to be global across every endpoint, tenant and user. That was tolerable when a duplicate was merely refused; it is not tolerable when a stored response is replayed to whoever presents the key. So the attribute will not let you declare it without saying who a key belongs to, and refuses at startup if you try.

The request is compared by a hash of the bytes sent — method, path, query string and body. A repeat that differs gets 422 rather than being answered with a response that does not belong to it. The corollary: a retry has to resend the same bytes, so a client that re-serializes and reorders JSON properties will see a 422.

It is opt-in and provisions its own table. opts.Durability.EnableDeduplicatedResponses = true, a separate wolverine_deduplicated_responses table, and nothing changes for an application that does not ask for it. PostgreSQL, SQL Server, MySQL and SQLite, which covers Marten and EF Core applications. F# endpoints are supported — [Deduplicated] still does not support them.

Three more deduplication fixes landed alongside it. Two cases where a claim outlived a request that did no work: middleware ending the request early with a success code, and a caller hanging up while the request was failing — in the second, the release itself was being cancelled by the caller's own disconnect, so the key survived for the full window. Deduplication keys also now compare the same way on every engine; they previously followed the database's default collation, so Abc and abc were one claim on SQL Server and MySQL but two on PostgreSQL and SQLite.

Thank you

  • @​uniquelau — [DeduplicatedWithResponse] itself, the two claim-leak fixes, the collation report, the EnableRelayOfUserName diagnosis, and a doc correction. Four pull requests and three issues in one release.
  • @​lahma — found that an inline InvokeAsync retry discarded the failed attempt's outgoing messages, and fixed it.
  • @​thechucklingatom — every application pointed at the same CosmosDB database joined one Wolverine cluster, so a durability agent could recover another application's envelopes into a node with no handler for them. Now each application can keep its own container.
  • @​alesdvorakcz — reported, with Query Store evidence from a failed blue/green deploy, that ReleaseIncomingAsync scanned the whole inbox on SQL Server. Measured at 6,748 logical reads before and 3 after.
  • @​faustodc — a deterministic reproduction of a message-loss window in the SQLite durable queue: the dequeue committed before the inbox row was written, so a crash mid-handler lost the message entirely.
  • @​chrisbbe — a Native AOT crash with a stack trace precise enough to find four more frames with the same defect.
  • @​jeremad — envelopes executed twice when a node started on a PostgreSQL queue backlog.

And from the core team, @​erdtsieck caught the worst regression in 6.44 — every durable-inbox message dead-lettered on partitioned Marten inboxes — and reported it with the exact SQL that QueueSqlCommand was rejecting, which is why it was fixed the same day.

Durability and reliability

  • The partitioned mark-as-handled command was two statements joined by ;, which Marten's QueueSqlCommand rejects — so every durable-inbox message was dead-lettered. (#​4736)
  • A Balanced node is registered before its transports start, so a node starting on a PostgreSQL queue backlog no longer executes envelopes twice. (#​4734)
  • The SQLite durable dequeue now moves the queue row into the incoming-envelope table in the same transaction, closing a window where a message was in neither place while its handler ran. (#​4758)
  • Ownership-release statements can use the owner index on SQL Server. A filtered index cannot serve a parameterized owner_id = @​owner, so these were clustered index scans; a blue/green deploy had been failing on the resulting command timeouts. (#​4739)
  • Two more pieces of per-attempt state survived an inline retry: a flushed context swallowed the retry's messages, and a forwarded message could not be retried at all. (#​4743)
  • A partially-initialized HttpChain could never serve a request again — a missed type lookup overwrote an already-resolved handler type with null. (#​4749)

Native AOT

... (truncated)

6.44.0

This release came overwhelmingly from outside the core team. Six people outside JasperFx reported or fixed
the work in it, including three who sent the pull request themselves, and one report that reconstructed a
production cluster failure in enough detail to find two defects nobody had suspected.

Thank you

@​alesdvorakcz (Aleš Dvořák) Found and fixed the EF Core mark-as-handled identity bug (#​4703 / GH-4701) — a fanned-out message under MessageIdentity.IdAndDestination had every destination's inbox row retired by the first handler to commit, losing the copies whose handlers had not run, with no dead letter and no log. Also reported GH-4704.
@​ayuksekkaya (Ali Yuksekkaya) Two OpenAPI accuracy PRs (#​4722, #​4724) and the ten-variant reproduction behind GH-4712 — declaratively loaded entities that were never saved.
@​thomhurst (Tom Longhurst) Found and fixed GH-4729 (#​4730): Azure Service Bus dead-letter moves passed a null propertiesToModify, so poison messages silently never reached $DeadLetterQueue. Bisected across 27 releases and identified why the existing tests were blind to it.
@​erdtsieck GH-4718 — see below. One of the best bug reports this project has received.
@​net0well GH-4699: TypeLoadMode.Static ignored DisableConventionalDiscovery().
@​alexandrefresnais GH-4700: a scheduled retry ran on a node that does not own the partition slot.

The agent control queue stall (GH-4718)

@​erdtsieck reported 5.4–6.7 MB agent commands being rejected on a sharded multi-tenant cluster — 512
databases, ~2,200 tenants, 37,000–58,000 agents — alongside agent assignment stalling for 30–60 minutes
after every node change, and explicitly marked the causal link between the two as a hypothesis rather than
a finding. It held, and the report's detail is what made both halves findable:

  • Stops were the one agent command never chunked. Starts and reassignments chunk by
    AgentStartBatchSize; stops took a destination's whole set in one envelope. A node shedding 7,592 agents
    emitted a single ~6 MB command that the destination refuses to read at all. This is also why lowering
    AgentStartBatchSize changed nothing — that knob had never touched the stop path.
  • One unreadable control queue row killed a node's entire control queue. The poller and the
    expired-row cleanup share one transaction, so the failed read rolled back the very expiry that would have
    cleared the offending row. The node then re-read it every poll and received no control message at all,
    which is why the leader reported "confirmed 0 of N" for chunk after correctly-sized chunk.
  • Both halves now name themselves in the logs — the row, its message type, its size and the node it was
    addressed to.

Fixes

  • GH-4701 / GH-4705 — mark-as-handled inside the caller's transaction now matches the whole inbox
    identity rather than the id alone, on EF Core (#​4703) and on Marten, Polecat and Fisher (#​4725). Both
    also pick up the partition-aware statement, so a redelivered row can be retired under
    EnableInboxPartitioning.
  • GH-4712 / GH-4717 — an entity loaded through [Entity], [All], [FirstOrDefault], [Queryable]
    or a query plan now enrolls the transaction. Mutating one and relying on the transactional middleware
    previously saved nothing at all, on EF Core with no diagnostic. GH-4717 closes the same gap for every
    other provider.
  • GH-4718 — see above.
  • GH-4729 — Azure Service Bus dead-letter moves no longer pass a null propertiesToModify.
  • GH-4700 — a promoted scheduled retry is settled by partition slot ownership rather than by whether
    the promoting node happens to be able to build a local circuit.
  • GH-4699 — the static handler registry honors DisableConventionalDiscovery().
  • GH-4716 — [Transactional] no longer reports a chain as transactional when no provider claimed it
    and no transaction was applied. HTTP endpoints affected by this stop advertising a 409 they cannot raise.
    ... (truncated)

6.43.0

A dependency release. The store pins had been queued behind two releases because every store's current version takes Weasel 9.36.0 as a floor, and 9.36 carried an Oracle regression. That is fixed upstream now, so all three move together.

from to
Marten (+ AspNetCore, Newtonsoft) 9.35.0 9.41.0
Polecat 5.29.0 5.33.1
Fisher 1.10.0 1.14.0
Weasel (all seven packages) 9.35.1 9.38.0

JasperFx stays at 2.76.1.

⚠️ Read this before upgrading: your next migration may emit DDL it never emitted before

This is why 6.43.0 is a minor rather than a patch. Nothing in Wolverine's own public API changed, but crossing six Weasel releases changes what a migration does:

  • A computed column whose definition changed now drops and recreates the indexes and foreign keys that depend on it (Weasel 9.36). That migration previously failed outright on SQL Server and silently dropped the index on PostgreSQL, so the new behaviour is the correct one — but it is new DDL.
  • Identity is now compared as a column difference (Weasel 9.38). A model that declares an identity column against one that lacks it will finally report drift and emit a MODIFY.
  • SQL Server 2025 users: JSON indexes were read as having no columns, so every apply emitted a DROP INDEX for them. Fixed.
  • Oracle users: descending indexes and co-migrated views never settled, and a failed migration used to strand the global advisory lock. Both fixed.

Oracle: the node tables stop drifting

Two fixes in Wolverine itself, both needed to work with Weasel 9.38.0.

Column defaults are no longer hand-escaped. Wolverine wrote every Oracle timestamp default as SYSTIMESTAMP AT TIME ZONE ''UTC'', with the quotes already doubled, because Weasel used to paste DDL verbatim into an EXECUTE IMMEDIATE '...' literal. Weasel 9.36 escapes literals itself, so both sides escaped and the executed default became AT TIME ZONE ''UTC'' — not a time zone region, and every table create failed with ORA-01882.

Identity columns are declared through AutoIncrement() rather than in the type string. wolverine_nodes.node_number and the node-record table's id were declared as NUMBER(10) GENERATED BY DEFAULT AS IDENTITY — with the clause written into the type. Oracle does not store it there: the catalog reports the type as plain NUMBER(10) and carries identity separately. The model could never equal what came back, so every migration emitted an ALTER … MODIFY that Oracle refuses with ORA-01031.

That type string was a workaround rather than an oversight — AutoIncrement() emitted the identity clause after the null constraint, which is ORA-03076, so it could not create a table at all. Fixed upstream in weasel#​667 along with two related defects: identity was never read back from ALL_TAB_COLUMNS.IDENTITY_COLUMN, and a schema containing an identity column could not be dropped at all, because the teardown tried to drop the system-generated sequence behind it.

Existing Oracle deployments should settle rather than migrate: the column is already NUMBER(10) identity in the database, and 9.38.0 now reads that back, so the model matches it.

Verification

Built pinned the way CI builds it (-c Release -f net9.0), 0 errors and 0 warnings, then 17 database suites locally across PostgreSQL, SQL Server, MySQL, Oracle and SQLite — including Marten, Polecat, Fisher, claim check and leader election on four engines. OracleTests went from 181 failing to 203 passing.

6.42.0

A release driven almost entirely by what users reported. Five of the durability fixes below came from one reporter working a multi-tenant deployment hard enough to find them, and the pattern running through them is the same: a failure that belonged to one tenant was being paid for by every tenant, or was not being reported at all.

⚠️ Read this first: a durable publish that cannot be persisted now fails the caller

GH-4662. PublishAsync/ScheduleAsync made outside a handler and outside any transaction, to a local queue made durable by UseDurableLocalQueues(), used to return without an error when the message store was unreachable. Four attempts inside ~400 ms, one line at Information, and the message was gone — not even recovered after the database came back, because no inbox row was ever written.

The cause was the shape of the retry rather than the retry itself. RetryBlock.PostAsync runs one attempt inline and posts the rest to its own worker, so the task the caller awaits completes the moment the first attempt fails. That is the right shape once an envelope is durable somewhere and recovery can find it; it is the wrong shape for the write that makes it durable, because there is no other copy.

DurableWriteRetry keeps the same 50/100/250 ms budget, awaits it inline, and lets the last exception reach the caller.

Who is affected is narrower than it sounds. If you enlist a real outbox (EF Core, Marten, DatabaseEnvelopeTransaction) nothing changes — that path never reached this code. Inside a handler nothing new escapes either; the publish is buffered and a failure is still caught, logged and recorded as a discard, just at Error with a real tracking event instead of a buried Information line. Only the un-transacted publish from outside a handler can now throw.

One tenant's outage stops costing you every tenant

  • A tenant-only inbox failure no longer pauses the listener for everyone (GH-4658), including the single-store batch path that the first fix missed (GH-4659).
  • A deferred tenant-scoped failure no longer spins. It was being redelivered with no delay, so one tenant's outage turned into a hot loop. There is now a per-tenant write brake.
  • An envelope whose completion could not be written no longer stays owned by the live node until that node stops (GH-4664) — the completion retries until the store returns.
  • PostgreSQL: the envelope tables' timestamp default is now a true UTC instant (GH-4663). It depended on the session's time zone, so InboxStaleTime handed back envelopes that were still running. ⚠️ Changing a column default does not migrate already-deployed tables — see the upgrade notes.
  • Documented what actually happens to a durable listener when one tenant's database is down (GH-4660).

Partitioning

A scheduled message no longer runs on a node that does not own the slot (GH-4673). Under global partitioning a scheduled envelope took the local shortcut regardless of slot ownership, so two nodes could process the same group id concurrently — the exact thing partitioning exists to prevent. A promoted scheduled envelope also no longer still reports its status as Scheduled.

HTTP

Stream large multipart uploads through a MultipartReader endpoint parameter, contributed by @​erdtsieck (#​4674) — no buffering the whole upload to handle it. F# code generation emits the new frame too, and the F# codegen coverage is now gated against a committed baseline so a new unsupported frame cannot slip in unnoticed.

Diagnostics, metrics and operations

  • Dead letters from local queues are counted again. Every local:// destination was being classified as System traffic, so a user's own local queue never reached the per-type/per-tenant DeadLetters metric (GH-4665).
  • Scheduled-message listings carry the envelope's tenant (GH-4666).
  • BrokerResource.Check no longer reports a thrown check as a missing resource (GH-4693), which was hiding the real cause behind "Missing known broker resources".
  • A projection agent that can never start is no longer retried forever (GH-4676), using ShardStartException.Reason.
  • SQLite: SqliteNodePersistence honors the table prefix (GH-4668); node shutdown failed outright on a prefixed store.
  • SQL Server tenant stores start against a named-instance connection string (GH-4685).

Dependencies

  • JasperFx 2.76.1 — carries the named-instance fix above.
  • Weasel stays at 9.35.1. The upgrade to 9.37.0 was prepared and deliberately held back: it regresses Oracle, where a node_number column that is already GENERATED BY DEFAULT AS IDENTITY is read back as drift, so every migration emits an ALTER ... MODIFY that Oracle refuses. It will ship once that is fixed upstream.

Thank you

@​framos-varajo Reported five of the durability issues above — GH-4658, 4659, 4662, 4663 and 4664 — with repros. Most of this release is theirs.
@​erdtsieck (Anne Erdtsieck-Wijnen) Contributed the MultipartReader streaming upload support in #​4674
@​alexandrefresnais Reported the global-partitioning scheduled-message ownership bug, GH-4673
@​frankvdb7 Reported the SQL Server named-instance startup failure, GH-4685
... (truncated)

6.41.0

28 commits since V6.40.0. Minor rather than patch because of the new EF Core and topology features below — and because one change is breaking.

⚠️ Breaking change

AutoApplyTransactions() now fails the chain build when two persistence providers can own it (#​4631). A handler taking both a DbContext and a Marten/Polecat/Fisher IDocumentSession matched two providers, and AutoApplyTransactions applied neither — no transaction support, no warning, no log line, and whatever the un-owned store buffered was discarded when the scope closed. [Transactional] on the same handler took the first ordered provider instead, so the document session was never saved. Both outcomes were silent and the handler reported success.

This stops an application that starts today. One that writes through two stores from a single handler has been losing one of those writes all along; it will now refuse to build until the handler says which store owns the transaction. Add [Transactional(typeof(X))] or [Storage(typeof(X))], or [NonTransactional] to opt out. Two-provider atomicity remains unsupported — the designated store commits, the other does not. Saga chains are exempt.

Entity Framework Core

  • Lightweight mode has a real outbox (#​4630). Cascaded messages from a Lightweight message handler now enlist in the same SaveChangesAsync() as the business write, and PublishDomainEventsFromEntityFrameworkCore() — inert in Lightweight before — runs on every path.
  • Declarative EfCoreOps set-based operations (#​4629), which force the eager transaction they need rather than failing quietly.
  • Scraped domain-event envelopes are flushed before the Eager commit (#​4628).
  • Storage actions and conjoined tenancy mean what they say (#​4611, #​4612, #​4613). Declaring a return type as Update<T> or Store<T> rather than IStorageAction<T> used to silently save nothing.
  • The conjoined tenant filter composes with your own query filter (#​4632) instead of replacing it — EF 9 discarded the second filter, EF 10 throws.
  • Four coverage gaps closed (#​4635), including the first tests for a non-flat EF model.

Multi-tenancy

  • Envelope.TenantId is normalised through TenantIdStyle (#​4640). MessageContext.TenantId always applied the style and Envelope.TenantId never did, so under ForceLowerCase a handler saw "RED" on the envelope beside "red" on the context. Polecat and Fisher read the envelope, so they wrote the raw casing — invisible on SQL Server's case-insensitive collation, and on Fisher's BINARY collation the row was unreachable.
  • A conjoined tenancy battery that runs against Marten, Polecat and Fisher (#​4634) — which is what exposed the above.
  • Fisher's OutboxedSessionFactory resolves the Main store lazily (#​4633).

Transports and topology

  • A partitioned topology can declare its own grouping rules, additively rather than authoritatively (#​4616) — thanks @​erdtsieck.
  • Google Pub/Sub shares one subscription across nodes by default (#​4615), rather than a subscription per node.
  • Kafka replay imitates the endpoint that really listens (#​4620), taking the replay group from a ListenToKafkaTopics group and stamping the topic as Destination — thanks @​lahma.

Durability and correctness

  • EnqueueDirectlyAsync retires the inbox row it forwards (#​4645). A delayed message sent from a handler to a database-backed queue that another node owns was silently destroyed: the forwarding node left an Incoming row behind, and the owning node's anti-duplicate probe then deleted the queue row it had just written. Nothing logged, nothing dead-lettered. Reported with a full four-step reconstruction by @​smoqmilus.
  • EndpointCollection._listeners is no longer a plain Dictionary (#​4646). A failover handing one node several exclusive listener agents at once had up to ten threads writing it while every send read it; a Dictionary corrupted mid-resize is poisoned permanently, and the node never routed again until the pod was replaced. Also reported by @​smoqmilus.
  • Messages no longer inherit a stale ambient Activity. Recurring occurrences (#​4647), inline receivers (#​4649) and the agent plumbing loops (#​4650) were parenting every message under whatever Activity.Current happened to be live when the agent or listener started.
  • A handler chain resolves its pre-generated type by full name (#​4625) — thanks @​skoging.
  • Batching honors a batch handler registered with AddMessageHandler.

Also

  • Upgraded to Weasel 9.35.1, finishing the batched-query registration (#​4624).
  • Fixed a CritterWatch link in the RabbitMQ dead letter queues guide — thanks @​jakobt.

Contributors

Thanks to @​erdtsieck, @​jakobt, @​lahma, @​skoging and @​smoqmilus.

6.40.0

51 commits since V6.39.1. A minor release rather than a patch, because this wave carries new capability and not only fixes.

New capability

  • Capacity-aware agent assignment (#​3959) — agent distribution now accounts for node capacity rather than treating every node as interchangeable. Landed as @​mlh758's #​4297, then hardened in #​4596 and #​4598 to require a real load monitor, stop a shed draining the cluster, and hold pins across every distribution path. Note that the node-load advertisement is PostgreSQL-only until #​4593 lands for the other stores.
  • External table transport for Oracle (#​4482) — implemented by @​Trasvi in #​4558, with follow-ups in #​4574, #​4577 and #​4578. The shared ExternalTableTransportCompliance suite now holds Oracle, PostgreSQL, SQL Server, MySQL and Sqlite to the same bar.
  • A deduplication claim now rides the business transaction on Marten (#​4505), Polecat (#​4570) and Fisher (#​4571). The claim is enlisted in the same unit of work as the handler's own writes instead of being committed on a connection of its own and compensated with a release. A refusal that never commits therefore never claims the id, and there is no release left to mis-order.

Behaviour changes

  • An unknown tenant id answers 404 ProblemDetails instead of 500 (#​4516).
  • Concurrency failures can be mapped to 409 with a one-line opt-in (#​4512).
  • MissingNamedConnectionStringsException stays an InvalidOperationException (#​4527), and every named connection string is now validated in one pass at startup.
  • Wolverine's Kafka error handler composes with the user's instead of replacing it (#​4522).

Exception message wave

#​4511–#​4532 — 18 PRs making the framework's exceptions name a remedy rather than only a symptom: saga failures, handler discovery, SNS/Rabbit MQ/Azure Service Bus/Redis/SignalR configuration problems, oversized messages, unreadable HTTP bodies, and the transports' previously message-less exceptions.

Fixes

  • Stop reading steady-state throughput as a stuck recovery batch, and detect a genuinely stuck outbox from the head of the queue (#​4499).
  • Sweep expired handled envelopes on Cosmos DB, and make DeleteAllHandledAsync work (#​4509).
  • Bound SQLite's two reaps (#​4567).
  • Only ever write ASP.NET Core's endpoint data sources from the composition thread (#​4500).
  • Grandfather a group-affinity candidate per member rather than per partition (#​4562) — @​erdtsieck's #​4563, with blue/green Polecat coverage in #​4576.
  • Declare a module's ancillary store once per namespace (#​4507) — @​uniquelau's #​4508.
  • Apply an ISendMyself published from a projection side effect, on all three stores (#​4556) — built on @​erdtsieck's #​4557.
  • Reject conflicting sending and listening modes on shared listeners (#​4059) — built on @​tmorejon's #​4506.
  • Say "disabled tenant" when that is what happened, and actually refuse one (#​4586).
  • Stop the Redis protocol version header surviving a round trip (#​4595).
  • Do not treat a sticky-bound listen-only endpoint as a local send target (#​4510).
  • Scope Polecat's and Fisher's duplicate-message Discard() to the inbox table (#​4565).

Dependencies

Unchanged from 6.39.1 — Marten 9.35.0, Polecat 5.29.0, Fisher 1.10.0, Weasel 9.32.0, JasperFx 2.74.0. This release ships exactly what its CI has been green against; the pin bump follows separately.

Contributors

Thank you to everyone who contributed code to this release:

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
  • `@depe...

Description has been truncated

Bumps WolverineFx from 6.39.1 to 6.45.0
Bumps WolverineFx.RabbitMQ from 6.39.1 to 6.45.0

---
updated-dependencies:
- dependency-name: WolverineFx.RabbitMQ
  dependency-version: 6.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget
- dependency-name: WolverineFx
  dependency-version: 6.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget
...

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 Oct 5, 2026
@dependabot dependabot Bot mentioned this pull request Oct 5, 2026
@dependabot @github

dependabot Bot commented on behalf of github Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #274.

@dependabot dependabot Bot closed this Oct 12, 2026
@dependabot
dependabot Bot deleted the dependabot/nuget/demo/nuget-abaa0c783c branch October 12, 2026 04:12
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