1

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);
    }
}
Pavel Voronin
  • 13,503
  • 7
  • 71
  • 137
  • Just a guess, but could be because the gRPC services are transient (I think thats the default way) on the server side? So whenever you send a call to a gRPC service, a new instance of that service is instantiated on the server -> new header? I think you are still reusing the gRPC channel, so in theory you are doing everything right? – dan-kli Aug 17 '23 at 07:39
  • @dan-kli, but this issue is in the client side. Interceptor adds multiple headers. – Pavel Voronin Aug 17 '23 at 12:21
  • Yes, but my point was when the server creates a new service instance for each call, a new header will be there on the client side for each call? If that makes sense? – dan-kli Aug 17 '23 at 12:53
  • I am not getting it,TBH. The header is set by the client for Client -> Server calls. – Pavel Voronin Aug 17 '23 at 14:02
  • Yes, and you need a new header for each call, then the call arrives on the server and creates a new service instance. So it makes sense that a new header is created, right? Why do you expect to only have one header when you reuse a gRPC channel? – dan-kli Aug 18 '23 at 10:01
  • > So it makes sense that a new header is created, right? Created where? I've added the code of the interceptor. – Pavel Voronin Aug 18 '23 at 12:13
  • You asked the following: *At least `context.Options.Headers` keeps growing, so after several calls it contains the same header several times. Why?* Each call creates a new header that is intercepted by your interceptor, and the interceptor is registered for a single gRPC channel in which all the calls take place (as a singleton). Why would you expect anything else to happen than the amount of headers increasing with each call? – dan-kli Aug 18 '23 at 12:29
  • Also sorry for my first comment, it (most likely) does not matter at all if the services are transient or not on the server side. – dan-kli Aug 18 '23 at 12:31

0 Answers0