From 0caa1d22940448d50664838d5c8b74285132c2c4 Mon Sep 17 00:00:00 2001 From: mgravell Date: Sat, 10 Oct 2026 12:26:07 +0100 Subject: [PATCH] ConnectionAttemptCompleted: take the sequence number and timestamp together The sequence number was taken with Interlocked.Increment and the timestamp read separately afterwards, so two attempts completing at once - the interactive and subscription connections failing together - could interleave: one takes the earlier sequence, the other takes the later sequence but reads the clock first, and a later SequenceNumber carries an earlier CompletedTimeUtc. Handlers that order outcomes by sequence (as ConnectionAttemptUnitTests.EveryFailedReconnectIsReported does) then see time going backwards. Both are now taken under one lock. The event is rare - once per physical connection attempt - so the lock costs nothing that matters. Found on the v4 branch, where the two connections are dialled concurrently and the test failed about 2 runs in 5. --- .../ConnectionMultiplexer.Events.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs index 91c0497e2..83de215bc 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs @@ -25,7 +25,17 @@ internal void OnConnectionAttemptCompleted( var handler = RawConfig.ConnectionAttemptCompletedHandler; if (handler != null) { - var sequenceNumber = Interlocked.Increment(ref _connectionAttemptSequence); + // the sequence and the timestamp are taken together, so that they agree on the order: taken apart, two + // attempts completing at once (the interactive and subscription connections failing together) could each + // take one first and the other second, and a later sequence number would carry an earlier time + long sequenceNumber; + DateTime completedTimeUtc; + lock (_connectionAttemptSequenceLock) + { + sequenceNumber = ++_connectionAttemptSequence; + completedTimeUtc = DateTime.UtcNow; + } + CompleteAsWorker(new ConnectionAttemptCompletedEventArgs( handler, this, @@ -39,12 +49,13 @@ internal void OnConnectionAttemptCompleted( tlsHostName, serverCertificateCheck, sequenceNumber, - DateTime.UtcNow, + completedTimeUtc, physicalName)); } } private long _connectionAttemptSequence; + private readonly object _connectionAttemptSequenceLock = new(); /// /// Raised whenever a physical connection fails.