diff --git a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpTransportTests.cs b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpTransportTests.cs index 615a5daf88..1b2c9663e6 100644 --- a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpTransportTests.cs +++ b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpTransportTests.cs @@ -71,6 +71,50 @@ public Task RequestResponse(bool useSsl) return RequestResponseTest(CreateServer, CreateClient); } + [Fact] + public async Task ConnectionClosedAfterTlsHandshake() + { + // backlog 1: the timeout sources come from a pool that keeps one + var serverAddr = new IPEndPoint(IPAddress.Loopback, 3790); + await using var server = new TcpServer(serverAddr, 1, CreateLocalMember(), NullLoggerFactory.Instance) + { + ReceiveTimeout = TimeSpan.FromMilliseconds(300), + MemoryAllocator = MemoryAllocator.Default, + TransmissionBlockSize = 65535, + GracefulShutdownTimeout = 2000, + SslOptions = CreateServerSslOptions() + }; + await server.StartAsync(TestToken); + + // A completes the TLS handshake and sends nothing: the handshake's timeout source goes back to the pool + using var a = new System.Net.Sockets.Socket(serverAddr.AddressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + await a.ConnectAsync(serverAddr, TestToken); + await using var tls = new SslStream(new System.Net.Sockets.NetworkStream(a, ownsSocket: false)); + await tls.AuthenticateAsClientAsync(CreateClientSslOptions(), TestToken); + + // B never starts its handshake: it takes the same source from the pool, times out, and the source is disposed + using var b = new System.Net.Sockets.Socket(serverAddr.AddressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + await b.ConnectAsync(serverAddr, TestToken); + Equal(0, await b.ReceiveAsync(new byte[1], TestToken)); // closed by the server after the timeout + + // A leaves before its first message: the connection must not return the handshake's source to the pool again + // (it threw ObjectDisposedException from async void HandleConnection, which ended the process) + a.Shutdown(System.Net.Sockets.SocketShutdown.Both); + a.Close(); + await Task.Delay(500, TestToken); + + // the server still answers + using var client = new TcpClient(CreateLocalMember(), serverAddr) + { + MemoryAllocator = MemoryAllocator.Default, + RequestTimeout = DefaultTimeout, + ConnectTimeout = DefaultTimeout, + TransmissionBlockSize = 65535, + SslOptions = CreateClientSslOptions(), + }; + True((await client.As().VoteAsync(42L, 1L, 56L, TestToken)).Value); + } + [Fact] public Task StressTest() { diff --git a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/TransportTestSuite.cs b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/TransportTestSuite.cs index 85542118b6..275a1ec8ca 100644 --- a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/TransportTestSuite.cs +++ b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/TransportTestSuite.cs @@ -168,6 +168,8 @@ ValueTask> ILocalMember.PreVoteAsync(ClusterMemberId sende int ILocalMember.Version => 0; } + private protected static ILocalMember CreateLocalMember() => new LocalMember(); + private protected delegate IServer ServerFactory(ILocalMember localMember, EndPoint address, TimeSpan timeout); private protected delegate RaftClusterMember ClientFactory(EndPoint address, ILocalMember localMember, TimeSpan timeout); diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpServer.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpServer.cs index d34efc2532..e8fb359683 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpServer.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpServer.cs @@ -113,6 +113,8 @@ private async void HandleConnection(Socket remoteClient) await timeoutSource.DisposeAsync().ConfigureAwait(false); } + // returned to the pool above: the finally below must not return it again + timeoutSource = default; protocol = new(ssl, MemoryAllocator, transmissionBlockSize); }