What is the lifetime/scope of ClientInterceptorContext in .NET?

143 Views Asked by At

I have a .NET gRPC client on .net 4.6.2 using Grpc.Core library.

I've registered gRPC channel as singleton to reuse it following [this recommendation].(https://learn.microsoft.com/en-us/aspnet/core/grpc/performance?view=aspnetcore-7.0#reuse-grpc-channels)

There's how I create an instance of the channel:

public static InvokerAndChannel<T> CreateGrpcCallInvokerAndChannel<T>(
    string endpoint, ChannelCredentials credentials, IServiceProvider serviceProvider)
    where T : class
{
    var licenseCode = serviceProvider.GetRequiredService<ILicenseCodeResolver>().GetLicenseCode();

    var channel = new Channel(endpoint, credentials, new ChannelOption(ChannelOptions.MaxSendMessageLength, "1mb").Yield());
    var callInvoker = channel
        .Intercept(
            new ClientTracingInterceptor(
                new ClientTracingInterceptorOptions { RecordMessageEvents = false }))
        .Intercept(new StaticMetadataInterceptor(licenseCode));

    return new InvokerAndChannel<T>(callInvoker, channel);
}

As you can see the pipeline includes StaticMetadataInterceptor which adds license header to the call. Effectively it's also a singleton.

I have multiple parallel calls to various methods of the same gRPC service. And I see that StaticMetadataInterceptor receives same ClientInterceptorContext. At least context.Options.Headers keeps growing, so after several calls it contains the same header several times. Why?

Here's the code for the interceptor:

internal sealed class StaticMetadataInterceptor : Interceptor
{
    private readonly string _licenseCode;

    public StaticMetadataInterceptor(string licenseCode)
    {
        _licenseCode = licenseCode;
    }

    private void ExtendMetadata<TRequest, TResponse>(ref ClientInterceptorContext<TRequest, TResponse> context)
            where TRequest : class
            where TResponse : class
    {
        var metadata = context.Options.Headers;
        if (metadata == null)
        {
            metadata = new Metadata();
            context = new ClientInterceptorContext<TRequest, TResponse>(context.Method, context. Host, context.Options.WithHeaders(metadata));
        }

        metadata.Add(GrpcStandardMetadataKeys.License, _licenseCode);
    }

    public override TResponse BlockingUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, BlockingUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(request, context);
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(request, context);
    }

    public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(context);
    }

    public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(request, context);
    }

    public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(context);
    }
}
1

There are 1 best solutions below

0
Pavel Voronin On

I found the reason, why headers collection kept growing.

Generated Grpc client has an optional parameter of Metada type. My assumption was that items from this collection will be copied, and if interceptor adds headers to the outgoing request, initial metadata won't be affected. That assumption was wrong.

Example:

var metadata = new Metadata();
client.CallFoo(request, metadata);

If interceptor adds headers to request, metadata will be changed.