Repository navigation
Wolverine 6.49.0 is a feature release. The big themes are optimized envelope ids for SQL Server, additive per-message topic routing across NATS, RabbitMQ and MQTT, a long list of NATS hardening, and a big round of Event Modeling and Bobcat work. Thank you to @knotekbr, @me-azlam-kp, @faustodc, @luisarellano09 and @alesdvorakcz, whose reports, tests and pull requests are all over this one.
The two Bobcat packages, WolverineFx.Bobcat and WolverineFx.Bobcat.Http, ship as 6.49.0-alpha.3 and are still pre-release.
SQL Server
- Optimized envelope ids for SQL Server, automatically (#4967, closes #4902, analysis and measurements by @alesdvorakcz). Wolverine's default envelope ids sort in time order the way .NET and PostgreSQL compare GUIDs, but SQL Server compares a
uniqueidentifierstarting from its last six bytes, so to SQL Server those ids arrive in random order and fragment the clustered inbox, outbox and dead letter tables. One production inbox was measured at 99% fragmented with its pages half full. The newEnvelopeIdGeneration.NewIdSqlServerOrderuses the same NewId algorithm with the timestamp where SQL Server sorts on it. Wolverine now switches to it on its own at startup when every message store in the application is SQL Server -- plain SQL Server persistence, the SQL Server transport, EF Core on SQL Server, or Polecat -- and you haven't setEnvelopeIdGenerationyourself. There's no schema change. If a SQL Server-only application sends messages to Wolverine services that store their envelopes in PostgreSQL, opt out, because the receiving service uses the same id as its inbox key and PostgreSQL indexes these ids worse than random GUIDs. Either setopts.EnvelopeIdGeneration = EnvelopeIdGeneration.NewIdor use the newoptimizeEnvelopeIdsForSqlServer: falseoverloads ofPersistMessagesWithSqlServer(),UseSqlServerPersistenceAndTransport()and Polecat'sIntegrateWithWolverine(). Behind it,IMessageStoregains aPreferredEnvelopeIdGenerationmember with a default implementation, so existing stores are unaffected.
Routing
- Publish to a broker topic and to a local queue at the same time (#4960 and #4966, superseding #4930 from @luisarellano09). A per-message topic rule --
PublishMessagesToNatsSubject<T>(),PublishMessagesToRabbitMqExchange<T>()orPublishMessagesToMqttTopic<T>()-- used to silently switch off an explicitPublishMessage<T>().ToLocalQueue()rule, local routing to a handler, and broker conventions for the same message type. Each of those now has an overload that takesadditive: true, so the other routing rules still apply. The original overloads are unchanged. A customIMessageRouteSourcecan opt into the same behavior through the newIPassThroughMessageRouteSource, andExplainRoutingFor()labels such a source[additive, pass-through]. MultipleHandlerBehavior.Separatedfor conventional routing on Amazon SQS, Azure Service Bus queues and GCP Pub/Sub (#4948, #4949, #4951, closing #4524, #4525 and #4526). Each handler type gets its own queue or subscription, on top of a small new hook for routing conventions.
NATS
- An inline endpoint throws a broker-refused publish to the caller (#4958, closes #4955). A JetStream publish the server refuses -- a stream full under
DiscardPolicy.New, a failedNats-Expected-*check, an oversized message -- used to be retried three times, logged as errors, and dropped, whileSendAsync()returned as if it worked. Now it isn't retried, andSendAsync()throws the newSendRejectedExceptionwith the JetStream error inside. Other transports can throwSendRejectedExceptionfrom their senders to get the same treatment. - Resource setup creates a declared stream from its declaration (#4957, closes #4956). A missing stream that was declared with
DefineStream()used to be created as a default work-queue stream, which then showed up as drift underProvisioning(Verify). - Leftovers from the 6.47 JetStream hardening (#4945, #4860): the transport reuses a live connection across start retries; header values are sanitized of CR/LF; stream mirrors, sources and placement can be declared; and JetStream publish options are configurable through
ConfigureJetStreamPublish.
HTTP
- A request body Kestrel refuses keeps Kestrel's status code (#4934 by @knotekbr, closes #4933). A body over
[RequestSizeLimit], for example, now answers 413 instead of a 500 saying the body could not be deserialized. - An optional body sent chunked is no longer silently dropped (#4936 by @knotekbr, closes #4935), followed by exact parity with Minimal API for optional bodies (#4944) and Kestrel-backed tests for request bodies (#4942), since TestServer and Kestrel don't agree on the details.
Persistence and durability
- Conjoined EF Core multi-tenancy with partition-per-tenant (#4937, closes #3531, #3541 and #3542). EF Core sagas support partition-per-tenant, there's a rebuild API for turning on
PartitionPerTenantfor a table that already exists, and EF Core, Marten and Polecat conjoined tenancy are tested together in one database. Along the way, this fixed a saga saved with a temporary EF Core key landing under a tenant id that was a random GUID. - A handled message is never brought back to life by a late retry (#4950, the main remaining work on #4216). Every message store now leaves a handled inbox row alone when a retry is booked for it after the fact. Before, most stores turned it back into a scheduled message that would run a second time. RavenDB and CosmosDB also got an update-first reschedule and a fixed release query.
- CosmosDB under
MessageIdentity.IdAndDestination(#4969, closes #4216). Running the shared message store tests under that identity found that it put slashes into Cosmos document ids, which Cosmos does not allow, so every update quietly missed its document. That's fixed, and CosmosDB and RavenDB both now run the whole contract under that identity. - The database control listener stops without waiting out its poll delay (#4952 by @me-azlam-kp), which took up to five seconds off a quiet node's shutdown on the relational stores and CosmosDB.
- A composite listener keeps disposing its children after one fails (#4953 by @faustodc).
- Fixed the CosmosDB CI hang (#4913) by moving to
Microsoft.Azure.Cosmos3.63.2 and pinning the emulator image.
Agents and leadership
- Leadership election and agent assignment follow-ups (#4932). A database split across nodes is consolidated onto the node running most of it, and pins are respected. A dead node's queued starts are abandoned instead of stalling. There's a cold-start hold, and a new leadership eligibility setting. Both new
DurabilitySettingshave backward-compatible defaults.
Code generation
- Generated code no longer depends on what else the process planned first (#4946, closes #4877). The local variable a continuation frame declares used to be numbered from a process-wide static counter.
Event Modeling and Bobcat
- A new Event Modeling tutorial (#4963, closes #4918) walks the whole Critter Stack workflow in one place: declaring the model in code, importing a board with Bobcat, the generated specifications, and scaffolding.
wolverine scaffoldfollows Bobcat'sFeatures/{Chapter}layout (#4959), gains--jsonoutput with stable notice codes (#4947, closes #4928), and has a round of output fixes: record views built withCreate, the events that start a stream first, and long attributes on their own line (#4929). Scaffolded HTTP endpoints are tested against Marten, Polecat and Fisher, and a stream start answers 201 with its id (#4947, closes #4927).- The CritterCrush review (#4924, closing 15 issues) covered the event-model scaffold,
[Emits]inference, definition discovery,describe-event-model, and new WolverineFx.Bobcat assertions. - Tracked sessions record the events each session appended (#4943, closes #4931, #4865, #4919 and #4926), along with WolverineFx.Bobcat support for DCB tags, paragraphs and
FetchLatestread models, and DCB and multi-stream scaffolding. - Bobcat specifies a DCB decision's consistency boundary (#4964, refs #4940), checked deterministically.
ThenEventuallyCausedchecks the events an act caused past an async boundary (#4965, closes #4939). The new WolverineFx.Bobcat step finds them by correlation id once the async daemons catch up, and there's an opt-in correlation header for every HTTP endpoint.