解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
6.00 KiB
X
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using NewLife.Log;
using NewLife.Net;
using Xunit;

namespace XUnitTest.Net;

/// <summary>服务端接受连接失败时的会话回滚测试</summary>
/// <remarks>
/// 会话入集合后,业务回调(CreateHandler/Init/Connected)或会话启动(SSL认证、启动接收环)抛异常时,
/// 会话必须立即移出集合并释放;否则它会一直留在集合里、Disconnected 永不触发、连接悬挂到会话超时。
/// </remarks>
[Collection("Net.B")]
public class TcpServerAcceptRollbackTests
{
    #region 工具
    private static async Task WaitUntilAsync(Func<Boolean> condition, Int32 timeoutMs = 10_000)
    {
        var sw = Stopwatch.StartNew();
        while (!condition())
        {
            if (sw.ElapsedMilliseconds > timeoutMs) throw new TimeoutException("等待条件超时");

            await Task.Delay(10);
        }
    }

    /// <summary>断言客户端立即感知连接关闭(读到流结束,或对端重置连接)</summary>
    private static async Task AssertClosedAsync(TcpClient client)
    {
        using var cts = new CancellationTokenSource(3_000);
        var buffer = new Byte[1];
        var closed = false;
        try
        {
            closed = await client.GetStream().ReadAsync(buffer, cts.Token) == 0;
        }
        catch (OperationCanceledException) { }
        catch (IOException) { closed = true; }

        Assert.True(closed, "会话释放后客户端应立即感知连接关闭");
    }
    #endregion

    /// <summary>新会话事件抛异常时,会话被移出集合并释放,客户端立即感知连接关闭</summary>
    [Fact(DisplayName = "接受回滚_新会话事件抛异常_会话移出集合并释放")]
    public async Task NewSessionThrows_SessionRolledBack()
    {
        using var server = new TcpServer { Port = 0, Log = XTrace.Log };

        ISocketSession? session = null;
        server.NewSession += (s, e) =>
        {
            session = e.Session;
            throw new InvalidOperationException("业务拒绝该连接");
        };
        server.Start();

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, server.Port);

        // 等回滚完成:会话已释放,且它已经从会话集合里消失
        await WaitUntilAsync(() => session != null && session.Disposed && server.Sessions.Count == 0);

        Assert.True(session!.Disposed);
        Assert.Empty(server.Sessions);

        // 会话释放后连接必须真的断开
        await AssertClosedAsync(client);
    }

    /// <summary>首个连接被业务拒绝后,服务端仍能正常接受并处理后续连接</summary>
    [Fact(DisplayName = "接受回滚_异常连接被拒后_后续连接仍可正常收发")]
    public async Task NewSessionThrows_ServerStillAccepts()
    {
        using var server = new TcpServer { Port = 0, Log = XTrace.Log };

        var count = 0;
        ISocketSession? failed = null;
        var received = new TaskCompletionSource<Byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);

        server.NewSession += (s, e) =>
        {
            if (Interlocked.Increment(ref count) == 1)
            {
                failed = e.Session;
                throw new InvalidOperationException("业务拒绝首个连接");
            }

            e.Session.Received += (ss, ee) => received.TrySetResult(ee.GetBytes());
        };
        server.Start();

        // 首个连接触发业务异常
        using (var client1 = new TcpClient())
        {
            await client1.ConnectAsync(IPAddress.Loopback, server.Port);
            await WaitUntilAsync(() => failed != null && failed.Disposed && server.Sessions.Count == 0);
        }

        // 后续连接必须照常建立并收发数据
        using var client2 = new TcpClient();
        await client2.ConnectAsync(IPAddress.Loopback, server.Port);

        await WaitUntilAsync(() => server.Sessions.Count == 1);

        var active = server.Sessions.Values.First();
        Assert.False(active.Disposed);

        var payload = "hello after rollback"u8.ToArray();
        await client2.GetStream().WriteAsync(payload);

        var got = await received.Task.WaitAsync(TimeSpan.FromSeconds(5));
        Assert.Equal(payload, got);
    }

    /// <summary>创建处理器抛异常时,网络会话与底层连接都必须被回收</summary>
    [Fact(DisplayName = "接受回滚_创建处理器抛异常_网络会话与连接均被回收")]
    public async Task CreateHandlerThrows_SessionAndConnectionReleased()
    {
        using var server = new ThrowingHandlerServer { Port = 0, ProtocolType = NetType.Tcp, Log = XTrace.Log };
        server.Start();

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, server.Port);

        // 等回滚完成:网络会话已释放,网络层与Socket层的会话集合、会话计数全部归零
        await WaitUntilAsync(() => server.Created != null && server.Created.Disposed
            && server.Sessions.Count == 0 && server.SessionCount == 0
            && server.Servers[0].Sessions.Count == 0);

        Assert.True(server.Created!.Disposed);
        Assert.Empty(server.Sessions);
        Assert.Equal(0, server.SessionCount);
        Assert.Empty(server.Servers[0].Sessions);

        // 连接必须真的断开
        await AssertClosedAsync(client);
    }

    /// <summary>创建网络处理器的动作会抛异常的服务端,用于验证会话启动失败的回滚</summary>
    private sealed class ThrowingHandlerServer : NetServer
    {
        /// <summary>最近一次创建的网络会话</summary>
        public INetSession? Created;

        public override INetHandler? CreateHandler(INetSession session) => throw new InvalidOperationException("测试:创建网络处理器失败");

        protected override INetSession CreateSession(ISocketSession session)
        {
            var ns = base.CreateSession(session);
            Created = ns;

            return ns;
        }
    }
}