grpclib Documentation

repository·master·Indexed 21 days ago

https://github.com/vmagamedov/grpclib

A pure-Python implementation of gRPC designed for asyncio, providing a high-performance asynchronous alternative to grpcio. It supports unary and streaming RPC calls, SSL/TLS secure channels, and custom codecs for non-protobuf encoding. Features include a Configuration class for tuning HTTP/2 settings, support for rich error details via google.rpc.Status, and a protoc plugin for generating Python stubs.

Tokens
12.7K
Snippets
45
Records
64
Agent score
76%

What's inside grpclib

  1. Understanding gRPC Deadlines and timeouts

    master

    Deadlines in grpclib function as propagated timeouts. They allow a timeout constraint to be passed through a chain of services to ensure the entire call chain respects the initial time limit.

    The Deadline Lifecycle:

    1. A service receives a request with a grpc-timeout in the metadata (e.g., 100m for 100 milliseconds).
    2. The service converts this into an absolute deadline: deadline = time.monotonic() + grpc_timeout.
    3. When making an outgoing request to another service, the service calculates the remaining time: new_timeout = max(deadline - time.monotonic(), 0).
    4. The outgoing request is sent with the new grpc-timeout metadata.

    This mechanism allows for simultaneous cancellation of an entire call chain, even in the event of network failures or broken connections.

    # Example of converting timeout to deadline and calculating remaining time
    import time
    
    # 1. Convert incoming timeout to absolute deadline
    grpc_timeout = 0.1  # 100ms
    deadline = time.monotonic() + grpc_timeout
    
    # 2. Simulate work
    time.sleep(0.02)  # 20ms work
    
    # 3. Calculate remaining timeout for the next service
    new_timeout = max(deadline - time.monotonic(), 0)  # Result: ~0.08s (80ms)
  2. Enable Health Checking and Reflection

    master

    To support standard gRPC operations, you can implement:

    • gRPC Health Checking Protocol: Used to monitor the health of your services.
    • gRPC Reflection Protocol: Allows inspecting running servers and calling methods via command-line tools (e.g., using curl).
  3. How gRPC cancellation works in grpclib

    master

    Because grpclib is built on the HTTP/2 (h2) protocol, it supports individual stream cancellation via the RST_STREAM frame. This allows a client to cancel a specific gRPC method call without dropping the entire TCP connection.

    When a client cancels a request, the server receives the RST_STREAM frame and can immediately cancel the task handling that specific request. This prevents the server from wasting resources on computations for results that are no longer needed.

  4. Follow gRPC naming conventions for service paths

    master

    Even when not using Protocol Buffers, you should follow the standard gRPC naming conventions to ensure correct :path pseudo-header construction. The path is built as:

    :path = /dotted.package.CamelCaseServiceName/CamelCaseMethodName

    According to the Protocol Buffers Style Guide, you should use CamelCase (with an initial capital) for both the service name and any RPC method names.

  5. How gRPC metadata works in grpclib

    master

    gRPC metadata follows the HTTP/2 pattern where metadata is sent as headers. It is categorized into three types:

    1. Request Metadata: Sent by the client to the server.
    2. Initial Metadata: Sent by the server immediately after receiving the request.
    3. Trailing Metadata: Sent by the server after the response data (includes grpc-status).

    Metadata Types

    • Text Metadata: Keys are standard strings. Values are received as str in Python.
    • Binary Metadata: Keys must have a -bin suffix. Values are automatically base64 encoded/decoded by grpclib. In Python, these are received as bytes.

    Note: Keys starting with grpc- are reserved for the protocol.

  6. How event properties and callbacks work in grpclib

    master

    The grpclib.events system follows these rules:

    1. Property Types:
      • Mutable: Properties you can change (e.g., event.metadata) that will affect the actual gRPC operation.
      • Read-only: Properties that can only be read.
    2. Callback Execution: Callbacks are executed in the order they were added (First-In, First-Out).
    3. Interrupting Sequences: A callback can call event.interrupt() to stop the sequence of calls for a particular event. This is often used to provide a custom RPC handler (e.g., returning an error instead of the intended method).
    from grpclib.events import RecvRequest
    from grpclib.exceptions import GRPCError
    from grpclib.server import Status
    
    async def authn_error(stream):
        # Custom handler that returns an error
        raise GRPCError(Status.UNAUTHENTICATED)
    
    async def recv_request(event: RecvRequest):
        if event.metadata.get('auth-token') != SECRET:
            # Replace the method function with the error handler
            event.method_func = authn_error
            # Stop the normal execution flow
            event.interrupt()
    
    listen(server, RecvRequest, recv_request)
  7. Implement gRPC Health Checking in grpclib

    master

    grpclib implements the standard gRPC Health Checking Protocol, providing both the unary Check method for synchronous checks and the unary-stream Watch method for asynchronous status updates.

    There are two primary ways to implement health checks:

    1. ServiceCheck: The simplest and most generic way for periodic checks. You provide an asynchronous callable that returns the status.
    2. ServiceStatus: A more advanced and efficient method for proactive status changes (e.g., reacting immediately to a lost connection) using the .set() method.
  8. gRPC method types

    master

    gRPC supports four distinct method types based on the number of messages sent in each direction:

    1. unary-unary: Exactly one message sent by client, exactly one response from server.
    2. unary-stream: Exactly one message sent by client, any number of messages sent by server (e.g., a download).
    3. stream-unary: Any number of messages sent by client, exactly one response from server (e.g., an upload).
    4. stream-stream: Any number of messages sent by client, any number of messages sent by server.
  9. Send and receive rich error details using google.rpc.Status

    master

    Beyond standard status codes and messages, you can send rich error details using the google.rpc.Status message format. This allows you to attach structured metadata (like field violations) to an error.

    Setup

    To enable automatic decoding of these rich details, you must install the following package:

    $ pip3 install googleapis-common-protos

    Important: Decoding Details

    To automatically decode error details, you must import the specific message types you expect to receive. If you do not import them, the error details will appear as stubs (e.g., Unknown('google.rpc.QuotaFailure')) instead of the actual objects.

    Server-Side: Sending Details

    When raising a GRPCError on the server, pass a list of messages as the third argument to include rich details.

    Client-Side: Handling Details

    On the client, catch GRPCError and iterate through the details attribute, using isinstance() to identify specific error types.

    ### Server-Side Example
    from google.rpc.error_details_pb2 import BadRequest
    
    async def Method(self, stream):
        ...
        raise GRPCError(
            Status.INVALID_ARGUMENT,
            'Request validation failed',
            [
                BadRequest(
                    field_violations=[
                        BadRequest.FieldViolation(
                            field='title',
                            description='This field is required',
                        ),
                    ],
                ),
            ],
        )
    
    ### Client-Side Example
    from google.rpc.error_details_pb2 import BadRequest
    
    try:
        reply = await stub.Method(Request(...))
    except GRPCError as err:
        if err.details:
            for detail in err.details:
                if isinstance(detail, BadRequest):
                    for violation in detail.field_violations:
                        print(f'{violation.field}: {violation.description}')
  10. Configure Detailed Service-Specific Health Checks

    master

    You can provide granular health status for specific gRPC services by mapping service instances (or names) to lists of check callables in the Health constructor.

    • A specific service is SERVING if all its associated checks pass.
    • The OVERALL status is determined by the checks assigned to the OVERALL key. If OVERALL is not explicitly defined, its status depends on all checks registered across all services.
    • You can override the OVERALL check list to include a specific subset of checks that do not represent every single service check.
    foo = FooService()
    bar = BarService()
    
    # Mapping specific services to specific checks
    health = Health({
        foo: [a_check, b_check],
        bar: [b_check, c_check],
        OVERALL: [a_check, c_check], # Overriding overall status logic
    })
    # Test specific service health
    $ grpc_health_probe -addr=localhost:50051 -service acme.FooService
    
    # Test overall health
    $ grpc_health_probe -addr=localhost:50051
  11. Manage complex application lifecycles with AsyncExitStack

    master

    For applications requiring complex resource management (like database connections) alongside the gRPC server, use contextlib.AsyncExitStack. This allows you to manage the lifecycle of multiple asynchronous resources and ensure the graceful_exit handler is correctly registered within the stack.

    from contextlib import AsyncExitStack
    from grpclib.utils import graceful_exit
    
    async with AsyncExitStack() as stack:
        # Setup resources
        db = await stack.enter_async_context(setup_db())
        foo_svc = FooService(db)
    
        # Setup server
        server = Server([foo_svc])
        
        # Register graceful exit in the stack
        stack.enter_context(graceful_exit([server]))
        
        await server.start(host, port)
        print(f'Serving on {host}:{port}')
        await server.wait_closed()
  12. Test gRPC services in-memory with grpclib.testing

    master
    You can test your gRPC services without setting up actual network interfaces. grpclib provides a testing utility that allows you to use real client-side code, real server-side code, and the actual h2/gRPC protocol, but keeps all data transmission in-memory. This is achieved using the ChannelFor abstraction from the grpclib.testing module.