Package: nameko-grpc
nameko-grpcThis is a prototype implementation of a gRPC server and client for use innamekomicroservices.All four of the request-response patterns are implemented and tested:unary-unaryunary-streamstream-unarystream-streamAsynchronous calls are also supported for every pattern.Python 3.4+ is supported.Installation$ pip install nameko-grpcExampleServerExample Nameko service that can respond to gRPC requests:fromexample_pb2importExampleReplyfromexample_pb2_grpcimportexampleStubfromnameko_grpc.entrypointimportGrpcgrpc=Grpc.implementing(exampleStub)classExampleService:name="example"@grpcdefunary_unary(self,request,context):message=request.value*(request.multiplieror1)returnExampleReply(message=message)@grpcdefunary_stream(self,request,context):message=request.value*(request.multiplieror1)yieldExampleReply(message=message,seqno=1)yieldExampleReply(message=message,seqno=2)@grpcdefstream_unary(self,request,context):messages=[]forreqinrequest:message=req.value*(req.multiplieror1)messages.append(message)returnExampleReply(message=",".join(messages))@grpcdefstream_stream(self,request,context):forindex,reqinenumerate(request):message=req.value*(req.multiplieror1)yieldExampleReply(message=message,seqno=index+1)ClientExample Nameko service that can make gRPC requests:fromexample_pb2importExampleReplyfromexample_pb2_grpcimportexampleStubfromnameko.rpcimportrpcfromnameko_grpc.dependency_providerimportGrpcProxyclassClientService:name="client"example_grpc=GrpcProxy("//127.0.0.1",exampleStub)@rpcdefmethod(self):responses=self.example_grpc.unary_stream(ExampleRequest(value="A"))forresponseinresponses:print(response.message)Example standalone client, can be used with or without Eventlet:fromexample_pb2importExampleReplyfromexample_pb2_grpcimportexampleStubfromnameko_grpc.clientimportClientwithClient("//127.0.0.1",exampleStub)asclient:responses=client.unary_stream(ExampleRequest(value="A"))forresponseinresponses:print(response.message)ProtobufThe protobuf for the above examples is:syntax = "proto3";

package nameko;

service example {
  rpc unary_unary (ExampleRequest) returns (ExampleReply) {}
  rpc unary_stream (ExampleRequest) returns (stream ExampleReply) {}
  rpc stream_unary (stream ExampleRequest) returns (ExampleReply) {}
  rpc stream_stream (stream ExampleRequest) returns (stream ExampleReply) {}
}

message ExampleRequest {
  string value = 1;
  int32 multiplier = 2;
}


message ExampleReply {
  string message = 1;
  int32 seqno = 2;
}StyleThe example protobufs in this repo usesnake_casefor method names as per the Nameko conventions rather thanCamelCaseas per gRPC. This is not mandatory -- decorated method names simply match to the methods defined in the protobufs; similarly for service names.Context and MetadataInsofar as it is implemented, thecontextargument to service methods has the same API as the standard Python implementation:context.invocation_metadata()returns any metadata provided by the calling client.context.send_initial_metadata()can be used to add metadata to the response headers.context.set_trailing_metadata()can be used to add metadata to the response trailers.The standalone Client and DependencyProvider both allow metadata to be provided using themetadatakeyword argument. They accept a list of(name, value)tuples, just as the standard Python client does. Binary values must be base64 encoded and use a header name postfixed with "-bin", as in the standard Python client.gRPC request metadata is added to the "context data" of the Nameko worker context, so is availble to other Nameko extensions.The DependencyProvider client adds Nameko worker context data as metadata to all gRPC requests. This allows the Nameko call id stack to be populated and propagate, along with any other context data.CompressionCompression is supported in both the server and the client. Thedeflateandgzipalgorithms are available by default and will be included in thegrpc-accept-encodingheaders on requests from the client and responses from the server.The server honours any acceptable compression algorithm that it is able to, preferring to encode the response with the same algorithm as the request.A default compression algorithm is specified when creating the client, and/or can specified per-call using thecompressionkeyword argument:client=Client(default_compression="deflate",...)client.unary_unary(ExampleRequest(value="foo"),compression="gzip")# use gzip insteadCompression levels are not supported.The gRPC spec allows for the server to respond using a different algorithm from the request, or not compressing at all. This is not currently supported in the standard Python gRPC implementation nor nameko-grpc.ErrorsClient sidegRPC errors are raised by the client as instances of theGrpcErrorexception class. AGrpcErrorencapsulates the statuscodeand amessagestring describing the error. These are transmitted as thegrpc-statusandgrpc-messageheaders defined by the gRPC spec.Additionally,GrpcErrorhas astatusattribute that can hold agoogle.rpc.status.Statusprotobuf message for holding additional information about the error. This is similar to thegrpc_statuspackagethat is part of the official Python gRPC library, and indeed that package is compatible with the nameko-grpc client. (TODO test)Thegoogle.rpc.status.Statusmessage received in thegrpc-status-details-bintrailing header.Server sideIf a service method raises an exception, the error that is generated has the following attributes by default:* `code`: grpc.StatusCode.UNKNOWN
* `message`: "Exception calling application: <stringified exception>"
* `status`: `google.rpc.Status` protobuf messageThegoogle.rpc.Statusmessage encapsulates thecodeandmessageagain, along with adetailsattribute containing the exception traceback as agoogle.rpc.error_details.DebugInfomessage.You can customise the errors returned by the server in two ways:Explictly set the code, message, and trailing headers using thecontextobject. This is essentially how the official Python gRPC library does it:classService:...@grpcdefstream_error_via_context(self,request,context):forindex,iteminenumerate(...):ifindex>MAX_TOKENS:context.set_code(StatusCode.RESOURCE_EXHAUSTED)context.set_message("Out of tokens!")context.set_trailing_metadata([("grpc-status-details-bin",make_grpc_status(...))])breakyieldReply(...)Return aGrpcErrordirectly:fromnameko_grpc.errorsimportGrpcError,StatusCodeclassService:...@grpcdefstream_grpc_error(self,request,context):forindex,iteminenumerate(...):ifindex>MAX_TOKENS:raiseGrpcError(code=StatusCode.RESOURCE_EXHAUSTED,message="Out of tokens!",status=make_grpc_status(...))yieldReply(...)Register an error handler mapping a given exception type to a function that generates aGrpcErrorinstance from the raised exception:fromnameko_grpc.errorsimportregister_handler,GrpcError,StatusCodeclassNoMoreTokens(Exception):passdefhandle_no_more_tokens(exc,code=None,message=None):returnGrpcError(code=StatusCode.RESOURCE_EXHAUSTED,message=str(exc),details=make_grpc_status(...))register_handler(NoMoreTokens,handle_no_more_tokens)classService:...@grpcdefgrpc_error_from_exception(self,request,context):forindex,iteminenumerate(...):ifindex>MAX_TOKENS:raiseNoMoreTokens("Out of tokens!")yieldReply(...)The final approach is useful when you want to map an exception without wrapping it in a try/except in the service method, or when there is no opportunity to do so -- for example when an exception is raised by a decorator on the service method.TimeoutsThe client and server both support timeouts, and will raiseDEADLINE_EXCEEDEDif an RPC has not completed within the requested time. The clock starts ticking on the client when the request is initiated, and on the server when it is received.The deadline is calculated as the current time plus the timeout value.On the client, the timeout value is specified in seconds by using thetimeoutkeyword argument when invoking a method:client=Client(...)client.unary_unary(ExampleRequest(value="foo"),timeout=0.1)# 100 ms timeoutThere is no default because there's no sensible value applicable to all use-cases, but it isrecommendedto always set a deadline.TestsMost tests are run against every permutation of gRPC server/client to Nameko server/client. This roughly demonstrates equivalence between the two implementations. These tests are marked with the "equivalence" pytest marker.Additionally, we run the interop tests from the official gRPC repo, which are used to verify compatibility between language implementations. The Nameko gRPC implementation supports every feature that the official Python gRPC implementation does. These tests are marked with the "interop" pytest marker.Thetest/specdirectory contains the protobufs and server implementations used in the various tests.Running the testsClone or download the repository, and ensure the development dependencies are installed:$ pip install nameko-grpc[dev]Then run the tests:$ pytest testThe interop tests require docker. They use the image athttps://hub.docker.com/r/nameko/nameko-grpc-interopwhich contains the pre-built C++ interop client. To run all tests excluding the interop tests:$ pytest test -m "not interop"Implementation NotesgRPC is built on HTTP2, so nameko-grpc relies heavily on thehyper-h2library. H2 is a finite state-machine implementation of the HTTP2 protocol, and its documentation is very good. The code in nameko-grpc is much more understandable when you're familiar with h2.Much of the heavy-lifting in nameko-grpc is done by either the server or client subclasses ofConnectionManager. AConnectionManagerhandles a single HTTP2 connection, and implements the handlers for each HTTP2 event on that connection (e.g.request_receivedorstream_ended). See:nameko_grpc/client.py::ClientConnectionManagernameko_grpc/entrypoint.py::ServerConnectionManagernameko_grpc/connection.py::ConnectionManagerThe next most significant module isnameko_grpc/streams.py. This module contains theSendStreamandReceiveStreamclasses, which represent an HTTP2 stream that is being sent or received, respectively. AReceiveStreamreceives data as bytes from aConnectionManager, and parses them into a stream of gRPC messages. ASendStreamdoes the opposite, encoding gRPC messages into bytes that can be sent across an HTTP2 connection.The@grpcEntrypoint is a normal Nameko entrypoint that executes a service method when an appropriate request is made. The entrypoint deals with aReceiveStreamobject encapsulating the request, and aSendStreamobject that accepts the response. The streams are managed by a sharedGrpcServer, which accepts incoming connections and wraps each in aServerConnectionManager.The standalone Client is a small wrapper around aClientConnectionManager. The Client simply creates a socket connection and then hands it to the connection manager. When a method is invoked on the client, the connection manager initiates an appropriate request. The headers for that request describe the method being invoked, encodings, message types etc. This logic is all encapsulated into theMethodclass.The gRPC DependencyProvider is a normal Nameko DependencyProvider, which is also just a small wrapper around aClientConnectionManager. It functions in exactly the same manner as the standalone Client.Equivalence tests notesTo demonstrate equivalence between the nameko-grpc implementations and the standard gRPC implementations, all tests marked with theequivalencemarker run against every permutation of:gRPC standard server (Python implementation) orNameko serverandgRPC standard client (Python implementation) orNameko standalone client orNameko DependencyProvider clientNameko uses Eventlet for concurrency, which is incompatible with the standard gRPC server and client. Consequently, these must be run in a separate process and somehow communicated with in order to make assertions about the behaviour of the standard implementation.The scripts which run the out-of-process client and server can be found intest/grpc_indirect_client.pyandtest/grpc_indirect_server.pyThe communication is done with ZeroMQ. The logic for this is contained within theRemoteClientTransportandCommandclasses withintest/helpers.py, and thestart_grpc_clientandstart_grpc_serverfixtures intest/conftest.py.In the future this arrangement would allow us to run equivalence tests against a different (more feature-complete) standard gRPC implementation.