Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api,core: Add ServerInterceptor2 interface #7746

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

ericgribkoff
Copy link
Contributor

ServerInterceptor2 allows rewriting the ServerMethodDefinition. This accommodates, among other things, setting stream tracers on the ServerMethodDefinition as part of an interceptor, rather than requiring stream tracer factories to be added directly to the server builder prior to construction.

Since some aspects of the stats trace context are currently created when the stream begins and before method handler lookup has occurred, which means any stream tracers attached via a ServerInterceptor2 must be invoked later, after method lookup. And one of the abilities of a server stream tracer is filtering the context in which the call will take place. This PR rewires things in ServerImpl to accomodate a (partially) delayed creation of the stats trace context itself and the context in which the call operates.

Notes for review:

  1. The StatsTraceContext class already had several client-specific and server-specific methods. This PR adds more. It is probably worthwhile splitting the class into a ClientStatsTraceContext and ServerStatsTraceContext.
  2. Looking at it now, the Javadoc for ServerBuilder#intercept(io.grpc.ServerInterceptor2)is confusing when discussing "order" in relation to the existing ServerInterceptor, as the ServerInterceptor2 API is actually invoked prior to the call starting, so outside of the ServerInteceptor#startCall ordering entirely - but ServerInterceptor2 can still rewrite the ServerCallHandler to mimic the operation of a ServerInterceptor.

Copy link
Member

@ejona86 ejona86 left a comment

Choose a reason for hiding this comment

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

Sending the review comments I have. I should have sent them out last week, but forgot to. They are incomplete, and even includes a note for myself, but probably are helpful.

@@ -24,11 +27,13 @@
public final class ServerMethodDefinition<ReqT, RespT> {
private final MethodDescriptor<ReqT, RespT> method;
private final ServerCallHandler<ReqT, RespT> handler;
private final List<ServerStreamTracer.Factory> streamTracerFactories;
Copy link
Member

Choose a reason for hiding this comment

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

This class must not be mutable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -654,6 +685,23 @@ public void cancelled(Context context) {
}
}

/** Wrapper to convert ServerInterceptor to ServerInterceptor2. */
private static final class ServerInterceptorConverter implements ServerInterceptor2 {
Copy link
Member

Choose a reason for hiding this comment

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

This should be provided as a method on ServerInterceptors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

/**
* Adds a {@link ServerInterceptor2} that is run for all services on the server. Interceptors
* added through this method always run before per-service interceptors added through {@link
* ServerInterceptors} and any interceptors added through {@link #intercept(ServerInterceptor)}.
Copy link
Member

Choose a reason for hiding this comment

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

I think we want to be able to mix ServerInterceptor and ServerInterceptor2 together, such that they are run in the order added, not in two stages. I imagine that would be implemented by converting ServerInterceptors to ServerInterceptor2 when being added.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is much clearer. Done.

@@ -554,13 +558,28 @@ private void runInternal() {
context.cancel(null);
return;
}
listener = startCall(stream, methodName, method, headers, context, statsTraceCtx, tag);
ServerMethodDefinition<?, ?> interceptedDef = getInterceptedMethodDef(method);
Copy link
Member

Choose a reason for hiding this comment

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

Since interceptors can now do non-trivial processing ahead-of-time for the method, I think we'll try to do this per-RPC only for the fallbackRegistry case. We can pre-apply the interceptors to the services in registry. Although there may be complications, like with reflection. Not essential today, but at least put a TODO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added TODO

ServerCallHandler<ReqT, RespT> handler = methodDef.getServerCallHandler();
for (ServerInterceptor interceptor : interceptors) {
handler = InternalServerInterceptors.interceptCallHandler(interceptor, handler);
Context previous = context.attach();
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: Context here and in JumpToApplicationThreadServerStreamListener seems should see some simplification. Why is it doing this?

StreamTracer[] tracerArr = new StreamTracer[factories.size()];
for (int i = 0; i < tracerArr.length; i++) {
ServerStreamTracer tracer = factories.get(i).newServerStreamTracer(fullMethodName, headers);
context = tracer.filterContext(context).withCancellation();
Copy link
Member

Choose a reason for hiding this comment

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

No, no. Every CancellableContext must be canceled at some point in the future. It's not clear to me why we'd even need to involve this code with CancellabelContext.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't need to be CancellableContext here. As far as the ServerStreamTracer#filterContext it should just be a Context. This is just a result of bad 'type matching' on my part: by the time this method is invoked from ServerImpl, we already have a CancellableContext - I think it's necessary very early in the call handling process for e.g. RPC deadlines - and I was just blindly maintaining that type here.

But I'm not sure I fully understand the problem that you are describing: looking at what withCancellation actually does, it's obvious that it's heavier weight than we actually need but I understood your comment to mean this is actually incorrect due to a mishandling of the context's lifetime. However, since we begin with a CancellableContext from ServerImpl (created and managed by the existing code prior to this PR, so I don't think I've broken anything there, probably :)) and according to the CancellableContext javadoc it is "A context which inherits cancellation from its parent but which can also be independently cancelled and which will propagate cancellation to its descendants" shouldn't this actually work as-is? Since the parent CancellableContext from ServerImpl.ServerTransportListenerImpl#createContext will eventually be cancelled and that cancellation propagated to the descendants created here?

I'm worried that I'm misunderstanding something fundamental here, but the basic constraint I was operating under was that, when this method is invoked from ServerImpl.ServerTransportListenerImpl#streamCreatedInternal, we have a CancellableContext and want to get a CancellableContext back. The current code is just a ham-handed way of maintaining that type, but is there a reason that we don't want a CancellableContext at this point at all and the "conversion" from Context to CancellableContext currently done in ServerImpl.ServerTransportListenerImpl#createContext should be deferred until after these interceptor-provided tracers have been invoked?

Copy link
Member

Choose a reason for hiding this comment

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

I was talking about "To avoid leaking memory, every CancellableContext must have a defined lifetime, after which it is guaranteed to be cancelled." Yes, if the parent is cancellable you are guaranteed this one will have a lifetime, although you aren't guaranteed the lifetime is appropriate (the lifetime could be the entire process lifetime in some Context cases). I think the only reason you know the lifetime is appropriate here is because you say "statsTraceCtx.serverFilterContext()" is expected to be called first, at which point the context should already be scoped to the call.

But there's costs to cancellable contexts, and it is really weird that they don't manage their own lifetime, and it's weird they are created in a loop (instead of only once at the end). I think this code should just be dealing with normal contexts; normal context input and normal context output.

Copy link
Contributor Author

@ericgribkoff ericgribkoff left a comment

Choose a reason for hiding this comment

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

Thanks for the review! I addressed most of the comments, and left a long-winded follow-up query in regards to properly handling the CancellableContext issue

StreamTracer[] tracerArr = new StreamTracer[factories.size()];
for (int i = 0; i < tracerArr.length; i++) {
ServerStreamTracer tracer = factories.get(i).newServerStreamTracer(fullMethodName, headers);
context = tracer.filterContext(context).withCancellation();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't need to be CancellableContext here. As far as the ServerStreamTracer#filterContext it should just be a Context. This is just a result of bad 'type matching' on my part: by the time this method is invoked from ServerImpl, we already have a CancellableContext - I think it's necessary very early in the call handling process for e.g. RPC deadlines - and I was just blindly maintaining that type here.

But I'm not sure I fully understand the problem that you are describing: looking at what withCancellation actually does, it's obvious that it's heavier weight than we actually need but I understood your comment to mean this is actually incorrect due to a mishandling of the context's lifetime. However, since we begin with a CancellableContext from ServerImpl (created and managed by the existing code prior to this PR, so I don't think I've broken anything there, probably :)) and according to the CancellableContext javadoc it is "A context which inherits cancellation from its parent but which can also be independently cancelled and which will propagate cancellation to its descendants" shouldn't this actually work as-is? Since the parent CancellableContext from ServerImpl.ServerTransportListenerImpl#createContext will eventually be cancelled and that cancellation propagated to the descendants created here?

I'm worried that I'm misunderstanding something fundamental here, but the basic constraint I was operating under was that, when this method is invoked from ServerImpl.ServerTransportListenerImpl#streamCreatedInternal, we have a CancellableContext and want to get a CancellableContext back. The current code is just a ham-handed way of maintaining that type, but is there a reason that we don't want a CancellableContext at this point at all and the "conversion" from Context to CancellableContext currently done in ServerImpl.ServerTransportListenerImpl#createContext should be deferred until after these interceptor-provided tracers have been invoked?

@@ -554,13 +558,28 @@ private void runInternal() {
context.cancel(null);
return;
}
listener = startCall(stream, methodName, method, headers, context, statsTraceCtx, tag);
ServerMethodDefinition<?, ?> interceptedDef = getInterceptedMethodDef(method);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added TODO

/**
* Adds a {@link ServerInterceptor2} that is run for all services on the server. Interceptors
* added through this method always run before per-service interceptors added through {@link
* ServerInterceptors} and any interceptors added through {@link #intercept(ServerInterceptor)}.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is much clearer. Done.

@@ -24,11 +27,13 @@
public final class ServerMethodDefinition<ReqT, RespT> {
private final MethodDescriptor<ReqT, RespT> method;
private final ServerCallHandler<ReqT, RespT> handler;
private final List<ServerStreamTracer.Factory> streamTracerFactories;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -654,6 +685,23 @@ public void cancelled(Context context) {
}
}

/** Wrapper to convert ServerInterceptor to ServerInterceptor2. */
private static final class ServerInterceptorConverter implements ServerInterceptor2 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@ericgribkoff
Copy link
Contributor Author

Also, I know we discussed a better name than ServerInterceptor2 - I didn't want to push a rename commit mid-review, but ServerMethodDefinitionInterceptor is definitely more informative (it's a bit long, but I'm not sure if that's really a concern - it's 2 characters shorter than our current io.grpc.api co-champions,PartialForwardingClientCallListener and PartialForwardingServerCallListener :))

Copy link
Member

@ejona86 ejona86 left a comment

Choose a reason for hiding this comment

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

Oh, apparently I've have some pending comments sitting here for a long time. I'm really surprised github didn't GC them.

this.method = method;
this.handler = handler;
this.tracerFactories = Collections.unmodifiableList(tracerFactories);
Copy link
Member

Choose a reason for hiding this comment

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

This is not immutable. You need to make a copy.

List<ServerStreamTracer.Factory> l = new ArrayList<>();
def = def.withStreamTracerFactories(l);
l.add(foo);

*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/????")
public ServerMethodDefinition<ReqT, RespT> withStreamTracerFactories(
List<ServerStreamTracer.Factory> tracerFactories) {
Copy link
Member

Choose a reason for hiding this comment

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

This is an annoying API to use, as it requires getting the current list, copying it, and adding your own factory to the list, all before calling this API. How about a withAdditionalStreamTracerFactory(ServerStreamTracer.Factory) which adds to the list for you. (withStreamTracerFactory may be an okay name, but that's more for an API review meeting discussion.)

* @since 1.36.0
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/????")
public List<ServerStreamTracer.Factory> getStreamTracerFactories() {
Copy link
Member

Choose a reason for hiding this comment

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

There may be value in this being a Collection. List is fine for now, but it should be discussed in the API review meeting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants