[Aio] Implement server interceptor for unary unary call#22032
[Aio] Implement server interceptor for unary unary call#22032lidizheng merged 13 commits intogrpc:masterfrom
Conversation
Sync with grpc repo
Sync with grpc repo
Sync with grpc repo
Sync with grpc
Sync with grpc
|
|
||
| async def _find_method_handler(str method, tuple metadata, list generic_handlers, | ||
| tuple interceptors): | ||
| def query_handlers(handler_call_details): |
There was a problem hiding this comment.
can't we replaced this by a geneator expression + next() ?
method_handlers = (generic_handler.service(handler_call_details) for generic_handler in generic_handlers)
method_handler = next((mh for mh in method_handlers if mh is not None), None)There was a problem hiding this comment.
We will need to iterate them in _run_interceptors[1] if we use generator at here.
IMO, current version will make _run_interceptors simpler.
|
|
||
| import inspect | ||
| import traceback | ||
| import functools |
There was a problem hiding this comment.
pfreixes
left a comment
There was a problem hiding this comment.
Thanks for the work done!
First round of comments, Ive just focused on the tests
| self.fail("Callback was not called") | ||
|
|
||
|
|
||
| class _LoggingServerInterceptor(aio.ServerInterceptor): |
There was a problem hiding this comment.
I would move all of the server test interceptor logic under a new file, like sever_interceptor_test.py, and maybe I would rename the former one to client_interceptor_test.py
|
|
||
| with self.assertRaises(aio.AioRpcError): | ||
| server_target, _ = await start_test_server( | ||
| interceptors=(InvalidInterceptor(),)) |
There was a problem hiding this comment.
a Channel with an invalid interceptor is not allowed to be instantiated [1 ], having the feeling that we would need to do the same with the server.
| # in the right order. | ||
| self.assertSequenceEqual(['log1:intercept_service', | ||
| 'log2:intercept_service',], self._record) | ||
| self.assertIsInstance(response, messages_pb2.SimpleResponse) |
There was a problem hiding this comment.
Maybe we can have another test - test_response_ok ? - for checking that the response is looking as we would be expecting by checking the status and the response content. With this specific test, we won't need to check at each test the response
| _LoggingServerInterceptor('log1', self._record), | ||
| conditional_interceptor, | ||
| _LoggingServerInterceptor('log2', self._record), | ||
| ) |
There was a problem hiding this comment.
Having all of the interceptors added into the setup which they would be used later for all of the tests makes the readability a bit harder for me, TBH I would prefer to have each test case with its own server with its own interceptors for testing the different functionalities.
The same as you already did for the test_invalid_interceptor test case.
Having the feeling that having this kind of sharing, the same happens with the _record class attribute, we have a lot of chances of creating down-side effects between tests.
| return _GenericServerInterceptor(intercept_service) | ||
|
|
||
|
|
||
| class TestServerInterceptor(AioTestBase): |
There was a problem hiding this comment.
I would like to have a test for checking observability of the code responses sent by the handler, in the same way as we have for the channel [1]
There was a problem hiding this comment.
Server interceptors are wrapped at find_method_handler , the interceptors won't get the status code of response.
| return await _run_interceptor(iter(interceptors), query_handlers, | ||
| handler_call_details) | ||
| else: | ||
| return query_handlers(handler_call_details) |
There was a problem hiding this comment.
Both _run_interceptor and _find_method_handler functions are on the data path, is it possible to simplify them? Or run the benchmark to check the impact on performance?
There was a problem hiding this comment.
what about doing something like:
async def _find_method_handler(...):
for generic_handler in generic_handlers:
method_handler = generic_handler.service(handler_call_details)
if method_handler is not None:
break
if interceptors:
return await _run_interceptor(....., method_handler)
else:
return method_handlerWith this change theoretically the execution path when there is no interceptors should not be hammered.
WDYT? /cc @lidizheng
There was a problem hiding this comment.
It depends on whether we allow users to change the method.
If allowed, the method handler needs to be ran as last function in the chain, otherwise users' interception might not work.
If not allowed, this is a valid optimization.
| return await self._fn(continuation, handler_call_details) | ||
|
|
||
|
|
||
| def _filter_server_interceptor(condition, interceptor): |
There was a problem hiding this comment.
To improve the maintainability of test cases, we might want to add type annotations, especially for callbacks.
| 'log3:intercept_service', | ||
| 'log2:intercept_service',], | ||
| self._record) | ||
|
|
There was a problem hiding this comment.
Should we add one more test case that the interceptor returns a different method handler?
There was a problem hiding this comment.
I couldn't understand this one, can you give me more details?
gnossen
left a comment
There was a problem hiding this comment.
LGTM pending resolution of everyone else's comments.
| return await self._fn(continuation, handler_call_details) | ||
|
|
||
|
|
||
| def _filter_server_interceptor(condition, interceptor): |
There was a problem hiding this comment.
Very cool usage of interceptors. 👍
|
One of my regrets with the original design for sync interceptors is allowing server-interceptors to be flexible and change the path which massively limits the opportunity of optimization for handing over to handlers and doing so is non-idiomatic gRPC anyway. Please make sure you at least revisit whether this flexibility is worth the cost. |
|
@mehrdada By
Are you referring to the potential optimization in finding methods? Otherwise, it could be done in Cython or C++? Can you explain more about this topic? |
|
@ZHmao also consider that current PR has conflicts that are preventing us to run the whole CI, I would appreciate if you could resolve the conflicts. Thanks! |
|
@lidizheng C-Core server supports registering RPC paths directly upfront as opposed to everything be "the generic handler". Python API is clearly designed to be able to leverage it (see |
Sure, I will. |
|
@mehrdada @lidizheng having the chance of adding interceptors for specific handlers is something that could be really useful, not only because of performance but because the user has the chance of adding a piece of logic that is related to a specific path. On the other hand, generic handlers are gonna be needed still for doing generic stuff like observability, tracing, etc ... IMO the interceptors discussion with regards the design is not yet closed, we have two outstanding g questions that would need to be answered before making this feature as no longer experimental:
Right now we are on the way of doing an alpha release and this feature is marked as experimental, so I guess that we have still full freedom on breaking the API or extending it without having the friction of being in a beta/stable release. |
|
@pfreixes I am fine with releasing alpha or whatever as long as it can be changed. To clarify, my concern is a bit orthogonal to interceptors being able to attach to specific paths (although I am not necessarily a fan, it is a valid feature to debate). The limitation that I like to see enforced is interceptor not being able to alter the path it is invoked on, i.e. return a different path than the one it's invoked on. It is valid for it to see the path. It might require more work and a more complex API, but it is also alright if it can be configured to get invoked on certain paths and not others as long as all this info is known upfront on server start, because you can still construct the handler pipelines once ahead of the time and dispatch accordingly, even if the pipelines vary per request path. |
| return None | ||
| # interceptor | ||
| if interceptors: | ||
| return await _run_interceptor(iter(interceptors), query_handlers, |
There was a problem hiding this comment.
Should we return and not execute, the _run_interceptor coroutine? This would mean that the execution will eventually happen at the very last step and the interceptor for example in the case of a unary call will be here [1]
Indeed we could name this method like _wrap_call_interceptor or something like that.
Without doing this, future interceptors that would take care of observability for measuring the time that handler took won't work since they won't be able to measure the real execution of the handler.
Also moving this to the right place, will eventually will help us for extending the interceptors for having visibility of the servicer context [2] at the interceptor level.
The only friction point of doing this that I can see, if Im not missing something is the access to the metadata. So the interceptor won't be able to have direct access to the metadata and mutate them, but could have access to the service context that will make the metadata available for the interceptor.
WDYT @lidizheng should we aim for moving the execution of the interceptor to the very last step?
Also, I'm wondering if since we are not most likely commit to having this feature for the alpha release we could start thinking on how a nex iteration of the servers interceptors could be done for knowing the response code of the method handler. Would this change help us to do so?
[1] https://github.com/grpc/grpc/blob/master/src/python/grpcio/grpc/_cython/_cygrpc/aio/server.pyx.pxi#L242
[2] https://github.com/grpc/grpc/blob/master/src/python/grpcio/grpc/_cython/_cygrpc/aio/server.pyx.pxi#L244
There was a problem hiding this comment.
I agree that the server interceptors should be more powerful. In fact, it would be great, if it has similar functionality as the client-side.
Back to your proposal, it might introduce a regression if users are selecting method handlers according to the invocation metadata (client-sent initial metadata). This feature is supported in the existing stack, even without interceptors (although I wonder if any user uses it).
If we decided to optimize the design of generic handlers (basically let the Core route the requests), then users cannot use this feature anyway. In this case, yes, I think this proposal is valid.
should we aim for moving the execution of the interceptor to the very last step? ... could be done for knowing the response code of the method handler.
If we want a proper full-featured server interceptor, we can make interceptors wrapping entire method handler (similar to client-side). If we only executed it till the last step, users can only either inject logic before or after the RPC (or I might be wrong).
If we only want an interceptor design better than the existing stack, I believe the move does improve server interceptors capability.
|
@lidizheng and @ZHmao Im happy on having an iterative process for implementing the server interceptors, so having first implemented a version that works as the legacy stack is IMO already giving value. So I would not have any inconvenience in approving this PR and later on have other PR for making the server interceptors more powerful. Indeed I would say that it will be the best thing to do. But there is something that I do not understand, looking at this interceptor [1] that is the one used by one of our first adopters of gRPC that are using the legacy stack, seems that the server version allows you to at least:
Looking at the PR seems that neither of the previous capabilities are ready to be used. am I wrong? what Im missing? |
|
Hi @pfreixes , they used a new class |
|
@ZHmao wow, I missed that! So they did what we would like to have.... great example on how the users follow their own paths for circumventing the current limitations. So in that case, if we are already meeting the same requirements that are met by the legacy stack take me 👍 but I would like to see a plan for designing a server interceptor that supersedes the current one and is paired to what lightstep [1] has done. PTAL to the sanity checks, most. of them are failing. |
lidizheng
left a comment
There was a problem hiding this comment.
Great work and great test coverage!
| @@ -0,0 +1,144 @@ | |||
| # Copyright 2019 The gRPC Authors. | |||
There was a problem hiding this comment.
| # Copyright 2019 The gRPC Authors. | |
| # Copyright 2020 The gRPC Authors |
| """ | ||
| self._loop.create_task(self._server.shutdown(None)) | ||
| if hasattr(self, '_server'): | ||
| self._loop.create_task(self._server.shutdown(None)) |
|
Thanks for all your reviews. |
lidizheng
left a comment
There was a problem hiding this comment.
The comment history got hidden by GitHub. Raise a comment that I forget to follow-up.
| return await self._fn(continuation, handler_call_details) | ||
|
|
||
|
|
||
| def _filter_server_interceptor(condition: Callable, |
There was a problem hiding this comment.
I missed this one in my last round of review. By adding type annotation, I mean what is the argument it accepts and returns. You can do this like:
def atoi(a: str) -> int:
return int(a)
atoi: Callable[[str], int]Also, if possible, please annotate all other arguments in this file, not only this function.
|
I have added type annotation for But it failed when I ran cc @lidizheng |
|
@ZHmao I'm not sure I understand the question here. Did you mean the (As I said, complex callbacks are usually confusing and needs proper typing.) |
|
I have written an example code and it shows the same error: I don't know what's wrong, do you have any advice? |
|
@ZHmao Thanks for the convincing code snippet, good find! It looks like # Generated .pyi file
def bar(fun: Callable[[str], Awaitable[str]]) -> Coroutine[Any, Any, str]: ...
def foo(a: str) -> Coroutine[Any, Any, str]: ...
def go() -> Coroutine[Any, Any, None]: ...If you have some free cycle and want to be a contributor of On the other hand, to move this PR forward, there are two (hacky) ways to solve the
@pfreixes Have you encountered this bug before? |
|
I'll use |
|
FYI, when I tried to fix this issue with Error message: But type |
|
@ZHmao Thanks for the update. This definitely looks like an issue for |
|
Can we merge it? Or I need to make all checks pass before we can merge it? |
Support for interceptors for the unary-unary call at the server-side.
Fix #21914
Ref #20482