Repository navigation
Wolverine 6.45.0
Wolverine 6.45.0 is mostly a community release. Seven people outside JasperFx shaped it, and the headline feature came in as a contributor's pull request with 17 tests already passing.
Idempotency keys that answer with the first response
6.44 could stop a POST running twice. It could not tell the second caller what the first one created.
[Deduplicated] refuses a repeat with a status code, which is the right answer when the client can go and fetch the resource. It is the wrong answer when the client is the one that lost the connection mid-request and has no idea what id it created. The docs said as much, and said replaying the original response was out of scope.
6.45 adds [DeduplicatedWithResponse], which stores the first response and replays it:
[DeduplicatedWithResponse(DeduplicationScope.User | DeduplicationScope.Endpoint)]
[WolverinePost("/orders")]
public static (OrderCreated, OrderPlaced) Post(PlaceOrder command) { ... }A repeat under the same Idempotency-Key gets the original status — a 201 stays a 201 — along with the body and the Location header. The handler does not run again, and its cascaded messages are not sent again.
Three details worth knowing before you reach for it:
Scope is required, not optional. A claimed key used to be global across every endpoint, tenant and user. That was tolerable when a duplicate was merely refused; it is not tolerable when a stored response is replayed to whoever presents the key. So the attribute will not let you declare it without saying who a key belongs to, and refuses at startup if you try.
The request is compared by a hash of the bytes sent — method, path, query string and body. A repeat that differs gets 422 rather than being answered with a response that does not belong to it. The corollary: a retry has to resend the same bytes, so a client that re-serializes and reorders JSON properties will see a 422.
It is opt-in and provisions its own table. opts.Durability.EnableDeduplicatedResponses = true, a separate wolverine_deduplicated_responses table, and nothing changes for an application that does not ask for it. PostgreSQL, SQL Server, MySQL and SQLite, which covers Marten and EF Core applications. F# endpoints are supported — [Deduplicated] still does not support them.
Three more deduplication fixes landed alongside it. Two cases where a claim outlived a request that did no work: middleware ending the request early with a success code, and a caller hanging up while the request was failing — in the second, the release itself was being cancelled by the caller's own disconnect, so the key survived for the full window. Deduplication keys also now compare the same way on every engine; they previously followed the database's default collation, so Abc and abc were one claim on SQL Server and MySQL but two on PostgreSQL and SQLite.
Thank you
- @uniquelau —
[DeduplicatedWithResponse]itself, the two claim-leak fixes, the collation report, theEnableRelayOfUserNamediagnosis, and a doc correction. Four pull requests and three issues in one release. - @lahma — found that an inline
InvokeAsyncretry discarded the failed attempt's outgoing messages, and fixed it. - @thechucklingatom — every application pointed at the same CosmosDB database joined one Wolverine cluster, so a durability agent could recover another application's envelopes into a node with no handler for them. Now each application can keep its own container.
- @alesdvorakcz — reported, with Query Store evidence from a failed blue/green deploy, that
ReleaseIncomingAsyncscanned the whole inbox on SQL Server. Measured at 6,748 logical reads before and 3 after. - @faustodc — a deterministic reproduction of a message-loss window in the SQLite durable queue: the dequeue committed before the inbox row was written, so a crash mid-handler lost the message entirely.
- @chrisbbe — a Native AOT crash with a stack trace precise enough to find four more frames with the same defect.
- @jeremad — envelopes executed twice when a node started on a PostgreSQL queue backlog.
And from the core team, @erdtsieck caught the worst regression in 6.44 — every durable-inbox message dead-lettered on partitioned Marten inboxes — and reported it with the exact SQL that QueueSqlCommand was rejecting, which is why it was fixed the same day.
Durability and reliability
- The partitioned mark-as-handled command was two statements joined by
;, which Marten'sQueueSqlCommandrejects — so every durable-inbox message was dead-lettered. (#4736) - A Balanced node is registered before its transports start, so a node starting on a PostgreSQL queue backlog no longer executes envelopes twice. (#4734)
- The SQLite durable dequeue now moves the queue row into the incoming-envelope table in the same transaction, closing a window where a message was in neither place while its handler ran. (#4758)
- Ownership-release statements can use the owner index on SQL Server. A filtered index cannot serve a parameterized
owner_id = @owner, so these were clustered index scans; a blue/green deploy had been failing on the resulting command timeouts. (#4739) - Two more pieces of per-attempt state survived an inline retry: a flushed context swallowed the retry's messages, and a forwarded message could not be retried at all. (#4743)
- A partially-initialized
HttpChaincould never serve a request again — a missed type lookup overwrote an already-resolved handler type with null. (#4749)
Native AOT
Five codegen frames were constructed reflectively and therefore trimmed, crashing an AOT-published application at startup while it built its HTTP chains. They are now constructed directly. Only the first was in the reported stack trace; the others would each have surfaced as the next crash. (#4752)
Also
EnableRelayOfUserNamenow reachesIDocumentSession.LastModifiedByfrom HTTP endpoints. It never did — the relay was emitted after the session was already open, so it was broken for every HTTP endpoint that opened one, not only the session-only ones the report described. (#4741)MetricsOptionscan now tell an explicitly-chosenModefrom the untouched default, so an extension can supply its own default without silently overwriting an application's choice. (#4753)- Store dependency pins reset: JasperFx 2.79.1, Marten 9.45.0, Polecat 5.35.0, Fisher 1.16.0, and all eight Weasel packages aligned on 9.38.0.
Weasel.Storagewas the one package nothing referenced directly and therefore the one the central pin never reached, so the solution had been running it against a newerWeasel.Corethan it was built against.
Upgrading
Durability.EnableMessageDeduplicationis obsolete in favour of a three-value mode —None,CompareByString, orCompareByHash. The boolean still works and means what it always did:falseisNone,trueisCompareByHash.CompareByHashis the engine-independent comparison, and it uses a separate table,wolverine_deduplication_hashed. Nothing migrates and nothing is dropped: the originalwolverine_deduplicationis left exactly as it is, its claims expire on their ownexpiresas usual, and you can drop it by hand once it is empty. Switching back toCompareByStringis therefore lossless in both directions.CompareByStringis byte-for-byte the table and the comparison that ship today, so it is the setting to choose if you would rather change nothing.- Nothing else here requires a configuration change.
[DeduplicatedWithResponse]and its table are opt-in.