Skip to content

Commit

Permalink
[Python fix-it][Aysnc Example] Adding examples for aysnc interceptor …
Browse files Browse the repository at this point in the history
…context propagaton (grpc#32840)

### Description

Fix grpc#24470.

Adding one example which demonstrate the following use cases:

* Generate RPC ID on client side and propagate to server.
* Context propagation from client to server.
* Context propagation between different server interceptors and the
server handler.

## Use:

1. Start server: `python3 -m async_greeter_server_with_interceptor`
2. Start client: `python3 -m async_greeter_client`

### Expected Logs:

* On client side:

```
Sending request with rpc id: 73bb98beff10c2dd7b9f2252a1e2039e
Greeter client received: Hello, you!
```

* On server side:

```
INFO:root:Starting server on [::]:50051
INFO:root:Interceptor1 called with rpc_id: default
INFO:root:Interceptor2 called with rpc_id: Interceptor1-default
INFO:root:Handle rpc with id Interceptor2-Interceptor1-73bb98beff10c2dd7b9f2252a1e2039e in server handler.
```
XuanWang-Amos authored May 4, 2023
1 parent 0e7cc36 commit 40f20c0
Showing 8 changed files with 286 additions and 2 deletions.
2 changes: 1 addition & 1 deletion doc/python/sphinx/grpc.rst
Original file line number Diff line number Diff line change
@@ -112,7 +112,7 @@ gRPC Exceptions
.. autoexception:: RpcError


Shared Context
RPC Context
^^^^^^^^^^^^^^

.. autoclass:: RpcContext
2 changes: 1 addition & 1 deletion doc/python/sphinx/grpc_asyncio.rst
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ gRPC Metadata
.. autoclass:: Metadata


Shared Context
RPC Context
^^^^^^^^^^^^^^^^^^^^

.. autoclass:: RpcContext
43 changes: 43 additions & 0 deletions examples/python/interceptors/async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# gRPC Python Async Interceptor Example

This example demonstrate the usage of Async interceptors and context propagation using [contextvars](https://docs.python.org/3/library/contextvars.html#module-contextvars).

## When to use contextvars

`Contextvars` can be used to propagate context in a same thread or coroutine, some example usage include:

1. Propagate from interceptor to another interceptor.
2. Propagate from interceptor to the server handler.
3. Propagate from client to server.

## How does this example works

This example have the following steps:
1. Generate RPC ID on client side and propagate to server using `metadata`.
* `contextvars` can be used here if client and server is running in a same coroutine (or same thead for Sync).
2. Server interceptor1 intercept the request, it checks `rpc_id_var` and decorate it with it's tag `Interceptor1`.
3. Server interceptor2 intercept the request, it checks `rpc_id_var` and decorate it with it's tag `Interceptor2`.
4. Server handler receives the request with `rpc_id_var` decorated by both interceptor1 and interceptor2.

## How to run this example

1. Start server: `python3 -m async_greeter_server_with_interceptor`
2. Start client: `python3 -m async_greeter_client`

### Expected outcome

* On client side, you should see logs similar to:

```
Sending request with rpc id: 59ac966558b3d7d11a06bd45f1a0f89d
Greeter client received: Hello, you!
```

* On server side, you should see logs similar to:

```
INFO:root:Starting server on [::]:50051
INFO:root:Interceptor1 called with rpc_id: default
INFO:root:Interceptor2 called with rpc_id: Interceptor1-59ac966558b3d7d11a06bd45f1a0f89d
INFO:root:Handle rpc with id Interceptor2-Interceptor1-59ac966558b3d7d11a06bd45f1a0f89d in server handler.
```
41 changes: 41 additions & 0 deletions examples/python/interceptors/async/async_greeter_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2023 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The Python AsyncIO implementation of the GRPC helloworld.Greeter client."""

import asyncio
import contextvars
import logging
import random

import grpc
import helloworld_pb2
import helloworld_pb2_grpc

test_var = contextvars.ContextVar('test', default='test')


async def run() -> None:
async with grpc.aio.insecure_channel('localhost:50051') as channel:
stub = helloworld_pb2_grpc.GreeterStub(channel)
rpc_id = '{:032x}'.format(random.getrandbits(128))
metadata = grpc.aio.Metadata(('client-rpc-id', rpc_id),)
print(f"Sending request with rpc id: {rpc_id}")
response = await stub.SayHello(helloworld_pb2.HelloRequest(name='you'),
metadata=metadata)
print("Greeter client received: " + response.message)


if __name__ == '__main__':
logging.basicConfig()
asyncio.run(run())
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright 2023 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The Python AsyncIO implementation of the GRPC helloworld.Greeter server."""

import asyncio
import contextvars
import logging
from typing import Awaitable, Callable, Optional

import grpc
import helloworld_pb2
import helloworld_pb2_grpc

rpc_id_var = contextvars.ContextVar('rpc_id', default='default')


class RPCIdInterceptor(grpc.aio.ServerInterceptor):

def __init__(self, tag: str, rpc_id: Optional[str] = None) -> None:
self.tag = tag
self.rpc_id = rpc_id

async def intercept_service(
self, continuation: Callable[[grpc.HandlerCallDetails],
Awaitable[grpc.RpcMethodHandler]],
handler_call_details: grpc.HandlerCallDetails
) -> grpc.RpcMethodHandler:
"""
This interceptor prepends its tag to the rpc_id.
If two of these interceptors are chained together, the resulting rpc_id
will be something like this: Interceptor2-Interceptor1-RPC_ID.
"""
logging.info("%s called with rpc_id: %s", self.tag, rpc_id_var.get())
if rpc_id_var.get() == 'default':
_metadata = dict(handler_call_details.invocation_metadata)
rpc_id_var.set(self.decorate(_metadata['client-rpc-id']))
else:
rpc_id_var.set(self.decorate(rpc_id_var.get()))
return await continuation(handler_call_details)

def decorate(self, rpc_id: str):
return f"{self.tag}-{rpc_id}"


class Greeter(helloworld_pb2_grpc.GreeterServicer):

async def SayHello(
self, request: helloworld_pb2.HelloRequest,
context: grpc.aio.ServicerContext) -> helloworld_pb2.HelloReply:
logging.info("Handle rpc with id %s in server handler.",
rpc_id_var.get())
return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)


async def serve() -> None:
interceptors = [
RPCIdInterceptor('Interceptor1'),
RPCIdInterceptor('Interceptor2')
]

server = grpc.aio.server(interceptors=interceptors)
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
listen_addr = '[::]:50051'
server.add_insecure_port(listen_addr)
logging.info("Starting server on %s", listen_addr)
await server.start()
await server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
asyncio.run(serve())
30 changes: 30 additions & 0 deletions examples/python/interceptors/async/helloworld_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions examples/python/interceptors/async/helloworld_pb2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Optional as _Optional

DESCRIPTOR: _descriptor.FileDescriptor

class HelloReply(_message.Message):
__slots__ = ["message"]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
message: str
def __init__(self, message: _Optional[str] = ...) -> None: ...

class HelloRequest(_message.Message):
__slots__ = ["name"]
NAME_FIELD_NUMBER: _ClassVar[int]
name: str
def __init__(self, name: _Optional[str] = ...) -> None: ...
70 changes: 70 additions & 0 deletions examples/python/interceptors/async/helloworld_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

import helloworld_pb2 as helloworld__pb2


class GreeterStub(object):
"""The greeting service definition.
"""

def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.SayHello = channel.unary_unary(
'/helloworld.Greeter/SayHello',
request_serializer=helloworld__pb2.HelloRequest.SerializeToString,
response_deserializer=helloworld__pb2.HelloReply.FromString,
)


class GreeterServicer(object):
"""The greeting service definition.
"""

def SayHello(self, request, context):
"""Sends a greeting
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')


def add_GreeterServicer_to_server(servicer, server):
rpc_method_handlers = {
'SayHello': grpc.unary_unary_rpc_method_handler(
servicer.SayHello,
request_deserializer=helloworld__pb2.HelloRequest.FromString,
response_serializer=helloworld__pb2.HelloReply.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'helloworld.Greeter', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))


# This class is part of an EXPERIMENTAL API.
class Greeter(object):
"""The greeting service definition.
"""

@staticmethod
def SayHello(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/helloworld.Greeter/SayHello',
helloworld__pb2.HelloRequest.SerializeToString,
helloworld__pb2.HelloReply.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

0 comments on commit 40f20c0

Please sign in to comment.