Skip to content

Commit d99df3c

Browse files
fakefeikclaude
andcommitted
Close connection on heartbeat OperationTimedOutException, not just SocketException
A heartbeat (OptionsRequest) that times out because the peer stopped responding while the TCP connection stays established was previously ignored, since IdleTimeoutHandler only closed the connection for SocketException. This left connections to a dead-but-still-accepting host open indefinitely once application traffic stopped flowing on them, well past the point the host was already marked DOWN via other connections. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 31ef0c8 commit d99df3c

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

src/Cassandra.IntegrationTests/Core/HeartbeatTests.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Cassandra.IntegrationTests.SimulacronAPI.Models.Logs;
2020
using Cassandra.IntegrationTests.TestBase;
2121
using Cassandra.IntegrationTests.TestClusterManagement.Simulacron;
22+
using Cassandra.SessionManagement;
2223
using Cassandra.Tests;
2324
using NUnit.Framework;
2425

@@ -71,10 +72,55 @@ await TestHelper.RetryAssertAsync(
7172
logs = await _testCluster.GetNodes().First()
7273
.GetQueriesAsync(null, OptionsQueryType).ConfigureAwait(false);
7374
Assert.That(logs.Count, Is.GreaterThan(initialCount));
74-
},
75+
},
7576
500,
7677
20).ConfigureAwait(false);
7778
}
7879
}
80+
81+
[Test]
82+
public async Task Connection_Should_Be_Closed_When_Heartbeat_Times_Out_With_Socket_Still_Open()
83+
{
84+
var builder = ClusterBuilder()
85+
.WithPoolingOptions(PoolingOptions.Create()
86+
.SetHeartBeatInterval(2000)
87+
.SetCoreConnectionsPerHost(HostDistance.Local, 1)
88+
.SetMaxConnectionsPerHost(HostDistance.Local, 1))
89+
.WithSocketOptions(new SocketOptions()
90+
.SetReadTimeoutMillis(2000)
91+
.SetDefunctReadTimeoutThreshold(int.MaxValue))
92+
.AddContactPoint(_testCluster.InitialContactPoint);
93+
94+
using (var cluster = builder.Build())
95+
{
96+
var session = await cluster.ConnectAsync().ConfigureAwait(false);
97+
await session.ExecuteAsync(new SimpleStatement(HeartbeatTests.Query)).ConfigureAwait(false);
98+
99+
var pool = ((IInternalSession)session).GetPools().Single().Value;
100+
var connection = pool.ConnectionsSnapshot.Single();
101+
Assert.IsFalse(connection.IsDisposed);
102+
103+
// Simulacron keeps the TCP connection ESTABLISHED and just stops reading, so the
104+
// heartbeat fails with OperationTimedOutException rather than SocketException. No
105+
// application requests are sent after this point, so only the heartbeat itself can
106+
// detect the unresponsive host and close the connection.
107+
await _testCluster.PauseReadsAsync().ConfigureAwait(false);
108+
try
109+
{
110+
await TestHelper.RetryAssertAsync(
111+
() =>
112+
{
113+
Assert.IsTrue(connection.IsDisposed);
114+
return Task.CompletedTask;
115+
},
116+
500,
117+
20).ConfigureAwait(false);
118+
}
119+
finally
120+
{
121+
await _testCluster.ResumeReadsAsync().ConfigureAwait(false);
122+
}
123+
}
124+
}
79125
}
80126
}

src/Cassandra/Connections/Connection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ private void IdleTimeoutHandler(object state)
438438
return TaskHelper.Completed;
439439
}
440440
Connection.Logger.Warning("Received heartbeat request exception " + error.Exception.ToString());
441-
if (error.Exception is SocketException)
441+
if (error.Exception is SocketException || error.Exception is OperationTimedOutException)
442442
{
443443
OnIdleRequestException?.Invoke(error.Exception);
444444
}

0 commit comments

Comments
 (0)