Skip to content

C# Interceptor Support#12613

Merged
mehrdada merged 21 commits intogrpc:masterfrom
mehrdada:csharp_interceptors_take_two
Feb 22, 2018
Merged

C# Interceptor Support#12613
mehrdada merged 21 commits intogrpc:masterfrom
mehrdada:csharp_interceptors_take_two

Conversation

@mehrdada
Copy link
Copy Markdown
Contributor

@mehrdada mehrdada commented Sep 18, 2017

This PR adds core interceptor hooks to gRPC C# (both server side and client side).

Addresses #11646
Related proposal grpc/proposal#38

@jtattermusch
Copy link
Copy Markdown
Contributor

CC @jtattermusch

/// <summary>
/// Returns a new instance of <c>AsyncServerStreamingCall</c> that has hooks to intercept on an underlying object of the same type.
/// </summary>
protected static AsyncServerStreamingCall<TResponse> Intercept<TRequest, TResponse>(AsyncServerStreamingCall<TResponse> call,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these .Intercept() methods used anywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are provided for interceptor implementors to be able to intercept additional events in async or streaming calls. The initial interceptor hook is only invoked at the beginning of the call.

These functions are only needed because the constructor for AsyncXYZCall are internal. If we are going to make them public, now would be a good time. I could just remove all of this mess.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd quite like a way of creating an interceptor to just modify/inspect one aspect (e.g. metadata) of all calls - Interceptor.ForMetadata(metadata => { ...})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, and have a few other helper extension methods in mind. I wanted to keep this PR minimal to the core hooks to make it easier to review... now I am thinking it might have been best to add such things for additional context?

if (responseHeaders != null)
{
var callResponseHeadersOriginal = callResponseHeaders; // Ensure it will not be captured by the closure
callResponseHeaders = new Task<Metadata>(() => responseHeaders(callResponseHeadersOriginal));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually unsure what this is supposed to do.

Copy link
Copy Markdown
Contributor Author

@mehrdada mehrdada Oct 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have thought about this a bit more and I think there is no good reason for this set of functions to exist, whose purpose is to circumvent the internal-ness of the constructors of Async***Call objects. If we open up the constructors, this whole thing becomes unnecessary.

What are the downsides to doing that?

Copy link
Copy Markdown
Contributor

@jtattermusch jtattermusch Oct 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downsides:
we'd need to polish the Async*Call constructor signature and be certain that it's not going to change (that would be an API breakage and we are trying hard to avoid that). At the same we would lose some ability to add new members to the Async**Call (what if you need to add more arguments to the constructor later?).
Also, more generally, there is always some risk associated with exposing lower-level APIs to end users, because non-expert users tend to shoot themselves in the foot if you expose too much of the internals - and gRPC users shouldn't be required to become experts before they can use gRPC efficiently.

That said, I think it makes sense to revisit making Async**Call constructors public - now that we have an official support for interceptors coming.

FTR, here's an external PR for publishing the Async**Call constructor - but I think a bit more refactoring would be useful https://github.com/grpc/grpc/pull/9330/files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'd need to polish the Async*Call constructor signature and be certain that it's not going to change

The members are already public, so we cannot remove them and the constructor at the moment is basically initializing the existing members. We will be able to add additional constructors should we add new members (as long as we can find sensible defaults for values). In effect, by exposing this functionality within this PR under the interceptor API, we are transitively doing most of this API retention cost (admittedly it is a bit contained, but still would be breaking). I think we should seriously consider opening them up, since the class itself and its members are already not internal and the InterceptAsync* methods are really ugly in this PR.

I do agree that polishing the constructor now makes sense, but I don't see anything wrong with it. What would be the ideal in your opinion?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's look into publishing the Async**Call constructors in a separate PR (and count on them being already public in this PR).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about not opening the constructors but adding a static factory method that returns a new instance? The advantage would be that we will retain the freedom to unseal the class and return a derived instance without an API breakage, but it is debatable that it would be worth much.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR for publicizing constructors is here: #12877 though I am still open to the static factory route idea.

/// <summary>
/// Intercepts an asynchronous invocation of a simple remote call.
/// </summary>
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request, Func<Method<TRequest, TResponse>, string, CallOptions, TRequest, AsyncUnaryCall<TResponse>> next)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would using a named delegate type be more readable than Func<Method<TRequest, TResponse>, string, CallOptions, TRequest, AsyncUnaryCall<TResponse>>?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think the name "next" can easily be confused as having to do with streaming, so we could come up with a better name (I think naming the delegate would also help here).

Copy link
Copy Markdown
Contributor Author

@mehrdada mehrdada Sep 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impartial about the delegate type making much of a difference, since this is the only place they are used, but if you think that'd be better, I'll change to that. How about continuation for the name?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation sounds much better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed my mind a bit on continuation and have reservations about it since in the retry case, you can invoke it more than once and you can choose not to call it at all. How about invoker, though the downside is it is not an instance of CallInvoker.

});
var server = helper.GetServer();
server.Start();
var callInvoker = helper.GetChannel().Intercept(interceptor);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the order of interceptors being applied if I do .Interept(interceptor1) .Intercept(interceptor2)? I assume it's the "decorator" style, so the interceptor applied as last will be invoked first (which is the same order as if I stacked CallInvokers on top of each other).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is how it is going to work.

I am considering adding another overload that takes params Interceptor[] and adds the list in-order (not the opposite order implied by decorator style).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd definitely add this order of operations to be clear. I pref FIFO, but I would say you want both functions to behave the same versus supporting one the opposite way.

Copy link
Copy Markdown
Contributor Author

@mehrdada mehrdada Sep 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think FIFO does not make sense when chaining calls like .Intercept(a).Intercept(b), because you can imagine a scenario in which you are given a CallInvoker by another part of your program that is created with channel.Intercept and you then decide to intercept it again and see the whole thing as a black box.

However, I do agree that an overload that takes multiple interceptors in a single call: .Intercept(a, b, c) should pass control to a, b, and c in that order.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the Intercept(a, b, c) overload would be useful.

Copy link
Copy Markdown

@plaisted plaisted Oct 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be nice to also have an overload like:

.Intercept(p => {
        p.AddLogging();
        p.AddAuth();
    });

where p is a pipeline object of some sort.

This would make it very easy to develop packagable pipeline plugins by just creating an extension method for the pipeline object. Additionally in cases where you don't care about the type of TRequest / TResponse (eg. logging) you could create a generic extension method so that your plugin will work for all types of requests / responses.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@plaisted Can you clarify what is the advantage of that over

.Intercept(new Logger(), new Authenticator());

?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mehrdada

Functionally they are the same but using the extension method allows you to nicely encapsulate what "API" you want the user of the package to use vs. what the actual constructor that the class needs.

As a contrived example the Authenticator interceptor may require a TokenValidator service so it's constructor is actually Authenticator(ITokenValidator validator). Adding an extensions method in your package of AddJwtAuth() would simply construct the Authenticator interceptor with a JwtTokenValidator that implements ITokenValidator.

Now the extension method would generally do a lot more than that, maybe even add multiple interceptors which is trivial with this sort of setup:

//extension method
public static void AddJwtAuth(this IInterceptorPipeline pipeline, Action<JwtOptions> optionsBuilder) {
    var options = optionsBuilder(new JwtOptions());
    pipeline.Add(new Authenticator(new JwtTokenValidator(options)));
    pipeline.AddInterceptor2(); // add a dependent interceptor
    pipeline.AddInterceptor3(); // add another
}

//using it
.Intercept(p => {
        p.AddJwtAuth(o=>{ o.ValidationCertificate = cert; });
    });

I've written a few addons (aspcore mainly) that use this sort of approach and it works very nicely as you don't have to expose any implementation details to the users of the package. Admittedly all of this other than adding multiple interceptors could all be implemented relatively easily using a static "factory" method so a lot of this is preference and wanting to stay similar to how I see this done across different frameworks in c#.

ContextPropagationToken propagationToken;
CallCredentials credentials;
CallFlags flags;
Dictionary<object, object> items;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't erasing the type to <object,object> unfriendly to users? What are some example situations where I'd want do use the "items" object?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we considered using a .NET context to carry the extra state? Also I think some .NET context objects use string as a key (not sure what's better here actually).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean by ".NET context" object? Synchronization context?
Yes, my initial version actually had this as <string, object> like HttpContext does, but I found myself wanting to use typeof(MyInterceptor) as a key, and it would not have worked with string keys, and I found no reason to restrict the dictionary to string keys (keys are generally used as input, not output, so they are rarely read to need explicit casting)—and for the value object it did not make sense to have anything less general than object.

What I have in mind is a higher level interceptor base class that inherits from ClientInterceptor that uses this dictionary under the hood, but relies on a convention to store data in that dictionary and expose it in a type-safe way.

Copy link
Copy Markdown

@Disturbing Disturbing Sep 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a huge fan of object versus string, sometime I like to store things via by type.

Get < T > ( ) for example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned that this already-large struct is becoming huge. The threading is also a concern... Dictionary<,> isn't thread-safe - what are the expected uses here, and are we satisfied they won't cause problems?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jskeet The use-case is to share some call-specific data between the interceptors and potentially the call invoker. (The closest .NET analogy I have in mind is HttpContext.Context.) I initially thought Metadata headers should be abused for this purpose, but it can be too inconvenient. For example, one use case is associating a User object (or any other state information) via an interceptor that can then be pulled and injected into the RPC. Another design I contemplated with is replacing the dictionary with a single object Tag, however, from a struct size point of view, it has the same effect. It will alleviate the threading concerns though, perhaps at a cost to usability.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the use case for "items", but I share @jskeet 's concerns.

Idea - could we postpone adding the "items" field and later add it without breaking the API? Adding the basic functionality for interceptors in this PR and having one or two "improvements" PR might actually be productive approach.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to move items to a different PR, but this is an important requirement it seems, and we need to tackle it soon anyway. Metadata strings are a really ugly approach.

Another idea is to introduce an InterceptorContext which holds interceptor state and pass it along in the continuation chain. However, the downside is there is no clear way to share that information with the final CallInvoker (which might or might not be necessary). If we are going to do this, we can't do it in another PR, because that is a core API change in the interceptor itself (adding another argument to interception callbacks. and the continuation delegate. I'd really like to know what @jskeet thinks about this alternative solution.

@Disturbing
Copy link
Copy Markdown

Disturbing commented Oct 1, 2017 via email

Copy link
Copy Markdown
Contributor

@jskeet jskeet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the general idea of interception. I think before merging we should:

  • Have benchmarks showing the impact of just adding the infrastructure without any interceptors (client + server)
  • Have benchmarks showing the impact of interceptors (client + server)
  • Write a bunch of interceptors (anything simple we can think of) so we've got a better chance of coming up with the best design for how to create them

{
return this.serviceDefinition;
}
set
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just turn this into an automatically implemented property? (Ditto for Host potentially, but that's not being changed in this PR, so it may want to be done separately.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered this, but all (or almost all?) properties in gRPC C# code base seem to be not automatically generated. I wonder if it is intentional for compatibility purposes with older compilers. That's why I kept consistent with the approach in the codebase.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ad auto-properties: there's no particular reason why we are not using them, but you're right the most times I didn't use them so for now it might makes sense to stay consistent.

I think autoproperties have been in C# since early version, so compiler compatibility is not a concern here. But there are other language constructs (from C# 6) that we intentionally didn't use because until about a year ago, we needed the code to compile on older mono and Visual Studio. Now when we switched to the new dotnet projects, we could start using C# 6 in our code (the compiled code will still be compatible with older .NET and the generated code needs to still compile with older .NET) - so feel free to use C# 6 if you want to. Sometime in the future, we might refactor portions of our existing code and introduce C# 6 constructs where appropriate.

ContextPropagationToken propagationToken;
CallCredentials credentials;
CallFlags flags;
Dictionary<object, object> items;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned that this already-large struct is becoming huge. The threading is also a concern... Dictionary<,> isn't thread-safe - what are the expected uses here, and are we satisfied they won't cause problems?

}

/// <summary>
/// Returns a new instance of <see cref="CallOptions"/> with <c>Items</c> set to the value provided.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly clarify that any previous items are effectively lost rather than merged?

/// <summary>
/// Returns a new instance of <c>AsyncServerStreamingCall</c> that has hooks to intercept on an underlying object of the same type.
/// </summary>
protected static AsyncServerStreamingCall<TResponse> Intercept<TRequest, TResponse>(AsyncServerStreamingCall<TResponse> call,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd quite like a way of creating an interceptor to just modify/inspect one aspect (e.g. metadata) of all calls - Interceptor.ForMetadata(metadata => { ...})

}
}

private Delegate WrapDelegate(Delegate d)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this reflection looks inefficient to me. I haven't quite got my head round how it's all used at the moment, but it would be great to avoid it...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This process only happens at the startup time of the service once, so I don't think the potential performance impact matters. Unfortunately, I don't know of any other way to avoid this dance, since dynamic or using C# binder to find the appropriate overload require depending on additional libraries. We need this to map a non-generic object back to the correct parameterized-type overload. If you have a better way to accomplish this in mind, definitely let me know please.

@jtattermusch
Copy link
Copy Markdown
Contributor

As @jskeet suggests, I think we really should implement several interceptors to get the feel of the API, that basically goes hand in hand with my request for snippets that demonstrate common use cases - once we have those implementations/snippets, we'll know more.

@mehrdada
Copy link
Copy Markdown
Contributor Author

mehrdada commented Oct 5, 2017

@Disturbing I am not sure there is a good way for the core API to be able to handle dynamic insertion and removal without incurring API complications/potential performance costs. However, a simple solution for the user, given the current API, would be to write a "MultiplexerInterceptor" that is a meta-interceptor and supports insertion and removal if they really need that feature and are willing to pay the cost.

@Disturbing
Copy link
Copy Markdown

Disturbing commented Oct 6, 2017 via email

@mehrdada
Copy link
Copy Markdown
Contributor Author

mehrdada commented Oct 6, 2017

@Disturbing I mostly worry about threading issues and the need to add access the dictionary concurrently, adding locks, and possibly slowing down all requests even if you don't intend to do any add/remove from the registry. Also, defining the semantics to make it clear what would happen when you remove an interceptor for an in-flight request, for example, can make things complicated and potentially hard to stick to across future versions.

@Disturbing
Copy link
Copy Markdown

Disturbing commented Oct 6, 2017 via email

@Disturbing
Copy link
Copy Markdown

Disturbing commented Oct 6, 2017 via email

@mehrdada
Copy link
Copy Markdown
Contributor Author

mehrdada commented Oct 6, 2017

@Disturbing Yeah, by "locking" I meant it more generally than the lock statement. I am most worried about the additional complexity. Let me think about it a bit more, but I don't think a mere dictionary alone will cut it, since I care about order of interceptors being invoked. I think if we end up doing it, it will effectively look like that MultiplexerInterceptor I just described, but shipped in the Grpc.Core assembly. I'll focus on the core functionality in this PR; we can add that later.

{
this.header = new Metadata.Entry(key, value);
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> next)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is async and return await next(), while the client test is non-async-but-still-Task and just returns next(). Is the difference intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which client method? Line 69?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was referring to difference between the client interceprot test
(in ClientInterceptorTest.AddHeaderClientInterceptor.BlockingUnaryCall) and the server interceptor test.

The relevant client test code is:

public override TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request, Func<Method<TRequest, TResponse>, string, CallOptions, TRequest, TResponse> next)
{
    options.Headers.Add(this.header);
    return next(method, host, options, request);
}

It just returns a Task, while in the similar ServerInterceptorTest.AddRequestHeaderServerInterceptor.UnaryServerHandler, the method is marked async and has the pattern return await next(...). Since there's no other async action in this method, I would have expected a non-async method that just return next(...);

Is there something special about the server-side or is this an unintentional difference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it out. Fixed.

@Xylez01
Copy link
Copy Markdown

Xylez01 commented Oct 30, 2017

Any reason this API differs from the Java implementation?

ClientInterceptor

ClientInterceptors

ServerInterceptor

ServerInterceptors

Just wondering because I see some added value for common APIs between language implementations coming from an environment that uses multiple languages/platforms.

@mehrdada
Copy link
Copy Markdown
Contributor Author

@JordyLangen There's value in consistency across implementations, and C# implementation is arguably more similar to Go, Ruby, and upcoming Python designs, leaving Java as the odd duck here. I think the differences in existing APIs are partially the cause for differences. I would say the updated C# server interceptors are more similar to Java style. On the client side, Java's implementation is very much tied to the Channel, whereas in C# we had the nice CallInvoker abstraction already existing. One design goal was to interfere in the core RPC machinery as little as possible, so as to (1) not potentially hinder the performance of the non-intercepted RPCs. (2) be able to short-circuit the whole thing. (I also think part of the root cause for some things can be traced back to differences in generics in C# and Java.)
This design (C#), basically gives control to the interceptor to do anything it wants right when a call is made. It is easy to build an interceptor on top of this that raises event notifications as headers get sent, messages get sent, etc. more similar to the Java ones.

@vassilvk
Copy link
Copy Markdown

@mehrdada - what is the status of this PR? Was it replaced by another implementation?

@mehrdada
Copy link
Copy Markdown
Contributor Author

@vassilvk I am planning to get back to this as soon as we get done with the imminent gRPC 1.8 release. I am considering some potential design tweaks though. Stay tuned.

@listepo-alterpost
Copy link
Copy Markdown

Hi, any news?

@mehrdada
Copy link
Copy Markdown
Contributor Author

@HideDev actively polishing some final stuff. will have an update for you this week

@mehrdada
Copy link
Copy Markdown
Contributor Author

@jtattermusch Addressed comments -- PTAL.

Copy link
Copy Markdown
Contributor

@jtattermusch jtattermusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another set of comments - after these are addressed, I think I am mostly happy with the code with the exception of GenericInterceptor:

  1. I agree that we should have a standard GenericInterceptor as part of Grpc, but that doesn't necessarily mean we have to rush things and merge ti now.

  2. I'd feel more comfortable reviewing GenericInterceptor in a separate PR (which might not make it to 1.10.x because it looks like we should cut the release branch asap). The problem is once the GenericInterceptor is checked in (even as an internal class), it is likely that it won't get proper review later on. Also, merging in a monlithic PR would make it hard to revert the GenericInterceptor if there's trouble.

  3. the only interceptor we need to ship right now is the interceptor for ClientBase (to replace the existing functionality), but implementing that interceptor should be trivial - we don't need the GenericInterceptor for that.

  4. I think it is healthy to have a decent code coverage for Interceptors by tests that don't utilize GenericInterceptor - as using GenericInterceptor is optional in its nature, we should also have tests that don't use it.
    Once we add GenericInterceptor in a followup PR, we should have a comprehensive set of tests for it (given the complexity of GenericInterceptor code, we could use more tests than we currently have).

/// invocation metadata.
/// </param>
/// <remarks>
/// Multiple interceptors can be added on top of each other by calling
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can't actually use Intercept(a,b,c) when you're passing Func<Metadata, Metadata> as an interceptor, so the comment is a bit misleading.

/// Provides extension methods to make it easy to register interceptors on Channel objects.
/// This is an EXPERIMENTAL API.
/// </summary>
public static class ChannelExtensions
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to leave as is.

/// </summary>
public InterceptingCallInvoker(CallInvoker invoker, Interceptor interceptor)
{
this.invoker = GrpcPreconditions.CheckNotNull(invoker, "invoker");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since C# 6 you can use nameof(invoker) instead of "invoker".

Assert.AreEqual("PASS", callInvoker.BlockingUnaryCall(new Method<string, string>(MethodType.Unary, MockServiceHelper.ServiceName, "Unary", Marshallers.StringMarshaller, Marshallers.StringMarshaller), Host, new CallOptions(), ""));
}

private class CallbackInterceptor : GenericInterceptor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: code style: I tend to put the private classes after all the instance methods as they are basically "helpers" (and the tests is what you should see first when reading the file).

public void CheckNullInterceptorRegistrationFails()
{
var helper = new MockServiceHelper(Host);
helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you don't need to set the UnaryHandler here IMHO.

if (!(e is RpcException))
{
Logger.Warning(e, "Exception occured in handler.");
Logger.Warning(e, "Exception occured in handler or interceptors.");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consistency of the error message - here it's "interceptors" and below it's "interceptor".


public IServerCallHandler Intercept(Interceptor interceptor)
{
return this; // Do not intercept unimplemented services
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/unimplemented services/unimplemented methods/

/// </remarks>
public static ServerServiceDefinition Intercept(this ServerServiceDefinition serverServiceDefinition, Interceptor interceptor)
{
GrpcPreconditions.CheckNotNull(serverServiceDefinition, "serverServiceDefinition");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use nameof(serverServiceDefinition). More occurrences below.

/// Gets or sets the <see cref="Grpc.Core.Method{TRequest, TResponse}"/> instance
/// representing the method to be invoked.
/// </summary>
public Method<TRequest, TResponse> Method { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// Serves as the base class for gRPC interceptors.
/// This is an EXPERIMENTAL API.
/// </summary>
public abstract class Interceptor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason why to have only a single Interceptor class for both server and client?
Another option would be to have Interceptor and ServerInterceptor classes and each would be only be defining interception methods that are server-specific and client specific? It's hard for me to imagine a scenario where I would use the same interceptor for both server and client.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is an abstract class rather than interface, I deliberately decided to merge the two interfaces into one. The overhead is minimal and it will allow one class to implement both sides, which is nice, if you are a library author for example, and provide a tracing or logging interceptor, you expose one class to the user that they register on both sides. If you want, you can implement only one side, but if we went with two base classes, there was no way to consolidate them into one implementation.

@mehrdada
Copy link
Copy Markdown
Contributor Author

mehrdada commented Feb 22, 2018

@jtattermusch Eliminated GenericInterceptor and addressed other comments as well. PTAL.

Copy link
Copy Markdown
Contributor

@jtattermusch jtattermusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for your patience!

Let's see what the feedback on the new API is once we release in v1.10.x and incorporate feedback into v1.11.x (I think all the new classes are marked as experimental).

@mehrdada mehrdada merged commit b74e050 into grpc:master Feb 22, 2018
@mehrdada mehrdada deleted the csharp_interceptors_take_two branch February 22, 2018 18:26
@mehrdada
Copy link
Copy Markdown
Contributor Author

Test failures:
Artifact build windows: shows pending on github, but log shows success
Multilang Linux: Infra error -- ignored (local csharp tests pass)

@893949088
Copy link
Copy Markdown

Awesome,When will this version be released?

@mehrdada
Copy link
Copy Markdown
Contributor Author

@893949088 1.10.0-RC1 will be out likely tomorrow or early next week. Hopefully the final release will be out in a week after that

@zsojma
Copy link
Copy Markdown
Contributor

zsojma commented Mar 13, 2018

Is there some manual how to use it? Thanks

@Falco20019
Copy link
Copy Markdown
Contributor

Falco20019 commented Mar 13, 2018

You can have a look at my GRPC-Opentracing implementation: https://github.com/Falco20019/grpc-opentracing/tree/csharp-experimental/csharp/Grpc.OpenTracing

You can implement your interception logic by extending the Interceptor class: TracingInterceptor You can do whatever you want in there and just have to call and return the continuation delegate with the (maybe modified) data.

On the usage side, you can intercept an Channel to get an CallInvoker that you can use with the generated clients (see here). On the server side, you intercept the service (see here).

This would be an example for a noop UnaryServerHandler:

public override Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
{
	// Do whatever you want with request and context, or maybe change it...
	return continuation(request, context);
}

@kkolstad
Copy link
Copy Markdown

For those trying to follow the links in the previous comment:

@Falco20019
Copy link
Copy Markdown
Contributor

Falco20019 commented Aug 14, 2018

@kkolstad Thanks for noticing. I updated all the links in my post so that it’s easier to follow along again :) The repository linked in my post is old and archived. The code has been migrated and advanced to https://github.com/opentracing-contrib/csharp-grpc

So in case that someone is specifically looking for OpenTracing with gRPC, follow the links in this post, not the one in the two posts above. There are also two more detailed pages to follow for two use cases:

@lock lock bot locked as resolved and limited conversation to collaborators Nov 12, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.