Wolverine 6.24.0
Two data-loss fixes — but for unusual usages
This release closes two bugs that silently destroyed data rather than failing loudly. Both are worth reading before you skip the rest of these notes.
Durable inbox rows were orphaned when a circuit breaker tripped (#3680). DurableReceiver checked its latched flag before calling MarkReceived. The latched path still persists each envelope to the inbox as a safety net — but on an envelope that never went through MarkReceived, Status is the enum default (Outgoing) and Destination is null. Both are filter columns for inbox recovery, so the rows were written in a state no recovery sweep on any node could ever see. The null Listener also skipped the nack back to the broker, and the broker's redelivery after restart hit DuplicateIncomingEnvelopeException — which acks and drops. Net result: genuine message loss under a durable inbox any time a circuit breaker trip latched the receiver mid-flight. Measured on the circuit-breaker suite, 9 of 1,200 messages were lost per run.
Dropping one tenant from a shared partition bucket destroyed its co-tenants' data (#3686). Found alongside #3683. Tenant bucketing — registering several small tenants against one partition suffix so they share a physical partition — is documented and exposed through PartitionPerTenant(p => p.AllowPartitionSharing = true), and it did not work on either engine. It had no test coverage, because the doc sample demonstrating it is compile-only and never executed.
Global partitioning
Part of the GlobalPartitioning epic (#3482).
- Global partitioning topologies for PostgreSQL and SQL Server queues (#3468, #3469)
- End-to-end sharded-processing suites for Azure Service Bus, GCP Pub/Sub, NATS, Redis Streams and Pulsar (#3467). The scenario is lifted into
Wolverine.ComplianceTests.Partitioning.ShardedProcessing, so a new transport costs one small test class - Native-mode design comparison and per-transport native alternatives documented (#3481)
The new suites immediately found two real bugs:
- NATS global partitioning had never worked at all. The topology forces
EndpointMode.Durableon every slot, and aNatsEndpointonly supportsDurablewhen JetStream-backed — so everyUseShardedNatsSubjects()call threw at configuration time. The topology now enables JetStream on its own endpoints and declares a work-queue stream per shard, without which the listener died at startup onstream not found - Pulsar named its companion local queues off the full topic path, producing queues like
global-persistent://public/default/orders1. They now use the topic's short name, matching every other transport
Multi-tenancy and persistence
- EF Core tenant partition back-fill (#3496). Routine migration deltas deliberately leave Weasel-managed partitions alone, so a table joining an existing managed set — a newly deployed service, or a newly mapped
ITenantedentity — had no partition for any tenant registered before that table existed.IConjoinedTenantPartitions<T>.MigrateTenantPartitionsAsync()reconciles every partitioned table against the full registered tenant set, with per-tableTenantPartitionResultreporting - Conjoined tenant partition bucketing actually works now, on both PostgreSQL and SQL Server (#3683, and see #3686 above)
- Exclusive listener inbox recovery is now covered for RavenDb (#3595) and CosmosDb (#3596)
Transports
- RabbitMQ: deliveries are settled against the channel they arrived on (#3687). Acking a delivery on a torn-down channel threw a
NullReferenceException - NATS: auto-provisioned JetStream durable consumers are filtered to their own subject (#3676).
FilterSubjectwas only assigned whenConsumerNamewas empty, so every durable consumer on a stream received every message. The fix needs aFilterSubjectsmulti-filter — a single filter cannot cover both{subject}and{subject}.scheduled, and a work-queue stream discards an uncovered control message - MQTT: the v5 authentication method name is configurable (#3588). It was hardcoded to
"OAUTH2-JWT". Azure Event Grid's custom JWT authentication requiresCUSTOM-JWT, so those brokers could not be reached through Wolverine's authentication support at all. You could already set the method by hand throughMqttClientOptionsBuilder.WithAuthentication(), but that gave up Wolverine's token refresh loop — the whole reason to useMqttJwtAuthenticationOptions. You no longer have to choose - The HTTP transport can send to a destination nobody pre-registered (#3681, reported as ProductSupport#34).
WolverineHttpTransportClientused the endpoint'sOutboundUripurely as anIHttpClientFactoryclient name, then posted to that client'sBaseAddress— so operator commands sent back over the HTTP transport failed withAn invalid request URI was provided
Performance
-
RabbitMQ consumer dispatch concurrency is now per-endpoint (#3492). The client default of 1 was the bottleneck. Simulated handler, 2,000 msg/s offered load, 30s measured window:
ConsumerDispatchConcurrencyThroughput Transit p50 1 (client default) 163.7/s — (nothing from the measured window was consumed before the run ended) 5 828/s 22,871.9 ms 20 1,999.1/s 1.486 ms (p95 2.54, p99 3.22) The 5.1x and 12.2x multiples understate it — at 1 and 5 the listener never catches up at all.
-
Amazon SQS batches message deletions and chunks outgoing batches on the 256KB request size limit (#3493)
-
Azure Service Bus session listeners are no longer quadratic — the n² session loops are now n.
MaxConcurrentCallsis surfaced, and a batched defer settles the original message (#3494)
HTTP and gRPC
codegen writeemitted an empty file — a bare namespace with no type — for every gRPC chain (#3692), which broke proto-first codegen
Fixes from the community
- CosmosDb: removed a spurious
CancellationTokendependency fromTransactionalFrame(#3685)
Dependencies
- Weasel 9.19.0 → 9.20.0 (required by the partition bucketing fix; the root cause was in Weasel, fixed by JasperFx/weasel#392)
- No JasperFx, Marten or Polecat bumps this cycle
Contributors
Thank you to everyone who contributed to this release:
- @thechucklingatom for the CosmosDb
TransactionalFramefix (#3685) - @sherif3hassan for reporting #3676 — the root-cause analysis in that issue was correct as written, which made the fix a great deal faster
- @radcki for reporting #3588 and identifying the
WithAuthentication()workaround - @ziaxdk for reporting #3692
Full Changelog: V6.23.1...V6.24.0