Bump the wolverine group with 7 updates - #246
Open
dependabot[bot] wants to merge 1 commit into
Open
dependabot[bot] wants to merge 1 commit into
dependabot[bot] wants to merge 1 commit into
Conversation
Bumps WolverineFx from 6.21.0 to 6.39.1 Bumps WolverineFx.EntityFrameworkCore from 6.21.0 to 6.39.1 Bumps WolverineFx.FluentValidation from 6.21.0 to 6.39.1 Bumps WolverineFx.Postgresql from 6.21.0 to 6.39.1 Bumps WolverineFx.RabbitMQ from 6.21.0 to 6.39.1 Bumps WolverineFx.RuntimeCompilation from 6.21.0 to 6.39.1 Bumps WolverineFx.SqlServer from 6.21.0 to 6.39.1 --- updated-dependencies: - dependency-name: WolverineFx dependency-version: 6.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine - dependency-name: WolverineFx.EntityFrameworkCore dependency-version: 6.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine - dependency-name: WolverineFx.FluentValidation dependency-version: 6.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine - dependency-name: WolverineFx.Postgresql dependency-version: 6.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine - dependency-name: WolverineFx.RabbitMQ dependency-version: 6.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine - dependency-name: WolverineFx.RuntimeCompilation dependency-version: 6.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine - dependency-name: WolverineFx.SqlServer dependency-version: 6.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: wolverine ... Signed-off-by: dependabot[bot] <support@github.com>
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: Repository: emeraldleaf/NextAurora/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 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 |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
Updated WolverineFx from 6.21.0 to 6.39.1.
Release notes
Sourced from WolverineFx's releases.
6.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 entry assembly name. The visible damage was an Event Model canvas splitting in two: Wolverine's source named its model one thing, a store's source named it another, and neither canvas held both halves. It only ever looked correct when a host's assembly name and service name happened to coincide, which is exactly why no test caught it.Recurring schedules
The remaining core operability gaps from #4437, which is closed by these two. (Durable last-run state, #4447, stays closed as not-planned: run state lives in OpenTelemetry, and the operability view belongs in CritterWatch.)
Non-UTC recurring schedules now record their tracking row (#4436). Cronos returns each occurrence carrying the schedule's offset, and Npgsql's
timestamptzbinder refuses any non-zero offset — so every tick of every zoned schedule threw on the bookkeeping write. Delivery was never affected; only the tracking row was missing. Normalizing inRecurringMessageRecord'sinitaccessors fixes it in one place for all four relational providers.Occurrences carry their schedule and their firing instant (#4445). The schedule name already reached the handler span. The occurrence instant did not:
ScheduledTimeis cleared by the scheduled machinery at fire time, so a handler could only learn which firing it was serving by string-parsing the deduplication id. Occurrences now carry arecurring-occurrenceheader, surfaced as thewolverine.schedule.occurrencetrace tag.Metrics also gained a
schedule.nametag, so the success, failure and effective-time counters can finally be sliced per cron job. It is read off the envelope header rather than set locally, because the metric tag list is never serialized — an occurrence published on one node and handled on another would otherwise reach the counters with no attribution at all. The occurrence instant is deliberately trace-only: one distinct value per firing would make those series unbounded in cardinality.IRecurringScheduleControl.TriggerAsyncruns a schedule once, on demand (#4446). Previously an operator's only option was hand-publishing the message type out of band, which bypasses the occurrence and deduplication machinery entirely. The request is recorded on the schedule's durable tracking row and the agent publishes one occurrence for it on its next pass, so it works from any node — the same reason pause already goes through the store.A manual run carries its own deduplication id, so a "run now" issued in the same instant as a scheduled firing is never silently collapsed into it. Triggering a paused schedule is refused: pausing says the schedule must not fire. A trigger is extra rather than a replacement — it leaves the cron cadence and the pending occurrence untouched, and it fires even for a fixed-date schedule whose occurrences have run out.
gRPC
[WolverineGrpcService]on the interface (#4396). A contract you do not own — or one carrying only[ServiceContract]— could not be registered at all.AddWolverineGrpc(grpc => grpc.IncludeCodeFirstContract<IMyService>())now registers it explicitly. Thanks to @erikshafer for the PR.6.37.0
Eight issues, one of them breaking.
ServiceCapabilities.EventModelis now anEventModelSetDescriptorrather than a singleEventModelDescriptor(#4424). A host can legitimately assemble several Event Models — each store names its own throughStoreOptions.EventModelName, and a modular monolith registers an ancillary store per module — and the export used to fold them all into one named for the service, losing a model's name outright and reporting nothing.This is a compile break for anything reading that property, and the capabilities wire shape changes with it. A consumer that can only render one model asks
.Sole, or folds explicitly with.Collapse()and gets aModelCollapsehotspot recording what it lost. CritterWatch consumes this shape and has the equivalent fold still to follow.Everything else in the public surface is additive.
Event Modeling
FinishModelcarried a private copy of the cross-slice join; it is re-based onEventModelDescriptor.Links, so the pattern Wolverine derives and the arrow a viewer draws cannot disagree. A slice triggered by another slice's event throughTriggerType— not onlyCommandType— is now classified too.ReadsFromis split out ofReadModelTypes(#4419).[ReadModel]and[Entity]parameters are things a slice reads;IStorageAction<T>returns are what it produces. They shared one list, which meant the Automation input edge — Event → Read Model → ⚙ Command — could not be drawn at all.Origin(#4425). Wolverine registers two sources on theDerivedrung, so a disagreement between them used to render asDerived claims X; Derived claims Y, naming neither file. It now readsevent-model://wolverineagainstevent-model://wolverine-http.Native AOT
codegen writeemits its own[DynamicDependency]rooting (#4426). Every Native AOT application had to hand-write a rooting block covering the generated registry, every generated handler, every handler class, every message type, andMessageRouter<T>/EmptyMessageRouter<T>closed over each one. Codegen now emits anAotRootscompanion anchored by[ModuleInitializer]— an unconditional ILC root — so there is no app-side code at all. Verified by a realPublishAotbinary booting and dispatching with the hand-written roots deleted.Bug fixes
Envelope.Storedoes not survive persistence, so the acknowledgement fell back to the main store — the ancillary row survived, and the message was recovered, sent and handled again on every restart. Thanks to @raypet-visma for the diagnosis and the fix sketch._consumer.Close()is a synchronous P/Invoke that can block forever against a degraded broker, soIHost.StopAsyncnever completed — observed wedged 20+ minutes, past bothDrainTimeoutandShutdownTimeout. It now runs under the drain budget on a dedicated thread, and an abandoned teardown suppresses the consumerDisposerather than destroying a handle another thread still owns.OnExceptionreturningOutgoingMessagescompiles again (#4416). It failed code generation with "Frame chain is being re-arranged" while the same method on a middleware class worked. Thanks to @uniquelau for the report and for locating the exact divergence. The error-handling docs gained an example of using the hook to publish messages when the original message fails.Build & dependencies
codegen writeoutput is regenerated and aCICodegenDriftgate now guards it — meaningful only now that the emitted statement order is deterministic. Regenerating surfaced real staleness rather than the expected reordering: six orphaned handler files and four missing registry files.6.36.0
Heads up when upgrading
CircuitBreaker()on a buffered local queue now stops the host from starting with anInvalidListenerConfigurationException(#4410, #4412). A buffered local queue can't pause, so the circuit breaker used to be accepted and then silently ignored. AddUseDurableInbox()to the queue, oropts.Policies.UseDurableLocalQueues(), or remove the circuit breaker. Durable local queues and external listeners are unaffected. If you use WolverineFx.AI withDurableQueue = falseplus a circuit breaker on the callout queue, this applies to you too.SlicePattern.Command(#4413).New
[SlicePattern]declares the Event Model slice pattern of a message handler whose message has no producer in the model, such as a message from another service or a hosted service (#4395, #4413). It only fills the gap: an HTTP route, gRPC RPC, schedule or inbound external system still decides the pattern.ActivityLink(#4398, #4405).Fixes
System.Security.Cryptography.Xmlis now 10.0.12, clearing a high-severity advisory (#4401).Docs
System.Threading.Channels, not TPL Dataflow (#4411).EnableNodeAgentSupport()from the exclusive node processing page (#4414).6.35.0
Store operation side effects, everywhere
MartenOpscovered store / insert / update / delete plusStartStream; anything else meant taking anIDocumentSessionand giving up on the handler being a pure function. All three stores now coverwhat their own session API supports.
HardDelete,HardDeleteWhere,UndoDeleteWhereUpdateExpectedVersionUpdateRevisionTryUpdateRevisionPatch,PatchWhereQueueSqlCommandInsertObjects,DeleteObjectsAppend,ArchiveStreamUnArchiveStream,TombstoneStreamThe gaps are deliberate: each set was checked against that store's own session API rather than copied
across, and an op whose
Executecould only throw is worse than the absence of one. Polecat's lastrow is the reverse case — two operations Marten has no counterpart for.
Every op also implements
ITenantedMartenOp/ITenantedPolecatOp/ITenantedFisherOp, so oneextension scopes any of them while preserving the concrete return type:
Thanks to @erdtsieck for the Marten half, which is where this started.
Event Modeling: a declared model and the code now meet
Three findings from one comparison of a curated Event Model against the application built from it
(#4385, #4386, #4387):
behaviour (
ConfirmAppointment) while a derived source names it for the message type or the route.An eleven-slice application assembled as twenty-two with no disagreements — not because the sources
agreed, but because they never met.
[Emits(typeof(...))]lets a handler name the events its signature cannot carry.EventsToAppendand
StartStreamerase the element types, so the more idiomatically event-modelled an applicationwas, the emptier its derived model got.
Patternis left unclaimed for a message handler. A handler cannot tell a Command from anAutomation, so a declaration wins the role instead of losing to a guess.
pattern: "Command"inevent-modeloutput or theServiceCapabilitiessnapshot.Patternis still derived wherever thecode answers the question — HTTP routes, gRPC RPCs, schedules, external systems, and any slice whose
... (truncated)
6.34.0
Seventy commits since 6.33.0. The bulk of it is a sustained performance wave on the durability
and message-execution paths, alongside a new recurring-schedule feature, Native AOT support that
now boots end to end, and a long run of clustering and transport fixes.
New
opts.Schedulesregisters messages to be published on acron expression, coordinated across the cluster so exactly one node fires each occurrence. (#4307)
a transport-wide default dead letter queue name. Note the migration hazard called out in the docs
if you adopt the prefix on an existing deployment. (#4263, #4281)
http://destinations, not justhttps://.ITransport.AdditionalProtocolsis the general mechanism, so any transport with legitimatelymulti-scheme addresses can opt in. (#4200 / #4379)
ResourceMigrationFailureMode.FailFaststays the default andkeeps
resources setupstrict;ContinueOnFailureslets a host whose broker topology isexternally owned log the failures and start. (#4119 / #4380)
Performance
The GH-4316 wave, measured on the multi-transport perf rig rather than by inspection. Highlights:
PostgreSQL and SQL Server, 37ms down to 0.04ms on the measured query. (#4336)
RavenDB. (#4369 / #4370)
throughput and 26% lower publish latency where the application publishes concurrently. (#4319 / #4368)
Envelopebodies above the LOHthreshold (13.5x at 100KB, with Gen1/Gen2 collections gone). (#4320, #4333)
transaction no longer trips SQL Server's 2100-parameter limit. (#4375 / #4376)
header handling, and metric accumulation.
DateTimeOffset.Nowis gone from the per-message paths.(#4322, #4323, #4324, #4325, #4326, #4328, #4335)
tables (#4334); SQL Server metrics counts come from index metadata instead of three full scans
(#4318); Redis Streams gains batched durable arrival, measured at +159% (#4329).
Two changes measured negative on the rig and were reverted rather than shipped — the Azure Service
Bus prefetch default and the coalescer's
Queueshape. Both are recorded so they are not revisited.Native AOT
A Wolverine application now completes a Native AOT publish and boots through its own bootstrap.
(#4287, #4298, #4301, #4305). A Native AOT app with any external transport used to die building its
first route — fixed in #4232 / #4378. The AOT publish smoke test hard-asserts a full boot.
Clustering, agents and durability
... (truncated)
6.33.0
The headline is a new package. WolverineFx.AI makes a one shot LLM call an ordinary Wolverine message: durable, outbox enrolled, retried by the same rules as everything else, and testable without a model anywhere in sight.
WolverineFx.AI (new package)
An
LlmCalloutis a message. Return one from a handler next to your storage action and it is enrolled in that handler's outbox, so a callout cannot fire for a transaction that did not commit and cannot be lost to a restart in between. The model's answer comes back as an ordinary cascading message, with an ordinary handler, an ordinary retry policy, and its own place in the correlation chain. (closes #4227)LlmBudget.MaximumPromptCharactersrefuses a runaway prompt before your provider is ever called;MaximumTokensPerWindowrefuses callouts once the node has burned its allowance. Both dead letter rather than retry, and so does an answer that cannot be parsed into the response type you asked for -- retrying either is the runaway spend the budget exists to stop.IChatClientfor testing.StubChatClientexercises a callout's whole round trip with no key, no network and no model.Wolverine.AI.AotSmokeproject underTrimMode=fullthat CI runs. (closes #4230)The package references only the Microsoft.Extensions.AI abstractions, never a vendor SDK. The provider -- Anthropic, OpenAI, Azure, Ollama -- and any middleware over it stay your choice.
Fixes
wolverine_nodescredited only one, so nothing in the system could ever stop the extra copy. (closes #4240)AssignmentChangeddescription carries an agent URI, a schema name and a destination node, which on a real cluster overran thedescriptioncolumn and failed the wholeAgentCommandbatch behind it. MySQL was worst hit atVARCHAR(255). (closes #4246)ServiceLocationPolicy.NotAllowed-- on HTTP endpoints and, now, on message handlers. Under the Wolverine 6 default this made the validation middleware unusable and threw at bootstrap. (closes #4238)[Entity],[All],[Queryable],[WriteAggregate]and the rest. (closes #3935)opts.DefaultDuplicateStatusCode, and deduplication refusals now advertise their problem document in OpenAPI.IHost.StopAsyncno longer tears the agents down twice.Upgrade note
6.33.0 requires Weasel 9.30.0, and that raises the GH-4246 fix from "new databases only" to "existing ones too": the schema differ now compares character lengths, so a widened
varcharis no longer invisible to it and an existing table is corrected in place by anALTER TABLE ... MODIFYthat keeps its rows.Worth knowing before you upgrade: that comparison runs in both directions. Width drift that was previously invisible now generates
ALTERs, and a model narrower than an existing column will emit a narrowingALTERthat can fail on real data. Sizes that are not character lengths -- a MySQLint(11)display width, a decimal precision, a datetime fsp -- are still ignored.Dependencies
JasperFx 2.60.0, Marten 9.30.0, Polecat 5.21.1, Fisher 1.0.6, Weasel 9.30.0.
6.32.0
See CHANGELOG.md for the full entries.
New packages
WolverineFx.AmazonS3andWolverineFx.AzureBlobStoragecarry document and saga persistence plus the claim check store that used to ship separately. Registration is explicit per type —Store<T>()andSaga<T>()are separate calls and each refuses the other's type — and saga writes are guarded by conditional requests, surfacing asSagaConcurrencyExceptionso oneOnException<ConcurrencyException>policy still covers every store. (#4160, originally #4165 by Anne Erdtsieck.)WolverineFx.ClaimCheck.AmazonS3is deprecated. The namespace is unchanged, so migration is a package reference swap — but keeping both referenced produces ambiguous-type errors.Redis document and saga persistence folds into the existing
WolverineFx.Redisrather than a new package, with saga concurrency implemented as a Lua compare-and-swap.Fixes
EnableInboxPartitioning(#4216); previously it could not be retired at all.AddStopConditionIfNullaccepts the null identity its signature declares (#4161).Diagnostics
BufferLimitis now null on the modes that never enforced it, with the broker's prefetch window reported asInFlightLimit.MaximumBrokerRedeliveriesis documented as the delivery count it actually is (#4216). Behaviour unchanged.Event model
Also
[FromMarten]and[FromEfCore](#4214).AddResourceSetupOnStartupandAutoProvision(#4223).6.31.0
Logical message deduplication
Envelope.Ididentifies one delivery. That is the right identity for "the broker handed me this twice" and the wrong one for "the operator clicked Rebuild twice" — those are different deliveries of the same intent, so each carries a differentEnvelope.Idand every one gets through.6.31.0 promotes
Envelope.DeduplicationIdinto a first-class logical id, with storage, enforcement, and a retention policy behind it.It is opt-in throughout — leaving it off means no schema change at all on upgrade. Storage is a separate
wolverine_deduplicationtable rather than a column on the inbox, because underEnableInboxPartitioningthe inbox isPARTITION BY LIST (status)and marking an envelope handled moves the row between partitions, which would let one logical id exist as both Incoming and Handled — silently, and only for users who enabled partitioning. Claiming is anINSERTthat either succeeds or trips the primary key, never aSELECT-then-INSERT.Refusals differ per chain type: a message handler discards and acks, HTTP returns 409 with
ProblemDetails(configurable to 2xx where a replay is benign), gRPC returnsAlreadyExists/InvalidArgumentper AIP-193. Storage on PostgreSQL, SQL Server, MySQL and SQLite.Deriving the id from the message
The publishing side does not have to remember
DeliveryOptions.DeduplicationIdat every call site. A message type declares its own logical identity once, the way it already declares a topic name with[Topic]or a saga id with[SagaIdentity]:These are
IEnvelopeRuleat the message type level, resolved once when the route is built rather than per message. An explicitDeliveryOptions.DeduplicationIdalways wins, then configured rules, then the attribute.Fixes
ListeningAgentsees past its receiver wrappers.ReceiverWithRules— installed by a bare endpoint-levelMessageTypeorTenantId— is unconditionally anILocalQueue, so a wrapped NativeAck or Inline receiver took the wrong branch and threw on the durability agent's re-entry path. The same blindness meant a terminally faulted receiver reported healthy forever on exactly the endpoints most likely to be non-trivially configured. (#4188, #4191)TriggerLabel, which was beating overlay declarations and minting aSourceDisagreementhotspot per labelled route; and a collection response now reads its element type instead of reporting an assembly-qualified CLR string as a canvas node. (#4181, #4182)Dependencies
Full changelog: JasperFx/wolverine@V6.30.3...V6.31.0
... (truncated)
6.30.3
Patch release. Requires JasperFx 2.57.1, which ships the code-generation half of two of these fixes.
Several of these failed silently — a host that started clean, passed health checks, and did less than it appeared to. Worth a look if any of the shapes below match your application.
Code generation and service location
ServiceProviderSource.IsolatedAndScopedis now honored by Wolverine.HTTP (#4171). An endpoint or middleware asking for anIServiceProvideralways receivedhttpContext.RequestServices, whatever you configured. Note the consequence: asking for anIServiceProviderin an endpoint is service location and now registers as such, so underServiceLocationPolicy.NotAllowedthose endpoints will throw where they previously slipped past the policy unnoticed. Message handlers have always behaved this way.Scope priming now fires for every chain that service-locates, not only those naming an
IServiceProvider(#4171). If a chain reached service location solely through an opaque scoped/transient registration, its child scope was never primed — so a service-locatedIMessageContext,IMessageBus, or MartenIDocumentSessionwas a second, un-enrolled instance rather than the one the handler already owned. Handlers and HTTP endpoints are both covered now.Lazy<T>dependencies resolve through their registration (#4159). An open-generic registration such asTryAddScoped(typeof(Lazy<>), typeof(LazyResolver<>))was ignored whenever the closed type was itself concrete, andnew Lazy<IFoo>()was emitted instead. That compiles and can never work — the first.ValuethrowsMissingMemberExceptionfor any service without a public parameterless constructor. Relatedly,AlwaysUseServiceLocationFor(typeof(Lazy<>))accepted an open generic and then matched nothing; it now matches that generic's closed forms.Sagas
ResequencerSagaadvancesLastSequencewhen a message is handled, not when it is published (#4172). A replayed message could let a queue backlog walk past the ordering guard while it was still in flight, reordering the handled sequence.An already-sequenced arrival is observable and overridable (#4175). A message whose order the saga had already passed was handled again in silence. The new
shouldHandleAlreadySequencedhook logs a warning by default — behavior is unchanged — and can be overridden to discard, raise a metric, or throw.Startup
AutoCreate.Noneno longer pays for a full schema diff at startup (#4166).Full changelog: JasperFx/wolverine@V6.30.2...V6.30.3
6.30.2
This addresses an issue encountered by a JasperFx client hitting a sudden crunch of messages being enqueued into local queues. Not something we expect to be common at all, but now we're better anyway!
What's Changed
Full Changelog: JasperFx/wolverine@V6.30.1...V6.30.2
6.30.1
There's some CritterWatch related functionality smuggled in here for our forthcoming Event Modeling visualization. Otherwise, this is mostly a ton of fine grained improvements for CI or message broker usage problems detected by dogfooding and some "Mr. AI tool, go try to identify potential problems" action
What's Changed
Full Changelog: JasperFx/wolverine@V6.30.0...V6.30.1
6.30.0
Wolverine 6.30.0 is a large release built around one headline feature — a new endpoint mode — plus the usual crop of transport fixes, and a couple of long-standing multi-tenancy and HTTP gaps closed.
EndpointMode.NativeAckThe main event. Buffered's throughput and partitioning with Inline's no-loss guarantee, and no database required.
A broker delivery is held unacknowledged while the envelope flows through an in-memory, optionally group-partitioned execution block, and is settled natively when the handler pipeline terminates. Nothing is acknowledged ahead of its handler, so work parked in a lane when a node goes away comes back rather than vanishing.
The guarantee, stated exactly: no two messages sharing a group id execute concurrently. Ordering is per-slot best-effort, not per-group guaranteed; redelivery may reorder. Anything needing strict order under failure keeps the durable inbox.
Transport support is opt-in and default-closed — a transport must explicitly claim the mode, because most settlement models cannot express out-of-order completion. Adopted by RabbitMQ, Amazon SQS, Azure Service Bus, NATS JetStream, Redis Streams, Pulsar and GCP Pub/Sub (#3708, #4046, #4047, #4050, #4051, #4052, #4053).
Supporting work in the same wave:
BufferedInMemory()(#3712, #4022).Multi-tenancy
IntegrateWithWolverine(). Marten hands Wolverine anNpgsqlDataSourcerather than a connection string, andNpgsqlDataSource.ConnectionStringdeliberately omits the password — so there is a newDbDataSourceoverload ofAddDbContextWithWolverineManagedConjoinedTenancythat carries credentials through intact. A second defect on the same path is fixed too:IntegrateWithWolverine()never registered the tenant partitioning provider, soPartitionPerTenant()failed (#4044).HTTP and event sourcing
[StreamState]and[StreamEvents]— new parameter attributes for handlers whose read is the raw stream rather than the folded aggregate, for timeline and audit shaped endpoints that[ReadModel]cannot express. Store-agnostic across Marten, Polecat and Fisher; Marten batches both fetches into a single round trip (#3627).[WriteAggregate]endpoints toProblemDetailsinstead of an unhandled 500. Note thatStreamLockedExceptionderives fromMartenException, notConcurrencyException, so catching only the latter silently leavesFetchForExclusiveWritingreturning 500s (#3764).HttpChainDescriptorandGrpcRpcDescriptornow carry the slice the route is, so a consumer walking endpoint by endpoint sees it next to the route rather than only through the assembled model (#4000).Transport fixes
MaxTotalAckExtensionsilently delivered a concurrent duplicate rather than reporting anything (#4066); effective listener concurrency was not what the configuration implied, and the flow-control bound is global perSubscriberClientrather than per inner client (#4067).PubsubTopicOptions.OrderBygained a configuration surface (#4087).DeleteStreamEntryOnAcksilently never acked on Redis < 8.2, whereXACKDELis unsupported (#4058).Upgrading
Additive.
EndpointMode.NativeAckis opt-in per endpoint and default-closed per transport, andMaximumBrokerRedeliveriesdefaults to off. Requires JasperFx 2.55.0.6.29.2
A fix release. Four changes, three of them reported bugs.
RavenDB users should take this one
ClearAllAsyncdeleted node records by tracked entity from a session that had never loaded them, so a Solo-mode start after a Balanced-mode run threwInvalidOperationException: WolverineNode is not associated with the sessionon every stale node and the application could not start at all. The workaround of clearingWolverineNodesby hand in RavenDB Studio is no longer needed. (#3993, closes #3986)The compliance coverage written for that fix caught a second provider: SQLite orphaned every agent assignment row, because its assignment table has no
ON DELETE CASCADE(PostgreSQL, Sql Server and Oracle do). The orphans stay invisible until a node re-registers under the same id — the GH-3604 ejection path — where it returns owning agents it was never reassigned. The underlying gap was thatNodePersistenceCompliancenever exercisedClearAllAsyncat all, which is how two providers shipped it broken. It does now.Agents no longer stall on a node that cannot build them
When
IAgentFamily.BuildAgentAsyncthrew, the leader saw only an unconfirmed agent — which it deliberately does not treat as a failure — so the assignment stood and the same agent was requested on the same failing node forever. Reported as a 54-minute fleet-wide projection stall on a blue/green cluster with disjoint projection versions. Consecutive failed starts are now counted on the node that catches them and feed into the existing GH-3888 release path. NewDurabilitySettings.MaxAgentStartFailuresBeforeRelease(default3); set it to0for the previous behaviour. (#3994, closes #3970)The orphaned-message sweep no longer dominates database load
Reported against a 466-shard PostgreSQL deployment. The sweep's predicate could not use an index, so it full-scanned the whole inbox per database every five seconds to find nothing; the update was unbounded, so one node loss became a single ~910,000-row rewrite across the fleet; and it ran inside the shared recovery transaction, blocking inbox inserts. All three are fixed, with a new
OrphanedMessageReleaseBatchSizeand a dedicatedOrphanedMessageSweepPollingTime. (#3995, closes #3971)HTTP endpoints can take immutable request types
A
Before/BeforeAsyncmethod on an endpoint class that accepts the request type and returns it now replaces the request body for the rest of the chain, exactly as it has on the handler side since GH-516. Use it to stamp server-supplied values onto an immutablerecordrequest before the endpoint runs. (#3984)Full detail for every item is in CHANGELOG.md.
What's Changed
New Contributors
Full Changelog: JasperFx/wolverine@V6.29.1...V6.29.2
6.29.1
This bumps the Fisher dependency to 1.0. We needed this for the CritterWatch 1.0 release.
What's Changed
New Contributors
Full Changelog: JasperFx/wolverine@V6.29.0...V6.29.1
6.29.0
A feature release. Three of the five items fix silent failure modes — work that acted on a write which could still roll back, a convention mirror that installed a relay over a real handler, and two concurrent writers to one entity — so the notes below say what the old behaviour looked like, not just what changed.
AfterCommit— run work after the transactional commitAfterreads like a post-handler hook that runs at the end. It does not run after the commit (#3976, closes #3975).The commit is itself a postprocessor contributed by the persistence provider, and
Aftermethods are inserted at the front of that list. So anAftermethod observing a write is observing one that is not durable yet and may still roll back — and there was no supported way to ask for the other side of it, even though Wolverine uses that position itself for the outbox flush.Use the
AfterCommit/AfterCommitAsyncconvention or[WolverineAfterCommit], on message handlers, sagas and HTTP endpoints. Parameters bind exactly asAfteralready does.The position is structural, not positional — frames go into a new
IChain.PostCommitPostprocessorslist concatenated after every postprocessor at frame-assembly time, rather than being appended from a policy sequenced after the persistence policy. Getting the position right by luck of policy ordering is precisely what breaks silently later.Two behaviours worth knowing:
try/finally, so the exception unwinds straight past them. That is the point — the reason to want "after the commit" is usually that the side effect must not happen for a write that did not land.After's pre-commit position is unchanged and stays that way. Verified per provider: Marten, Polecat, Fisher, EF Core, RavenDb and CosmosDb each have a codegen test asserting the emitted call lands after that provider's own commit frame.A store-agnostic
EventsToAppendreturn typeWolverine.Marten.Events,Wolverine.Polecat.EventsandWolverine.Fisher.Eventsare identical but store-named, so a handler that wanted to be store-agnostic could not name any of them (#3969, closes #3941).The store-agnostic path did exist — a bare
IEnumerable<object>return is picked up by a fallback — but that fallback is positional.IEnumerable<T>is covariant, so every reference-typed collection in a return tuple is a candidate and the first one wins. Nothing failed at codegen and nothing failed at runtime; the wrong collection simply became the appended events.Ask what will be handled, and how a batch is shaped
Discovery materializes after options time, so an extension installing fallback handlers could not ask "will this message type have a handler?" and had to hand-roll a mirror of Wolverine's own discovery convention (#3977, closes #3974).
Such a mirror drifts, and it drifts silently: one that scanned a single assembly stopped seeing handlers that moved to a second, and installed a bare relay over a real handler — the exact defect the guard existed to prevent, with every codegen test still passing.
These are the document side counterparts to the
IEventOperationscontracts Wolverine already understood, and they are the only way store agnostic source can take a session without naming a concrete store type.Before this, such a handler failed codegen outright on a stock host. Once bound, its writes were queued into the session's unit of work and silently discarded — no exception. Both halves are fixed.
Durability agents no longer assigned to nodes that cannot run them
A node started with
Durability.DurabilityAgentEnabled = falsenever registers the durability agent family, so it threwUnrecognized agent scheme 'wolverinedb'the moment the leader handed it one. The leader re-issued the identical assignment every five minutes indefinitely, no durability agent ran anywhere for that store, andowner_id = 0outgoing envelopes were never recovered (#3963, closes #3954).The failure was silent in both directions — every queue table read zero while the backlog grew. Nodes now publish a marker capability when the family is actually registered, the leader skips nodes that have not, and when no node in the cluster is capable a warning names the condition and the setting.
If you run a Balanced cluster with
DurabilityAgentEnabled = falseon any node, this release is worth taking.Ancillary store transaction ownership
Ancillary store inference scanned
chain.ServiceDependencies(), which walks constructor graphs recursively — so a dependency that merely held an ancillary store matched. A read only store injected two hops down counted the same as an injectedDbContext, and a tenant Marten handler had its inbox and dead letters stolen by the wrong store (#3957, closes #3953).That inference was only ever correct for EF Core. There is a new default null
IPersistenceFrameProvider.TryDetermineTransactionOwnerTypefor it, implemented only by EF Core.RabbitMQ
ListenToRabbitQueue("orders").DrainWaitForPrefetch()to let already prefetched messages finish rather than letting the broker requeue them.StopAsyncis not always terminal, and aBatchingChannelsilently discards a post after completion, so a delivery landing between the drain and the dispose latch vanished and was redelivered.Description has been truncated