Repository navigation
Default envelope ids no longer collide between processes on one machine - #5399
Merged
Merged
Conversation
EnvelopeIdGeneration.NewId and NewIdSqlServerOrder used the static MassTransit NewId, which builds an id from the machine's network identity, a tick and a sequence each process counts on its own. Two Wolverine processes on one machine therefore drew identical ids: 9,233 of 200,000 shared in a two-process probe. Found by an out-of-process chaos rig (SIGKILL / SIGTERM of workers and the producer, Marten outbox, PostgreSQL queue and RabbitMQ, ledger check). In a shared inbox a collision showed up as - 23505 on pkey_wolverine_incoming_envelopes_id when a handler cascaded, so the handler failed and the message was dead-lettered (the RabbitMQ native DLQ, invisible in the database), - "Discarding a duplicate delivery ... which the inbox already holds" for a DIFFERENT message, silently dropped by inbox deduplication, - the PostgreSQL queue pop rolling its whole batch back. Losses per run: 4 of 18,770 (PostgreSQL queue), 10 of 22,160 (RabbitMQ). With this change the same seeds ran clean: 0 lost, 0 duplicates, 0 duplicate-key errors. Wolverine now owns a NewIdGenerator with NewId's own tick and worker id providers plus CurrentProcessIdProvider (EnvelopeIds). It does not call NewId.SetProcessIdProvider, so ids anything else in the process draws from NewId are unchanged. Ordering is unchanged: still time ordered for .NET / PostgreSQL (NewId) and for SQL Server (NewIdSqlServerOrder). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 6, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
EnvelopeIdGeneration.NewId(the default) andNewIdSqlServerOrderused the static MassTransitNewId. It builds an id from the machine's network identity, a tick, and a sequence number each process counts on its own, and Wolverine registered no process id provider. So two Wolverine processes on one machine draw identical envelope ids: in a two-process probe on one machine, 9,233 of 200,000 ids were shared. With a process id provider, none were.In a shared inbox, under load, a collision shows up three ways. All of them look like durability bugs:
pkey_wolverine_incoming_envelopes_idwhen a handler cascades. The handler's transaction fails and the message is dead-lettered. On RabbitMQ it goes to the broker's dead-letter queue, so it is invisible in the database.Discarding a duplicate delivery of envelope X ... which the inbox already holdsfor a different message, which inbox deduplication then drops silently.This could account for some of the hard-to-reproduce data-loss reports, for example #4162 (RabbitMQ, SIGKILL, durable inbox, no inbox row), but that is not confirmed against any specific report.
How it was found
An out-of-process chaos rig. A producer commits a ledger row together with its
Workmessage through the Marten outbox. Three to five workers in separate processes handleWork, which can cascade to a durable local queue, schedule a follow-up, and fail on its first attempt so it gets a scheduled retry. The driver SIGKILLs or SIGTERMs a random process every few seconds and restarts it, lets the cluster settle, then checks for messages that were lost, processed twice, or left stuck in the inbox, outbox or dead letters. The rig is not part of this PR.A longer run with the same workaround applied in the rig (5 workers, 5 minutes, 48 SIGKILLs, 52 graceful stops, about 98k messages over both transports) was also clean.
The fix
EnvelopeIdsis Wolverine's ownNewIdGenerator. It uses NewId's own tick and worker id providers, plusCurrentProcessIdProvider. It deliberately does not callNewId.SetProcessIdProvider(), which is global and would change the ids MassTransit or application code in the same process draw fromNewId. Ordering is unchanged: ids are still time ordered for .NET and PostgreSQL (NewId), and for SQL Server (NewIdSqlServerOrder).Inside containers the app is usually PID 1 everywhere, so the process id does not separate instances that share a network identity. Those normally have distinct MACs anyway, and
GuidV7remains the documented choice for them.Tests
the_default_ids_differ_between_two_processes_drawing_them_at_the_same_moment: two generators at a fixed tick overlap without process ids (the bug, reproduced deterministically) and do not overlap with distinct process ids.default_ids_are_unique_across_threads.typeof(NewId)now point atEnvelopeIds: CoreTests, SqlServerTests and PolecatTests.Run locally: the CoreTests id-generation classes (19), the SqlServerTests (7) and PolecatTests (2, net10.0) id-order tests, and
dotnet build wolverine.slnx -c Release -f net9.0. Not run locally: the full CoreTests suite.🤖 Generated with Claude Code