Skip to content

Wolverine 6.18.0

Choose a tag to compare

@jeremydmiller jeremydmiller released this 14 Jul 00:27
e617d39

Wolverine 6.18.0

A security-relevant serialization fix, a startup-fatal codegen fix, a silently-dead-listener fix in RabbitMQ, the first F# saga codegen support of any persistence provider, and the CI split that makes "merge when green" mean something again.

If you use MassTransit interop over a durable listener, take this release. See the first section.

⚠️ Security-relevant: reserved envelope headers could be spoofed through the durable inbox

#3408fixed in #3411

EnvelopeSerializer wrote the typed envelope properties to the wire format and then appended every Envelope.Headers entry verbatim, with no reserved-key filter — and the appended entries came last. Because the reader parses reserved keys straight back into typed properties, a Headers entry under a reserved key silently overwrote the real property on the next read.

A value in envelope.Headers["tenant-id"] is inert while the envelope is in memory. It stops being inert the moment the envelope crosses the serializer — any durable listener, the inbox/outbox, or the scheduled-message store:

  1. Something puts tenant-id into envelope.Headers.
  2. The durable inbox persists the envelope; the header is appended after the (null) typed property.
  3. On read back, env.TenantId is set from it.

saga-id reaches another saga's state, and id rewrites Envelope.Id — the inbox's dedupe identity.

This was live, not theoretical. MassTransitEnvelope.TransferData already copies every incoming MassTransit header into envelope.Headers unfiltered (and by assignment, not TryAdd). Any Wolverine app doing MassTransit interop over a durable listener has had this path open. If that describes you, this release is the one to take.

The fix filters reserved keys on the write side, so the typed property stays authoritative and a reserved key sitting in Headers becomes a no-op. causation-id is deliberately not filtered — DeliveryOptions intentionally carries it as a loose header for Wolverine.Marten's OutboxedSessionFactory, and it is never promoted by the reader.

Startup-fatal codegen fix

#3399fixed in #3406 — invalid generated class name for batched (array) message types. This one prevents the application from starting.

Fixes

  • #3388 (#3400) — refuse a competing Marten daemon under Wolverine-managed event subscription distribution. A DaemonMode.Solo/HotCold daemon alongside managed distribution is now an actionable startup exception instead of two schedulers quietly fighting over the same shards.
  • CritterWatch #698 (#3396) — IAgentRuntime.ApplyRestrictionsAsync persisted the restriction and then never dispatched the commands it computed, so pausing an agent had no immediate effect. Reported by @erdtsieck against a live cluster.
  • #3385 (#3403) — a header-identified saga invoked over a gRPC hop failed with an opaque Internal status. It now returns an actionable diagnostic telling you to put the saga identity on the request DTO.
  • #3398 (#3404) — [AsParameters] now rejects unparseable values in collection query parameters, closing the gap left by the scalar fix in #3372.
  • #3365 (#3412) — the Polecat primary IEventStore bridge registered twice, so GetServices<IEventStore>() returned the same store instance two times and anything iterating it double-counted. Polecat's own AddPolecat() had started registering IEventStore and Wolverine was still bridging it as well.
  • #3391 (#3419) — RabbitMQ: a successful eager channel restart never re-consumed. A callback-exception restart could leave an open channel with zero consumers while reporting State = Connected — a silently dead listener. The listener now defers to ReconnectedAsync(), which re-declares and re-consumes. Also pins the ConnectionMonitor tracking invariant that #3370 fixed but nothing guarded.

OpenAPI

#3380 (#3418) — OpenAPI parameters are now derived from the full binding chain rather than the handler signature alone. Two real defects closed:

  • Query/header values bound only by an After/Finally postprocessor were omitted from the operation entirely.
  • Route parameter types were read off resolved binding variables, so they degraded to the route constraint (or string) whenever the description was assembled before those frames resolved — which is exactly the build-time OpenAPI / openapi CLI path, because ASP.NET caches the first ApiExplorer read.

More importantly, this ships the OpenAPI shape-test harness that was missing. Adding a shape assertion is now one endpoint plus one [Fact], which is why this class of omission kept shipping unnoticed.

New: Azure Service Bus emulator support

#3366 (#3409) — the docs told you to call UseAzureServiceBusTesting(), which only ever existed in Wolverine's own test suite. It is now a real, shipping API:

// standard emulator ports (AMQP 5672, management 5300)
opts.UseAzureServiceBusEmulator();

// or explicit, if you've mapped the emulator elsewhere
opts.UseAzureServiceBusEmulator(connectionString, managementConnectionString);

The destructive delete-all-objects cleanup is strictly opt-in (DeleteAllExistingObjectsOnStartup()) — a mistaken call against a real namespace destroys nothing.

CI

#3350 (#3417) — the AWS and Polecat suites chronically blew the 20-minute job cap, so real failures were indistinguishable from timeouts and "merge when green" was unachievable. Both are now split across parallel shards; the slowest is 5m9s against an unchanged 20m cap, and CIPolecat's one-off 30-minute exemption is gone.

Community contributions

F# saga codegen for CosmosDB (#3410) by @thechucklingatom — the first GenerateFSharpCode saga-load support of any Wolverine persistence provider. Marten, RavenDb, and EF Core have none; Cosmos sets the precedent.

Note for F# saga authors generally: your saga type currently needs [<AllowNullLiteral>], because the generated null-guard emits isNull x and F#'s isNull carries a 'T : null constraint. That's a framework-wide constraint this PR was simply the first to surface, and it's tracked upstream in jasperfx#513.

Thanks

@erdtsieck — for #3388, CritterWatch #698, and a steady stream of production-grade bug reports out of a 512-tenant-database deployment that keep finding things our test suites can't.

Kafka record timestamps and headers (#3407, @jakub-petrylak-onerail) is still in review and lands in the next release.

Also in this release

  • Marten pinned to 9.15.1 (#3401), which fixes a data-correctness regression in nested-tenant session identity-map sharing.

Everything in this release

Every issue closed and every pull request merged between V6.17.3 and V6.18.0.

Issue Pull request Summary
#3408 #3411 ⚠️ Reserved envelope headers could be spoofed through the durable inbox
#3399 #3406 🚨 Startup-fatal: invalid generated class name for batched (array) message types
#3391 #3419 RabbitMQ: a successful eager channel restart never re-consumed — open channel, zero consumers, State = Connected
#3388 #3400 Refuse a competing Marten daemon under Wolverine-managed subscription distribution
#3385 #3403 Actionable diagnostic for a header-identified saga over a gRPC hop
#3398 #3404 [AsParameters]: reject unparseable values in collection query parameters
#3380 #3418 OpenAPI: derive parameters from the full binding chain + a shape-test harness
#3366 #3409 Ship a real UseAzureServiceBusEmulator() API
#3365 #3412 Polecat primary IEventStore bridge registered twice
#3350 #3417 CI: split the CIAWS and CIPolecat suites across parallel shards
CritterWatch #698 #3396 ApplyRestrictionsAsync never dispatched the commands it computed
#3410 F# saga codegen for CosmosDB (@thechucklingatom)
#3401 Bump Marten to 9.15.1

Issues opened during this release cycle, not yet fixed

Several were found while fixing the above, and a few are more consequential than what they came out of:

Issue Why it's worth knowing about
#3421 The AddOpenApi() pre-start freeze is only half-closed on hybrid hosts. The document goes from "empty" to "Wolverine routes present, every minimal-API route missing" — which looks populated and is therefore more deceptive
#3414 CosmosDB saga persistence has no optimistic concurrency — a blind UpsertItemAsync silently loses updates under concurrent messages
#3415 Every CosmosDB saga lands in one logical partition → 20GB / 10k RU/s ceiling for all saga state
#3416 CosmosDB requires a camelCase serializer policy and nothing documented it
#3413 Test suites pollute each other via the shared wolverine schema — and, underneath it, a real question about whether the durability agent should throw on an unresolvable transport
#3420 OpenAPI: Marten-aggregate-bound route ids render as string, not uuid, on unconstrained routes
jasperfx#513 isNull x codegen forces [<AllowNullLiteral>] on every F# saga, framework-wide

Full changelog: V6.17.3...V6.18.0