Skip to content

Commit b04761e

Browse files
authored
PhysicalBridge: Fix orphaning cases of PhysicalConnection where it never finishes (#2338)
What happens here is that `PhysicalConnection` attempts to connect but never hears back, so it's stuck in `State.ConnectedEstablishing` state, which is handled in the heartbeat code. In the situation where the "last write seconds ago" passes in the heartbeat and we haven't heard back, we fire an `PhysicalBridge.OnDisconnected()` which clears out `physical` and orphans the socket listening forever. This now properly disposes of that `PhysicalConnection` mirroring like we do in `State.Connecting` which will properly fire `OnDisconnected()` and clean up the orphan socket. The situation manifests where we establish a TCP connection, but not a Redis connection. All of the cases in the memory dump we're analyzing are some bytes sent and 0 received. Likely a Redis server issue, but the client is then handling it incorrectly and leaking. I nuked the unused `RemovePhysical` method just to prevent further oops here. Addresses a new case of #1458.
1 parent c4e5453 commit b04761e

2 files changed

Lines changed: 9 additions & 5 deletions

File tree

docs/ReleaseNotes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ Current package versions:
88

99
## Unreleased
1010

11-
- Internal: revisit endpoint-snapshot implementation
11+
- Fix [#1458](https://github.com/StackExchange/StackExchange.Redis/issues/1458): Fixes a leak condition when a connection completes on the TCP phase but not the Redis handshake ([#2238 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2238))
12+
- Internal: ServerSnapshot: Improve API and allow filtering with custom struct enumerator ([#2337 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2337))
1213

1314

1415
## 2.6.86

src/StackExchange.Redis/PhysicalBridge.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -542,11 +542,16 @@ internal void OnHeartbeat(bool ifConnectedOnly)
542542
// abort and reconnect
543543
var snapshot = physical;
544544
OnDisconnected(ConnectionFailureType.UnableToConnect, snapshot, out bool isCurrent, out State oldState);
545-
using (snapshot) { } // dispose etc
545+
snapshot?.Dispose(); // Cleanup the existing connection/socket if any, otherwise it will wait reading indefinitely
546546
TryConnect(null);
547547
}
548548
break;
549549
case (int)State.ConnectedEstablishing:
550+
// (Fall through) Happens when we successfully connected via TCP, but no Redis handshake completion yet.
551+
// This can happen brief (usual) or when the server never answers (rare). When we're in this state,
552+
// a socket is open and reader likely listening indefinitely for incoming data on an async background task.
553+
// We need to time that out and cleanup the PhysicalConnection if needed, otherwise that reader and socket will remain open
554+
// for the lifetime of the application due to being orphaned, yet still referenced by the active task doing the pipe read.
550555
case (int)State.ConnectedEstablished:
551556
var tmp = physical;
552557
if (tmp != null)
@@ -576,6 +581,7 @@ internal void OnHeartbeat(bool ifConnectedOnly)
576581
else
577582
{
578583
OnDisconnected(ConnectionFailureType.SocketFailure, tmp, out bool ignore, out State oldState);
584+
tmp.Dispose(); // Cleanup the existing connection/socket if any, otherwise it will wait reading indefinitely
579585
}
580586
}
581587
else if (writeEverySeconds <= 0 && tmp.IsIdle()
@@ -614,9 +620,6 @@ internal void OnHeartbeat(bool ifConnectedOnly)
614620
}
615621
}
616622

617-
internal void RemovePhysical(PhysicalConnection connection) =>
618-
Interlocked.CompareExchange(ref physical, null, connection);
619-
620623
[Conditional("VERBOSE")]
621624
internal void Trace(string message) => Multiplexer.Trace(message, ToString());
622625

0 commit comments

Comments
 (0)