Repository navigation
Bump WolverineFx and 2 others - #1507
Merged
Merged
Conversation
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bumps WolverineFx from 6.14.0 to 6.48.3 Bumps WolverineFx.Postgresql from 6.14.0 to 6.48.3 Bumps WolverineFx.RuntimeCompilation from 6.14.0 to 6.48.3 --- updated-dependencies: - dependency-name: WolverineFx dependency-version: 6.48.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine - dependency-name: WolverineFx.Postgresql dependency-version: 6.48.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine - dependency-name: WolverineFx.RuntimeCompilation dependency-version: 6.48.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine ... Signed-off-by: dependabot[bot] <support@github.com>
dependabot
Bot
force-pushed
the
dependabot/nuget/dev/wolverine-403b5d5b5f
branch
from
October 10, 2026 19:35
d19b2f8 to
c745451
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pinned WolverineFx at 6.48.3.
Release notes
Sourced from WolverineFx's releases.
6.48.3
A patch release for blue/green warm-ups at fleet scale, from a second production report by @erdtsieck on 6.48.2: every green-only agent went to the first green node to register, and the two green nodes that joined seconds later got nothing for half an hour.
WOLVERINE_FLEET_SCALE=production.IQuerySessionbut sends messages now gets its commit (#4910, @srimalw; #4912 for Polecat and Fisher). TheIQuerySessionpermutation of #2941: the read-only parameter was served by casting the outbox-enrolled session, butCanApplyignored it, so a scheduled cascade was never handled and a durable local message only ever ran from memory. Reading alone still does not make a chain transactional; reading plus sending does — an injectedIMessageBus, a returnedOutgoingMessages, or a cascading return value.Everything else is as in 6.48.2.
6.48.2
A patch release for agent assignment at fleet scale, from a production report by @erdtsieck: ~62,000 per-tenant event-subscription agents on 5 to 8 nodes, where a newly elected leader spent 66 minutes in its first assignment evaluation during a blue/green warm-up and three leaders in a row were replaced before one finished.
AssignmentGrid.MatchAgentsToCapableNodesForwas quadratic in the agent count whenever any capability differed between nodes — which a single bumped projection version is enough to trigger.batchCommandsremoving each start command from the list one at a time, the heartbeatFill()-ing every running agent into the node record, per-placementFill()on the grid's node, and per-placement recounts in both even distribution paths. Measured through the leader's real evaluation at the reported shape: the even blue/green path's first evaluation went from 76 s to under 5 s.DurabilitySettings.LeaderTakeoverHoldEvaluations(default 1, 0 disables) withholds bare first-time placements for that many evaluations after election, so the predecessor's starts can surface as assignment rows and be kept where they are. It stands down when no other node holds any assignment, so a cold start pays nothing.WOLVERINE_CHAOS_SOAK=<rounds>) that stays out of CI.Everything else is as in 6.48.1.
6.48.1
A patch release for one global partitioning bug found by @alexandrefresnais right after 6.48.0.
Everything else is as in 6.48.0.
6.48.0
A fast follow-up to 6.47.0 with five fixes, three of them for how a Wolverine cluster behaves when nodes come and go.
Clustering and agent distribution
StartAgents,StopAgents,QueryAgentPresence) were logged as enqueued, never reached the broker, and timed out, so agents stopped on a scaled-in node were never re-placed. Routes now resolve their sending agent on every use. Reported from production on Azure Container Apps with Azure Service Bus control queues; the workaround there was a very longSendingAgentIdleTimeout.TooBusy, so the back pressure sweep later restarted the slot on a node that no longer owned it. Reported with a complete reproduction by @alexandrefresnais.ApplyRestrictionsAsyncwaits for the released or pinned agent to appear in the persisted node assignments and throwsAgentRestrictionsNotConfirmedExceptionnaming it, instead of returning as if the agent had started. NewDurabilitySettings.AgentRestrictionConfirmationTimeout(default 10 seconds) bounds the wait. This is what lets CritterWatch stop acknowledging a restart that started nothing.RabbitMQ
Deduplication
[Deduplicated]takes its own claim window (#4857, #4858, by @uniquelau).[Deduplicated(WindowInSeconds = 600)]gives one handler a different claim lifetime from the host-wideDeduplicationWindow, matching what[DeduplicatedWithResponse]already offered for HTTP. Works across the RDBMS, Marten, Polecat and Fisher deduplicators; pre-generated code for handlers without a window is unchanged.Thanks to @alexandrefresnais, @tannerwatson and @uniquelau for the reports, reproductions and pull requests.
6.47.0
Wolverine 6.47.0 is a release that largely came from outside the core team. The biggest change in it, a hardening pass over the NATS JetStream transport, was reported, designed and written by a community member, and so was the fix for a global partitioning handoff that stranded scheduled messages. Thank you to everyone who sent code, reproductions, or diagnoses this time around.
Community contributions
NATS JetStream
Sending durably over JetStream, with an outbox in front of a stream, lost messages in several places without an error or a log line. All of them are closed (#4856, with follow-ups in #4859):
DiscardPolicy.New, a failedNats-Expected-*check, an oversized message) with aPubAckcarrying an error, not an exception. Wolverine read only the sequence, reported success, and the durable outbox deleted a message the stream never stored. It throws now, the outbox keeps the message, and a duplicateNats-Msg-Idis still a success.MaxDeliveris used up. A buffered listener acknowledges on receipt, so under the old guard the message was simply gone. The dead letter copy carries its ownNats-Msg-Id, because the original's id had it discarded as a duplicate when the dead letter subject sat in the same stream.AsWorkQueue()has always meant interest retention, which drops a message published while no consumer is bound, which is exactly the startup and rebalancing window a sharded topology has. The streamsUseShardedNatsSubjects()declares now keep a message until it is acknowledged.AsWorkQueue()itself is unchanged; its docs now say what it does.ConfigureNatsOpts()gives you the last word over the NATS.Net client options, for the pending channel size, ping interval and anything else Wolverine does not surface. It applies to the shared and every tenant connection.AckWaitorMaxDeliverin code just works.Provisioning(NatsProvisioning.CreateOnly | CreateOrUpdate | Verify)controls what startup does with declared streams and named consumers that already exist, withVerifyfailing the start and listing every deviation. Resource setup and the listener now write the same consumer settings, filter included, so aVerifyhost starts cleanly after resource setup.InvokeAsync()fails at once when nobody is listening. A request now carries a per-request reply subject, so the server's "no responders" answer reaches the waiting call and it fails in milliseconds withWolverineRequestReplyExceptioninstead of waiting out its timeout. The responding side answers on that exact subject, which a NATS user limited toallow_responsesrequires.Global partitioning and durability
An exploratory pass over global partitioning after the long chain of slot handoff fixes, looking for the next "runs on a node that does not own the slot" before it was reported (#4853). It found two:
And around it:
Envelope.Attempts; a database or broker queue saw attempts go 1 then 3 (#4855).Native AOT
The 6.46.0 AOT work stopped at the first failure each publish cycle, because the smoke target did. That is fixed, and the lanes it now runs found the rest:
SideEffectPolicycould not findExecuteonIStartStream, the HTTPIEndpointMetadataProviderclose was trimmed, and Marten could not mapEnvelopein a native image. All three are rooted or avoided now.InvokeAsync<Guid>request/reply shape, killed a native image at startup because the routing warm-up closed a generic over it reflectively. The routers take the message type as an argument now, so the one reflective close every message type had to be rooted for is gone, along with everything that existed to prop it up.Also in this release
JasperFx.Events.InMemoryprototyping store, so a store-agnostic application's handlers run before anyone has chosen Marten, Polecat or Fisher. Only Wolverine's store-agnostic usage is supported (#4844).6.46.0
Wolverine 6.46.0 is the release where Native AOT stopped being aspirational. Ten of the changes below came out of one sustained hunt, and the pattern that emerged is worth stating plainly: under Native AOT, the fix is almost never to root a generic harder — it is to stop closing one.
Thank you to the people outside the core team who shipped code in this one. Five of the fixes here came from the community, and several of them are in corners the maintainers would not have found on their own.
Community contributions
[FromClaim]code generation was broken for URI claim types (#4783), and EF Core had three distinct problems in one report:[Entity]on a step method, query plans, and multi-tenantDbContextresolution (#4769).User-scope warning (#4779), plus a documentation fix for something that bites every newcomer: the first-use handler compile counts against a tracked session's timeout (#4786).[Deduplicated]claims are kept in the main store when you run a database per tenant (#4813). Every[Deduplicated]chain used to throw on its first keyed message in that configuration.Native AOT
Sagas work under Native AOT now (#4809). That took three separate blockers, and each one taught us a rule that is now written down:
[DynamicDependency]preserves metadata, not code. An AOT root over a value-type instantiation is silently ineffective — no warning, no diagnostic, just a missing type at startup. The roots that appear to work survive through reference-type canonical sharing, which is why this went unnoticed for so long.Acting on those: a frame can now contribute its own AOT root (#4801), the EF Core frames closed over your
DbContextare rooted (#4803), and the Marten, Polecat and FisherFetchLatestframes were de-genericized rather than rooted (#4793, #4806). The HTTP and response-aware chain paths got the closed generics they need (#4790), and an[UnconditionalSuppressMessage]scope that ILC rejects outright was fixed (#4791).Two things worth calling out for anyone running AOT in production:
Guidtenant id, or a[DeduplicationIdentity]on aGuid, which needs no configuration at all. Both crashed the host at startup in a native image. Neither was reachable by any existing rooting mechanism, so both now avoid closing the generic at all.Durability and clustered agents
A wave of fixes to inbox ownership and global partition slots, all of them cases where a message could stall rather than fail:
TenantedSenderdeliberately cannot forward anISenderCallback— doing so broke fire-and-forget senders in a previous attempt. There is now aCallbackAwareTenantedSenderfor the senders that settle the outbox themselves, with the original left exactly as it was. Every broker-per-tenant transport can use it; only MQTT does so far.Security and dependencies
System.Formats.Asn17.0.0 is lifted, and the whole package graph is audited (#4781, GH-4773).JasperFx.Eventsinterface breaks a store at runtime rather than at compile time, so leaving a store package behind is the actual hazard.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
POSTrunning 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:A repeat under the same
Idempotency-Keygets the original status — a 201 stays a 201 — along with the body and theLocationheader. The handler does not run again, and its cascaded messages are not sent again.Three details worth knowing before you reach for it:
Scopeis 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
422rather 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 separatewolverine_deduplicated_responsestable, 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
Abcandabcwere one claim on SQL Server and MySQL but two on PostgreSQL and SQLite.Thank you
[DeduplicatedWithResponse]itself, the two claim-leak fixes, the collation report, theEnableRelayOfUserNamediagnosis, and a doc correction. Four pull requests and three issues in one release.InvokeAsyncretry discarded the failed attempt's outgoing messages, and fixed it.ReleaseIncomingAsyncscanned the whole inbox on SQL Server. Measured at 6,748 logical reads before and 3 after.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
QueueSqlCommandwas rejecting, which is why it was fixed the same day.Durability and reliability
;, which Marten'sQueueSqlCommandrejects — so every durable-inbox message was dead-lettered. (#4736)owner_id = @owner, so these were clustered index scans; a blue/green deploy had been failing on the resulting command timeouts. (#4739)HttpChaincould 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
MessageIdentity.IdAndDestinationhad 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.propertiesToModify, so poison messages silently never reached$DeadLetterQueue. Bisected across 27 releases and identified why the existing tests were blind to it.TypeLoadMode.StaticignoredDisableConventionalDiscovery().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:
AgentStartBatchSize; stops took a destination's whole set in one envelope. A node shedding 7,592 agentsemitted a single ~6 MB command that the destination refuses to read at all. This is also why lowering
AgentStartBatchSizechanged nothing — that knob had never touched the stop path.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.
addressed to.
Fixes
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.[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.
propertiesToModify.the promoting node happens to be able to build a local circuit.
DisableConventionalDiscovery().[Transactional]no longer reports a chain as transactional when no provider claimed itand 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.
JasperFx stays at 2.76.1.
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:
MODIFY.DROP INDEXfor them. 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 anEXECUTE IMMEDIATE '...'literal. Weasel 9.36 escapes literals itself, so both sides escaped and the executed default becameAT TIME ZONE ''UTC''— not a time zone region, and every table create failed withORA-01882.Identity columns are declared through
AutoIncrement()rather than in the type string.wolverine_nodes.node_numberand the node-record table'sidwere declared asNUMBER(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 plainNUMBER(10)and carries identity separately. The model could never equal what came back, so every migration emitted anALTER … MODIFYthat Oracle refuses withORA-01031.That type string was a workaround rather than an oversight —
AutoIncrement()emitted the identity clause after the null constraint, which isORA-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 fromALL_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.OracleTestswent 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.
GH-4662.
PublishAsync/ScheduleAsyncmade outside a handler and outside any transaction, to a local queue made durable byUseDurableLocalQueues(), used to return without an error when the message store was unreachable. Four attempts inside ~400 ms, one line atInformation, 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.PostAsyncruns 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.DurableWriteRetrykeeps 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 atErrorwith a real tracking event instead of a buriedInformationline. Only the un-transacted publish from outside a handler can now throw.One tenant's outage stops costing you every tenant
timestampdefault is now a true UTC instant (GH-4663). It depended on the session's time zone, soInboxStaleTimehanded back envelopes that were still running.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
MultipartReaderendpoint 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
local://destination was being classified as System traffic, so a user's own local queue never reached the per-type/per-tenantDeadLettersmetric (GH-4665).BrokerResource.Checkno longer reports a thrown check as a missing resource (GH-4693), which was hiding the real cause behind "Missing known broker resources".ShardStartException.Reason.SqliteNodePersistencehonors the table prefix (GH-4668); node shutdown failed outright on a prefixed store.Dependencies
node_numbercolumn that is alreadyGENERATED BY DEFAULT AS IDENTITYis read back as drift, so every migration emits anALTER ... MODIFYthat Oracle refuses. It will ship once that is fixed upstream.Thank you
MultipartReaderstreaming upload support in #46746.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.
AutoApplyTransactions()now fails the chain build when two persistence providers can own it (#4631). A handler taking both aDbContextand a Marten/Polecat/FisherIDocumentSessionmatched two providers, andAutoApplyTransactionsapplied 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
Lightweightmessage handler now enlist in the sameSaveChangesAsync()as the business write, andPublishDomainEventsFromEntityFrameworkCore()— inert inLightweightbefore — runs on every path.EfCoreOpsset-based operations (#4629), which force the eager transaction they need rather than failing quietly.Update<T>orStore<T>rather thanIStorageAction<T>used to silently save nothing.Multi-tenancy
Envelope.TenantIdis normalised throughTenantIdStyle(#4640).MessageContext.TenantIdalways applied the style andEnvelope.TenantIdnever did, so underForceLowerCasea 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.OutboxedSessionFactoryresolves theMainstore lazily (#4633).Transports and topology
ListenToKafkaTopicsgroup and stamping the topic asDestination— thanks @lahma.Durability and correctness
EnqueueDirectlyAsyncretires 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 anIncomingrow 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._listenersis no longer a plainDictionary(#4646). A failover handing one node several exclusive listener agents at once had up to ten threads writing it while every send read it; aDictionarycorrupted mid-resize is poisoned permanently, and the node never routed again until the pod was replaced. Also reported by @smoqmilus.Activity. Recurring occurrences (#4647), inline receivers (#4649) and the agent plumbing loops (#4650) were parenting every message under whateverActivity.Currenthappened to be live when the agent or listener started.AddMessageHandler.Also
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
ExternalTableTransportCompliancesuite now holds Oracle, PostgreSQL, SQL Server, MySQL and Sqlite to the same bar.Behaviour changes
404 ProblemDetailsinstead of500(#4516).409with a one-line opt-in (#4512).MissingNamedConnectionStringsExceptionstays anInvalidOperationException(#4527), and every named connection string is now validated in one pass at startup.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
DeleteAllHandledAsyncwork (#4509).ISendMyselfpublished from a projection side effect, on all three stores (#4556) — built on @erdtsieck's #4557.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:
ISendMyselfprojection side effect fix, #45576.39.1
A bug fix release. If you use
codegen testas a CI gate, run dead letter queues on any broker, or persist with Oracle, there's something in here for you.codegen testworks againSince 6.37.0,
dotnet run -- codegen testhas failed on a clean checkout for any application that has message handlers, with oneCS0234per handler:A lot of you use
codegen testas the PR gate on pre-generated code, so this has been quietly breaking builds for two releases.codegen writefollowed bydotnet buildwas unaffected, and so was running inTypeLoadMode.Static, which is why it took a while to surface.Two changes collided.
codegen testcompiles each generated file into its own assembly so it can enforce service-location rules per file, and 6.37.0 taught the handler registry to root every generated handler by name for native AOT. Compiled in isolation, those names point at types in other in-memory assemblies. HTTP-only applications never saw it, because their rooting only names the registry itself.The fix is in JasperFx 2.73.2, which this release picks up. Wolverine's CI now runs
codegen testagainst a project with message handlers, which nothing here did before -- the drift gate runscodegen writeand the AOT smoke tests runpublish, so this whole path had no coverage at all. (#4486)Thanks to @andrevlins, who bisected it across four versions, found the root cause, wrote the upstream fix and validated it against four of his own services.
Dead lettering settles the message exactly once
MoveToErrorQueuealways callsCompleteAsync()right after moving a message to the dead letter queue, and it has to: on SQS and Google Pub/Sub the dead letter move only sends a copy, so that trailing call is the only thing that ever settles the original. On Azure Service Bus and RabbitMQ the move is itself a settle, and the second one is redundant.Azure Service Bus never said which it was doing. On a normal queue the redundant settle came back as "the lock supplied is invalid" and was swallowed -- harmless and invisible. On a session-enabled queue it comes back as
SessionLockLost, which forces the AMQP management link closed and reopened before the next session can be accepted. The message always reached the dead letter queue; you paid for it in latency on whatever picked up that queue next. (#4481)Two more in the same area:
MaximumBrokerRedeliveries, an over-delivered duplicate on SQS or Google Pub/Sub was dead lettered and then left unsettled, so the broker redelivered it and it was dead lettered again -- one copy per redelivery, in the very branch that exists to break that loop. (#4488)Oracle can recover incoming messages again
The durability agent threw on every cycle:
Oracle returns
count(*)as aNUMBER, which ODP.NET surfaces asdecimalorInt64, andGetFieldValueAsync<int>()is a cast rather than a conversion -- it throws on anything butInt32. Recovery of incoming messages was dead for Oracle users. (#4480)This is the second time the same provider mapping has broken a durability operation, so the conversion now lives in one place rather than being fixed at each call site as it turns up. Thanks to @Trasvi for the report, the bisect and the fix.
EF Core DbContext abstractions compile
If you registered an abstraction with
WithDbContextAbstraction<IBillingDbContext, BillingDbContext>(), the generated handler came out as:... (truncated)
6.39.0
Two themes in this release: multi-tenancy correctness and modular monolith ergonomics, plus a health-signal fix that will quiet a lot of false alerts.
Declare a module's ancillary store once
Modular monoliths on ancillary stores had to repeat
[Storage(typeof(IOrdersStore))]on every handler, endpoint and service in a module -- restating on each type a fact that belongs to the module, where missing one meant quietly committing to the wrong database.That covers message handlers, HTTP endpoints and gRPC services in that assembly, for Marten, Polecat and Fisher alike. An explicit
[Storage]on a type still wins, so one handler can opt out of its module's default.For gRPC this is not an ergonomic win but the only thing that works: the gRPC chains never apply chain-modifying attributes, so
[Storage]on a gRPC service compiles, looks right, and does nothing. (#4477)The stuck-poller health check was mostly crying wolf
This was for CritterWatch
The scheduled-job "poller is stuck" signal counted every scheduled envelope, whether or not it was due yet. A queue holding messages that are not due is a queue doing its job -- so any deliberate delay longer than the check window reported the poller as stuck, and stayed that way. Ordinary retry scheduling has the same shape, which means the signal grew with correct usage.
Measured on a production fleet of 512 sharded message databases: 254 of 271 active alerts -- 94% -- were this one check, across 265 databases. 114 of those were "degraded" over a single envelope scheduled 15 minutes out by an application deliberately waiting for a quiet period.
PersistedCounts.ScheduledDuenow counts only envelopes already past their execution time, and the health signal reads that instead. It is anint?, and null means not measured rather than zero: a store that does not report it makes the signal stand down rather than falling back to the undifferentiated count, because falling back is the defect. PostgreSQL implements it as aFILTERon the existing scan, so the due count costs no extra query; the other providers report null and are simply silent here for now. (#4476)Tenant message stores were sharing an identity
IMessageStore.Nameis a tenant routing cache key, so two tenant stores answering to the same name send one tenant's messages to the other tenant's database.If you run multi-tenanted durable messaging on any of these three, this release is worth taking.
RabbitMQ virtual-host tenants never got publisher confirms
ConfigureChannelCreation(...)reached only the parent transport's channels. Every virtual-host tenant created its channels withPublisherConfirmationsEnabledandPublisherConfirmationTrackingEnabledfalse regardless, with no public way to set them per tenant.That matters more than a missing option: without confirmation tracking,
BasicPublishAsyncreturns before the broker can refuse the publish, so the sending agent counts it successful and deletes the envelope from the durable outbox. A refused publish -- anACCESS_REFUSEDafter a vhost user loses write permission, say -- silently drops a message whose enrolling transaction has already committed.Behaviour change worth knowing about: if you call
ConfigureChannelCreationand have tenants configured, your tenant channels now get confirms andConsumerDispatchConcurrencywhere they previously got neither. Publishing to tenant vhosts gets slower, correctly so.Thanks to @outofrange-consulting for a report that arrived with a measurement table and the fix already located. (#4473)
Ancillary-only hosts picked the wrong persistence strategy
A host registering only an ancillary store through
IntegrateWithWolverine<T>registered no codegen extension, so[Entity]and storage-action code silently read an empty in-memory dictionary. Fixed for Marten (#4464), Polecat (#4465) and Fisher (#4466).Scheduled messages promoted from RavenDb and CosmosDb lost their store
... (truncated)
6.38.0
Eight issues, no breaking changes.
This is a correctness and operability release. Most of it is one shape of bug — something resolved against the wrong scope, which looked right only because two defaults usually coincide — plus the two remaining halves of recurring-schedule operability.
Multi-store and multi-tenancy
The multi-tenanted message store no longer swallows batch failures (#4435). A durable batch spanning several tenants was split across stores, but
RetryBlocknever rethrows — so a store that refused its share failed silently while the receiver acknowledged the whole batch. Messages no store had accepted were acked and lost. The batch is now split by the resolved store and a failure propagates.Natural keys resolve through the store the chain is routed to (#4439).
Identity types resolve through the store the chain is routed to (#4441).
These two are a matched pair: a saga's natural key and its identity type are facts about the store, not about the application. A modular monolith with an ancillary store per module resolved both against the main store, so a saga in module B was looked up with module A's rules.
A Wolverine service name reaches JasperFx, so the Event Model canvas stays whole (#4448).
WolverineOptions.ServiceNameandJasperFxOptions.ServiceNameboth name the one running service, but the value only ever travelled one way — so the documented way to name a Wolverine service left the JasperFx side on its default, the...Description has been truncated
The updated Wolverine release changes its generated handler adapters. This PR also regenerates the Gateway and Admin adapters, including the new message-context registration and trimming metadata. Validation: both ordinary production Release builds and scripts/generate-wolverine-code.ps1 -Verify passed against isolated Postgres and Redis instances.