RPC远程过程调用,二进制封装,提供高吞吐低延迟的高性能RPC框架
大石头 authored at 2022-08-10 13:26:19
5.33 KiB
NewLife.Remoting
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NewLife;
using NewLife.Log;
using NewLife.Messaging;
using NewLife.Remoting;
using Xunit;

namespace XUnitTest.Integration;

/// <summary>
/// ApiClient 接收事件回归测试。
/// 语义约定:ApiClient.Received 面向数据监视,与底层 socket Received 一致——请求与响应
/// (含被 SendMessageAsync 匹配消费的响应)一律触发;同时解码产物 ApiMessage.Data 独立克隆,
/// 保证与 SendMessageAsync 等待方共享同一消息负载时,并发解码不产生 ArrayPool 脏读。
/// 修复对象:ApiServer搜索 等靠 Received 事件收集应答的场景(曾因拦截 Reply 收不到任何应答)。
/// </summary>
public class ApiClientReceivedEventTests : DisposeBase
{
    private readonly ApiServer _Server;
    private readonly Int32 _Port;

    public ApiClientReceivedEventTests()
    {
        _Server = new ApiServer(TestPort.PickDual())
        {
            Log = XTrace.Log,
            ShowError = true,
            ReuseAddress = true,
        };
        _Server.Start();

        _Port = _Server.Port;
    }

    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);
        _Server.TryDispose();
    }

    private ApiClient CreateClient()
    {
        var client = new ApiClient($"tcp://127.0.0.1:{_Port}");
        client.Open();
        return client;
    }

    [Fact(DisplayName = "单播 InvokeAsync 的响应同时经 Received 事件可见且数据完整")]
    public async Task MatchedReply_VisibleInReceived()
    {
        using var client = CreateClient();

        var got = new TaskCompletionSource<Boolean>(TaskCreationOptions.RunContinuationsAsynchronously);
        String? action = null;
        Object? decoded = null;
        var count = 0;
        client.Received += (s, e) =>
        {
            Interlocked.Increment(ref count);
            if (e.Message is IMessage imsg && imsg.Reply)
            {
                action = e.ApiMessage?.Action;
                // ApiMessage 仅在回调内有效,须在回调内同步解码,验证共享负载未被提前归还
                if (e.ApiMessage?.Data != null)
                    decoded = client.Encoder.DecodeResult(action!, e.ApiMessage.Data, e.Message, typeof(Object));

                got.TrySetResult(true);
            }
        };

        var rs = await client.InvokeAsync<Object>("Api/Info");
        Assert.NotNull(rs);

        var completed = await Task.WhenAny(got.Task, Task.Delay(3_000));
        Assert.True(completed == got.Task, "被匹配消费的响应未上抛 Received 事件");

        Assert.Equal("Api/Info", action);
        Assert.NotNull(decoded);
        Assert.Equal(1, Volatile.Read(ref count));
    }

    [Fact(DisplayName = "只发不等的请求,应答作为未匹配响应经 Received 事件可见")]
    public async Task UnmatchedReply_VisibleInReceived()
    {
        using var client = CreateClient();

        var got = new TaskCompletionSource<Boolean>(TaskCreationOptions.RunContinuationsAsynchronously);
        String? action = null;
        client.Received += (s, e) =>
        {
            if (e.Message is IMessage imsg && imsg.Reply)
            {
                action = e.ApiMessage?.Action;
                got.TrySetResult(true);
            }
        };

        // 只发不等:不注册挂起请求(模拟 ApiServer搜索 的广播探测),服务端应答即为未匹配响应
        using var req = client.Encoder.CreateRequest("Api/Info", null);
        var socket = client.Cluster.Get();
        try
        {
            socket.SendMessage(req);
        }
        finally
        {
            client.Cluster.Return(socket);
        }

        var completed = await Task.WhenAny(got.Task, Task.Delay(3_000));
        Assert.True(completed == got.Task, "未收到未匹配的服务端应答");
        Assert.Equal("Api/Info", action);
    }

    [Fact(DisplayName = "并发多请求的响应均经 Received 事件可见且无脏读")]
    public async Task ConcurrentReplies_VisibleInReceived()
    {
        using var client = CreateClient();

        var got = new ConcurrentQueue<String>();
        var done = new TaskCompletionSource<Boolean>(TaskCreationOptions.RunContinuationsAsynchronously);
        client.Received += (s, e) =>
        {
            if (e.Message is IMessage imsg && imsg.Reply)
            {
                // 回调内同步解码,验证共享负载完整可用
                if (e.ApiMessage?.Data != null)
                    client.Encoder.DecodeResult(e.ApiMessage.Action, e.ApiMessage.Data, e.Message, typeof(Object));

                got.Enqueue(e.ApiMessage?.Action ?? "");
                if (got.Count >= 10) done.TrySetResult(true);
            }
        };

        // 10 个并发请求复用同一连接,按序列号匹配各自响应
        var tasks = new List<Task<Object?>>();
        for (var i = 0; i < 10; i++)
            tasks.Add(client.InvokeAsync<Object>("Api/Info"));
        await Task.WhenAll(tasks);

        var completed = await Task.WhenAny(done.Task, Task.Delay(5_000));
        Assert.True(completed == done.Task, "并发响应未全部经 Received 事件可见");

        Assert.Equal(10, got.Count);
        Assert.All(got, action => Assert.Equal("Api/Info", action));
    }
}