Repository navigation
Wolverine 6.39.1
A bug fix release. If you use codegen test as a CI gate, run dead letter queues on any broker, or persist with Oracle, there's something in here for you.
codegen test works again
Since 6.37.0, dotnet run -- codegen test has failed on a clean checkout for any application that has message handlers, with one CS0234 per handler:
CS0234: The type or namespace name 'CreateInvoiceHandler2048194527' does not
exist in the namespace 'Internal.Generated.WolverineHandlers'
A lot of you use codegen test as the PR gate on pre-generated code, so this has been quietly breaking builds for two releases. codegen write followed by dotnet build was unaffected, and so was running in TypeLoadMode.Static, which is why it took a while to surface.
Two changes collided. codegen test compiles 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 test against a project with message handlers, which nothing here did before -- the drift gate runs codegen write and the AOT smoke tests run publish, 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
MoveToErrorQueue always calls CompleteAsync() 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:
- A message dead lettered from a session-specific listener now carries the failure diagnostics that the other three Azure Service Bus listeners attach. That listener was the only one not stamping them. (#4481)
- SQS was deleting a message twice when a requeue was retried. The guard meant to prevent it read a flag that nothing ever set, so it had never once fired. Deleting twice is harmless on SQS, but the flag is also how SQS reports whether it settled anything, which the next fix needs. (#4489)
- If you set
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:
System.InvalidCastException: Unable to cast object of type 'System.Decimal' to type 'System.Int32'
Oracle returns count(*) as a NUMBER, which ODP.NET surfaces as decimal or Int64, and GetFieldValueAsync<int>() is a cast rather than a conversion -- it throws on anything but Int32. 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:
if (billingDbContext is not BillingDbContext billingDbContext) throw ...Both variables took their name from their type, and IFoo/Foo reduces to the same identifier -- so you got CS0128 and nothing compiled. The feature worked only if your abstraction and your DbContext happened to have unrelated names, which is why the tests never caught it. Generated code is unchanged for anyone already in that situation, so there's nothing to regenerate. (#4479)
Marten subscriptions and tenant ids
A Marten store name was being stamped as the tenant id on subscriptions, which is not the same thing and broke appending for single-tenant stores using the default Main tenant. (#4485)