using System.Collections.Concurrent; using System.Net; using Esiur.Core; using Esiur.Data; using Esiur.Net; using Esiur.Net.Sockets; namespace Esiur.Tests.Unit; public class NetworkLifecycleConcurrencyTests { [Fact] public async Task NetworkConnection_DrainsDistinctBuffersQueuedWhileParserIsBusy() { using var parserEntered = new ManualResetEventSlim(); using var releaseParser = new ManualResetEventSlim(); var connection = new BlockingConnection(parserEntered, releaseParser); var socket = new TestSocket(); connection.Assign(socket); var firstBuffer = BufferWith(1); var secondBuffer = BufferWith(2); var firstReceive = Task.Run(() => connection.NetworkReceive(socket, firstBuffer)); try { Assert.True(parserEntered.Wait(TimeSpan.FromSeconds(2))); var secondReceive = Task.Run(() => connection.NetworkReceive(socket, secondBuffer)); Assert.Same(secondReceive, await Task.WhenAny( secondReceive, Task.Delay(TimeSpan.FromSeconds(2)))); releaseParser.Set(); var bothReceives = Task.WhenAll(firstReceive, secondReceive); Assert.Same(bothReceives, await Task.WhenAny( bothReceives, Task.Delay(TimeSpan.FromSeconds(2)))); await bothReceives; Assert.Equal(new byte[] { 1, 2 }, connection.Received.ToArray()); Assert.Equal(0u, secondBuffer.Available); } finally { releaseParser.Set(); await firstReceive; } } [Fact] public async Task NetworkConnection_DrainsNewSocketBufferAfterReplacementDuringReceive() { using var parserEntered = new ManualResetEventSlim(); using var releaseParser = new ManualResetEventSlim(); var connection = new BlockingConnection(parserEntered, releaseParser); var oldSocket = new TestSocket(); var newSocket = new TestSocket(); connection.Assign(oldSocket); var firstReceive = Task.Run(() => connection.NetworkReceive(oldSocket, BufferWith(1))); try { Assert.True(parserEntered.Wait(TimeSpan.FromSeconds(2))); var replacementBuffer = BufferWith(2); var replacementReceive = Task.Run(() => { Assert.Same(oldSocket, connection.Unassign()); connection.Assign(newSocket); connection.NetworkReceive(newSocket, replacementBuffer); }); Assert.Same(replacementReceive, await Task.WhenAny( replacementReceive, Task.Delay(TimeSpan.FromSeconds(2)))); releaseParser.Set(); var bothReceives = Task.WhenAll(firstReceive, replacementReceive); Assert.Same(bothReceives, await Task.WhenAny( bothReceives, Task.Delay(TimeSpan.FromSeconds(2)))); await bothReceives; Assert.Equal(new byte[] { 1, 2 }, connection.Received.ToArray()); Assert.Equal(0u, replacementBuffer.Available); } finally { releaseParser.Set(); await firstReceive; } } [Fact] public void NetworkConnection_IgnoresCallbacksFromUnassignedSocket() { var connection = new RecordingConnection(); var staleSocket = new TestSocket(); var currentSocket = new TestSocket(); var connectEvents = 0; var closeEvents = 0; connection.OnConnect += _ => connectEvents++; connection.OnClose += _ => closeEvents++; connection.Assign(staleSocket); Assert.Same(staleSocket, connection.Unassign()); connection.Assign(currentSocket); var staleBuffer = BufferWith(9); connection.NetworkReceive(staleSocket, staleBuffer); connection.NetworkConnect(staleSocket); connection.NetworkClose(staleSocket); Assert.Equal(1u, staleBuffer.Available); Assert.Empty(connection.Received); Assert.Equal(0, connection.ConnectedCalls); Assert.Equal(0, connection.DisconnectedCalls); Assert.Equal(0, connectEvents); Assert.Equal(0, closeEvents); connection.NetworkConnect(currentSocket); connection.NetworkReceive(currentSocket, BufferWith(1)); connection.NetworkClose(currentSocket); Assert.Equal(new byte[] { 1 }, connection.Received.ToArray()); Assert.Equal(1, connection.ConnectedCalls); Assert.Equal(1, connection.DisconnectedCalls); Assert.Equal(1, connectEvents); Assert.Equal(1, closeEvents); } [Fact] public void NetworkServer_ClosesSocketAcceptedAfterStopSnapshot() { var acceptedSocket = new TestSocket(); using var listener = new BlockingListener(acceptedSocket); var server = new TestServer(); try { server.Start(listener); Assert.True(listener.AcceptEntered.Wait(TimeSpan.FromSeconds(2))); server.Stop(); listener.ReleaseAccept.Set(); Assert.True(SpinWait.SpinUntil( () => acceptedSocket.State == SocketState.Closed, TimeSpan.FromSeconds(2))); Assert.Empty(server.Connections); Assert.Equal(0, Volatile.Read(ref acceptedSocket.BeginCalls)); Assert.Equal(0, Volatile.Read(ref server.ClientConnectedCalls)); } finally { listener.ReleaseAccept.Set(); server.Destroy(); } } private static NetworkBuffer BufferWith(byte value) { var buffer = new NetworkBuffer(); buffer.Write(new[] { value }); return buffer; } private class RecordingConnection : NetworkConnection { public ConcurrentQueue Received { get; } = new(); public int ConnectedCalls; public int DisconnectedCalls; protected override void DataReceived(NetworkBuffer buffer) { var data = buffer.Read(); if (data == null) return; foreach (var value in data) Received.Enqueue(value); } protected override void Connected() => Interlocked.Increment(ref ConnectedCalls); protected override void Disconnected() => Interlocked.Increment(ref DisconnectedCalls); } private sealed class BlockingConnection : RecordingConnection { private readonly ManualResetEventSlim parserEntered; private readonly ManualResetEventSlim releaseParser; private int blocked; public BlockingConnection( ManualResetEventSlim parserEntered, ManualResetEventSlim releaseParser) { this.parserEntered = parserEntered; this.releaseParser = releaseParser; } protected override void DataReceived(NetworkBuffer buffer) { base.DataReceived(buffer); if (Interlocked.CompareExchange(ref blocked, 1, 0) == 0) { parserEntered.Set(); releaseParser.Wait(TimeSpan.FromSeconds(5)); } } } private sealed class ServerConnection : NetworkConnection { protected override void DataReceived(NetworkBuffer buffer) { } protected override void Connected() { } protected override void Disconnected() { } } private sealed class TestServer : NetworkServer { public int ClientConnectedCalls; protected override void ClientConnected(ServerConnection connection) => Interlocked.Increment(ref ClientConnectedCalls); protected override void ClientDisconnected(ServerConnection connection) { } } private class TestSocket : ISocket { public event DestroyedEvent? OnDestroy; public SocketState State { get; protected set; } = SocketState.Established; public INetworkReceiver Receiver { get; set; } = null!; public IPEndPoint RemoteEndPoint { get; } = new(IPAddress.Loopback, 12345); public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, 54321); public int BeginCalls; public void Close() { if (State == SocketState.Closed) return; State = SocketState.Closed; Receiver?.NetworkClose(this); } public void Destroy() { Close(); OnDestroy?.Invoke(this); OnDestroy = null; } public bool Begin() { Interlocked.Increment(ref BeginCalls); return State == SocketState.Established; } public AsyncReply BeginAsync() => new(Begin()); public AsyncReply Connect(string hostname, ushort port) => new(true); public virtual ISocket Accept() => null!; public AsyncReply AcceptAsync() => new((ISocket)null!); public void Send(byte[] message) { } public void Send(byte[] message, int offset, int length) { } public AsyncReply SendAsync(byte[] message, int offset, int length) => new(true); public void Hold() { } public void Unhold() { } } private sealed class BlockingListener : TestSocket, IDisposable { private readonly ISocket acceptedSocket; private int acceptCalls; public BlockingListener(ISocket acceptedSocket) { this.acceptedSocket = acceptedSocket; State = SocketState.Listening; } public ManualResetEventSlim AcceptEntered { get; } = new(); public ManualResetEventSlim ReleaseAccept { get; } = new(); public override ISocket Accept() { if (Interlocked.Increment(ref acceptCalls) != 1) return null!; AcceptEntered.Set(); ReleaseAccept.Wait(TimeSpan.FromSeconds(5)); return acceptedSocket; } public void Dispose() { ReleaseAccept.Set(); AcceptEntered.Dispose(); ReleaseAccept.Dispose(); } } }