Cysharp / MagicOnion

Unified Realtime/API framework for .NET platform and Unity.
MIT License
3.68k stars 417 forks source link

Memory leak when reusing GrpcChannel on many clients #754

Closed Alezy80 closed 1 month ago

Alezy80 commented 2 months ago

The next example not real production code, but written to reproduce memory leak faster. I have one GRPC channel to which I've connect some clients like this:

private static async Task Main(string[] args)
{
    // Connect to the server using gRPC channel.
    var channel = GrpcChannel.ForAddress("http://localhost:5000");
    var ci = channel.CreateCallInvoker();
    while (true)
    {
        var client = MagicOnionClient.Create<IMyFirstService>(ci);
        var result = await client.SumAsync(123, 456);
        Debug.Assert(result == 123 + 456);
    }
}

GRPC channel not recreated, because it stated

Client objects can reuse the same channel. Creating a channel is an expensive operation

Clients are cached in many situations, but sometimes I need to create a new client with custom headers or set default timeout. Each client adds many objects to GrpcChannel. After a few seconds of client running the many new objects are created:

изображение

Moving GrpcChannel creation into while cycle solves leakage problem, but at the cost of performance.

Project for reproduce error with client and server: MagicOnionLeak.zip

mayuki commented 2 months ago

This problem is caused by the existence of a cache of methods inside GrpcChannel. MagicOnionClient.Create generates a method each time it creates a client for the purpose of binding a serializer/marshaller.

MagicOnionClient has a method called WithOptions that returns a client with different options only. This client shares the same methods as the original client.

private static async Task Main(string[] args)
{
    // Connect to the server using gRPC channel.
    var channel = GrpcChannel.ForAddress("http://localhost:5000");
    var ci = channel.CreateCallInvoker();
    var client = MagicOnionClient.Create<IMyFirstService>(ci); // <-- Create a client only once.
    while (true)
    {
        var clientWithOptions = client.WithOptions(...); // <-- Create a client with new options here.
        var result = await clientWithOptions.SumAsync(123, 456);
        Debug.Assert(result == 123 + 456);
    }
}