asio-grpc

repository·master·Indexed 19 days ago

https://github.com/tradias/asio-grpc

A header-only C++17 library providing an Asio-compatible interface to gRPC's CompletionQueue. It enables the use of modern concurrency primitives—including C++20 coroutines, sender/receiver patterns (libunifex/stdexec), and Boost.Coroutines—for unary, client-streaming, server-streaming, and bidirectional-streaming RPCs without requiring extra codegen.

Tokens
22K
Snippets
69
Records
93
Agent score
61%

What's inside asio-grpc

  1. Overview of asio-grpc

    master

    asio-grpc provides an Executor, Networking TS, and std::execution interface to grpc::CompletionQueue. It allows developers to write asynchronous gRPC clients and servers using modern C++ concurrency patterns, including C++20 coroutines, Boost.Coroutines, Asio's stackless coroutines, callbacks, and sender/receiver models.

    Key features include:

    • Asio Compatibility: An ExecutionContext compatible wrapper around grpc::CompletionQueue.
    • RPC Support: Supports unary, client-streaming, server-streaming, and bidirectional-streaming RPCs.
    • Flexible Completion Tokens: Works with any Asio CompletionToken or Sender, including allocator customization.
    • Alarm Support: Asynchronous waiting for grpc::Alarms with support for cancellation_slots and StopTokens.
    • Sender/Receiver Support: Compatible with libunifex or stdexec.
    • No Extra Codegen: Works with the standard vanilla gRPC C++ plugin (grpc_cpp_plugin).
    • Flexible Dependencies: Offers a No-Boost version (using standalone Asio) and a No-Asio version (using libunifex or stdexec).
  2. Overview of asio-grpc core components

    master

    The asio-grpc library provides an Asio-based interface for gRPC. The core architecture revolves around two main workhorses:

    • agrpc::GrpcContext: The primary execution context for gRPC operations.
    • agrpc::GrpcExecutor: The executor used to drive gRPC tasks.

    Key functional areas include:

    • Asynchronous Clients: Managed via agrpc::ClientRPC and various reactor patterns.
    • Asynchronous Servers: Managed via agrpc::ServerRPC and various registration handlers (awaitable, yield, sender, callback, or coroutine).
    • Timers: Provided by agrpc::Alarm.
    • Concurrency Utilities: agrpc::Waiter allows for Rust/Golang-style select operations.
    • Health Checks: agrpc::HealthCheckService serves as a high-performance replacement for gRPC's DefaultHealthCheckService.
  3. Manage `io_context` and `GrpcContext` threading (Server)

    master

    When designing a server, you can choose how to manage the relationship between your application's io_context and the gRPC GrpcContext:

    • Shared Threading: Run the io_context and GrpcContext on the same thread. This is useful when integrating gRPC into an existing HTTP server to avoid synchronization complexity.
    • Separate Threading: Use the application's io_context as the main context and run the GrpcContext on a separate dedicated thread.
    // Example showing how to run an io_context and a GrpcContext on the same thread for gRPC servers.
    
    // Example showing how to use an io_context as the main context and a GrpcContext on a separate thread for gRPC servers.
  4. Implement multi-threaded Asio clients

    master

    There are two primary patterns for scaling Asio gRPC clients across multiple threads:

    1. Multiple GrpcContext instances: Use a separate GrpcContext for each thread/group of threads.
    2. Single GrpcContext instance: Use a single GrpcContext shared across multiple threads.
    // Multi-threaded client using multiple GrpcContexts
    
    // Multi-threaded client using single a GrpcContext
  5. Why io_context cannot handle gRPC directly

    master

    Directly using asio::io_context for gRPC operations is currently not possible due to how event loops interact with system APIs (like epoll, kqueue, or IOCompletionPorts).

    For Asio and gRPC to interoperate perfectly, they would need to collect all network file descriptors (sockets, pipes, etc.) and perform a single system call (e.g., poll) to wait on all of them at once. However, these file descriptors are managed deep within the internal implementation of the respective libraries, and the sleep/wait calls are also encapsulated.

    While gRPC is working on an EventEngine to potentially allow using Asio sockets for gRPC, a full integration for all gRPC network operations is not yet available.

  6. Interoperate between GrpcContext and asio::io_context

    master

    Because of limitations in the gRPC CompletionQueue and Callback API, an asio::io_context cannot handle gRPC RPCs directly. Instead, you must interoperate between a GrpcContext (which handles gRPC operations) and an asio::io_context (which handles standard Asio I/O operations like TCP sockets).

    There are two primary ways to manage these contexts:

    1. Implicitly constructed io_context

    Since GrpcContext is an asio::execution_context, it supports Asio's Service mechanism. You can use an Asio I/O object (like signal_set or ip::tcp::socket) directly with a GrpcContext. This implicitly creates an io_context, starts a background thread, and runs it. The completion of the Asio operation is then posted to the GrpcContext where your lambda is invoked.

    Pros: Most convenient approach. Cons: The io_context cannot be run on more than one thread, and there is runtime overhead due to non-customizable thread switching.

    2. Explicitly constructed io_context

    You can create both GrpcContext and io_context manually. This allows you to decide which one acts as the "main" context and how they are executed.

    Execution Strategies:

    • Separate Threads: Run the io_context and GrpcContext on different threads.
    • Same Thread: Run both contexts on the same thread using agrpc::run_io_context_and_grpc_context (runs until GrpcContext stops) or agrpc::run_io_context_shared_work_tracking (runs until both stop).

    Pros/Cons: Running on separate threads may require additional synchronization in your code, while running on the same thread reduces peak performance but simplifies concurrency management.

  7. How Completion Tokens work in asio-grpc

    master
    All asynchronous functions in asio-grpc accept a CompletionToken as their final argument. This token determines how the library notifies you that an asynchronous operation (like an RPC) has completed. Common patterns include using a callback, using Boost.Coroutine, or using agrpc::use_sender to integrate with sender/receiver frameworks.
  8. Implement multi-threaded Asio servers

    master

    Scale your gRPC server using two main patterns:

    1. Multiple GrpcContext instances: Use the callback API to handle unary requests across multiple GrpcContext instances.
    2. Single GrpcContext instance: Use a single GrpcContext to handle unary requests via the callback API.
    // Multi-threaded server handling unary requests using callback API and multiple GrpcContexts
    
    // Multi-threaded server handling unary requests using callback API and single GrpcContext
  9. Use the Asio'nized gRPC callback API

    master

    For fine-grained control over gRPC lifecycle events using a reactor pattern, asio-grpc provides several Basic reactor classes for both clients and servers. These allow you to handle unary, streaming (read/write), and bidirectional (bidi) RPCs using Asio-style asynchronous patterns.

    ### Server Reactors
    - `agrpc::BasicServerUnaryReactor` 
    - `agrpc::BasicServerReadReactor` 
    - `agrpc::BasicServerWriteReactor` 
    - `agrpc::BasicServerBidiReactor` 
    
    ### Client Reactors
    - `agrpc::BasicClientUnaryReactor` 
    - `agrpc::BasicClientWriteReactor` 
    - `agrpc::BasicClientReadReactor` 
    - `agrpc::BasicClientBidiReactor`
  10. Understand the core abstractions: GrpcContext and GrpcExecutor

    master

    The library is built around two primary abstractions that integrate gRPC with Asio:

    1. agrpc::GrpcContext: Implements asio::execution_context. It serves as the central hub for managing gRPC operations and can be passed to any Asio function requiring an ExecutionContext (e.g., asio::spawn).
    2. agrpc::GrpcExecutor: Satisfies the Executor and Networking TS requirements, as well as the Scheduler concept. This allows it to be used anywhere Asio or libunifex expects an Executor or Scheduler.

    These components allow you to treat gRPC operations as standard asynchronous tasks within the Asio ecosystem.

  11. Use CompletionTokens for asynchronous RPCs

    master

    The RPC API is modeled after the asynchronous, tag-based gRPC API but replaces the void* tag with Asio's CompletionToken mechanism. This allows you to use various asynchronous patterns for RPC calls.

    Supported completion patterns include:

    • C++20 Coroutines: Using asio::use_awaitable.
    • Stackless Coroutines: Using asio::coroutine.
    • Callbacks: Passing a standard handler/callback.
    • Boost.Coroutine: Using asio::yield_context.
    • Senders: Using the special token agrpc::use_sender to return a Sender (compatible with libunifex).

    For example, a gRPC Read operation that normally uses a void* tag is performed in asio-grpc using agrpc::ClientRPC.read(message, CompletionToken).

    // Conceptual comparison:
    // Standard gRPC: 
    // grpc::ClientAsyncReader<Reply>.Read(Reply*, void* tag);
    
    // asio-grpc:
    // agrpc::ClientRPC.read(reply_instance, CompletionToken);
  12. Implement generic Asio client-side requests

    master

    For scenarios requiring generic request handling, you can use Asio's stackless coroutines or Boost.Coroutine:

    • Generic Unary: A simple unary request using Boost.Coroutine.
    • Generic Bidirectional-streaming: A bidirectional stream using Asio's stackless coroutines.
    // A simple generic unary with Boost.Coroutine.
    
    // A generic bidirectional-streaming request that simply sends the response from the server back to it using Asio's stackless coroutines.