refactor(transport): implemented Bi stream based abstraction for actor communication with next loop override
This commit is contained in:
@@ -1,46 +1,283 @@
|
||||
use futures::{Stream, StreamExt};
|
||||
use tokio::sync::mpsc::{self, error::SendError};
|
||||
use tonic::{Status, Streaming};
|
||||
//! Transport-facing abstractions for protocol/session code.
|
||||
//!
|
||||
//! This module separates three concerns:
|
||||
//!
|
||||
//! - protocol/session logic wants a small duplex interface ([`Bi`])
|
||||
//! - transport adapters need to push concrete stream items to an underlying IO layer
|
||||
//! - server/client boundaries may need to translate domain outbounds into transport
|
||||
//! framing (for example, a tonic stream item)
|
||||
//!
|
||||
//! [`Bi`] is intentionally minimal and transport-agnostic:
|
||||
//! - [`Bi::recv`] yields inbound protocol messages
|
||||
//! - [`Bi::send`] accepts outbound protocol/domain items
|
||||
//!
|
||||
//! # Generic Ordering Rule
|
||||
//!
|
||||
//! This module uses a single convention consistently: when a type or trait is
|
||||
//! parameterized by protocol message directions, the generic parameters are
|
||||
//! declared as `Inbound` first, then `Outbound`.
|
||||
//!
|
||||
//! For [`Bi`], that means `Bi<Inbound, Outbound>`:
|
||||
//! - `recv() -> Option<Inbound>`
|
||||
//! - `send(Outbound)`
|
||||
//!
|
||||
//! For adapter types that are parameterized by direction-specific converters,
|
||||
//! inbound-related converter parameters are declared before outbound-related
|
||||
//! converter parameters.
|
||||
//!
|
||||
//! [`ProtocolConverter`] is the boundary object that converts a protocol/domain
|
||||
//! outbound item into the concrete outbound item expected by a transport sender.
|
||||
//! The conversion is infallible, so domain-level recoverable failures should be
|
||||
//! represented inside the domain outbound type itself (for example,
|
||||
//! `Result<Message, DomainError>`).
|
||||
//!
|
||||
//! [`GrpcAdapter`] combines:
|
||||
//! - a tonic inbound stream
|
||||
//! - a Tokio sender for outbound transport items
|
||||
//! - a [`ProtocolConverter`] for the receive path
|
||||
//! - a [`ProtocolConverter`] for the send path
|
||||
//!
|
||||
//! [`DummyTransport`] is a no-op implementation useful for tests and local actor
|
||||
//! execution where no real network stream exists.
|
||||
//!
|
||||
//! # Component Interaction
|
||||
//!
|
||||
//! The typical layering looks like this:
|
||||
//!
|
||||
//! ```text
|
||||
//! inbound (network -> protocol)
|
||||
//! ============================
|
||||
//!
|
||||
//! tonic::Streaming<RecvTransport> -> GrpcAdapter::recv() -> Bi::recv() -> protocol/session actor
|
||||
//! |
|
||||
//! +--> recv ProtocolConverter::convert(transport)
|
||||
//!
|
||||
//! outbound (protocol -> network)
|
||||
//! ==============================
|
||||
//!
|
||||
//! protocol/session actor -> Bi::send(domain outbound item, e.g. Result<Message, DomainError>)
|
||||
//! -> GrpcAdapter::send()
|
||||
//! |
|
||||
//! +--> send ProtocolConverter::convert(domain)
|
||||
//! -> Tokio mpsc::Sender<SendTransport> -> tonic response stream
|
||||
//! ```
|
||||
//!
|
||||
//! # Design Notes
|
||||
//!
|
||||
//! - `recv()` collapses adapter-specific receive failures into `None`, which
|
||||
//! lets protocol code treat stream termination and transport receive failure as
|
||||
//! "no more inbound items" when no finer distinction is required.
|
||||
//! - `send()` returns [`Error`] only for transport delivery failures (for example,
|
||||
//! when the outbound channel is closed).
|
||||
//! - Conversion policy lives outside protocol/session logic and can be defined at
|
||||
//! the transport boundary (such as a server endpoint module). When domain and
|
||||
//! transport types are identical, [`IdentityConverter`] can be used.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
// Abstraction for stream for sans-io capabilities
|
||||
pub trait Bi<T, U>: Stream<Item = Result<T, Status>> + Send + Sync + 'static {
|
||||
type Error;
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::mpsc;
|
||||
use tonic::Streaming;
|
||||
|
||||
/// Errors returned by transport adapters implementing [`Bi`].
|
||||
pub enum Error {
|
||||
/// The outbound side of the transport is no longer accepting messages.
|
||||
ChannelClosed,
|
||||
}
|
||||
|
||||
/// Minimal bidirectional transport abstraction used by protocol code.
|
||||
///
|
||||
/// `Bi<Inbound, Outbound>` models a duplex channel with:
|
||||
/// - inbound items of type `Inbound` read via [`Bi::recv`]
|
||||
/// - outbound items of type `Outbound` written via [`Bi::send`]
|
||||
///
|
||||
/// The trait intentionally exposes only the operations the protocol layer needs,
|
||||
/// allowing it to work with gRPC streams and other transport implementations.
|
||||
///
|
||||
/// # Stream termination and errors
|
||||
///
|
||||
/// [`Bi::recv`] returns:
|
||||
/// - `Some(item)` when a new inbound message is available
|
||||
/// - `None` when the inbound stream ends or the underlying transport reports an error
|
||||
///
|
||||
/// Implementations may collapse transport-specific receive errors into `None`
|
||||
/// when the protocol does not need to distinguish them from normal stream
|
||||
/// termination.
|
||||
pub trait Bi<Inbound, Outbound>: Send + Sync + 'static {
|
||||
/// Sends one outbound item to the peer.
|
||||
fn send(
|
||||
&mut self,
|
||||
item: Result<U, Status>,
|
||||
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
|
||||
item: Outbound,
|
||||
) -> impl std::future::Future<Output = Result<(), Error>> + Send;
|
||||
|
||||
/// Receives the next inbound item.
|
||||
///
|
||||
/// Returns `None` when the inbound stream is finished or can no longer
|
||||
/// produce items.
|
||||
fn recv(&mut self) -> impl std::future::Future<Output = Option<Inbound>> + Send;
|
||||
}
|
||||
|
||||
// Bi-directional stream abstraction for handling gRPC streaming requests and responses
|
||||
pub struct BiStream<T, U> {
|
||||
pub request_stream: Streaming<T>,
|
||||
pub response_sender: mpsc::Sender<Result<U, Status>>,
|
||||
/// Converts protocol/domain outbound items into transport-layer outbound items.
|
||||
///
|
||||
/// This trait is used by transport adapters that need to emit a concrete stream
|
||||
/// item type (for example, tonic server streams) while protocol code prefers to
|
||||
/// work with domain-oriented outbound values.
|
||||
///
|
||||
/// `convert` is infallible by design. Any recoverable protocol failure should be
|
||||
/// represented in [`Self::Domain`] and mapped into the transport item in the
|
||||
/// converter implementation.
|
||||
pub trait ProtocolConverter: Send + Sync + 'static {
|
||||
/// Outbound item produced by protocol/domain code.
|
||||
type Domain;
|
||||
|
||||
/// Outbound item required by the transport sender.
|
||||
type Transport;
|
||||
|
||||
/// Maps a protocol/domain outbound item into the transport sender item.
|
||||
fn convert(&self, item: Self::Domain) -> Self::Transport;
|
||||
}
|
||||
|
||||
impl<T, U> Stream for BiStream<T, U>
|
||||
where
|
||||
T: Send + 'static,
|
||||
U: Send + 'static,
|
||||
{
|
||||
type Item = Result<T, Status>;
|
||||
/// A [`ProtocolConverter`] that forwards values unchanged.
|
||||
///
|
||||
/// Useful when the protocol-facing and transport-facing item types are
|
||||
/// identical, but a converter is still required by an adapter API.
|
||||
pub struct IdentityConverter<T> {
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
self.request_stream.poll_next_unpin(cx)
|
||||
impl<T> IdentityConverter<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Bi<T, U> for BiStream<T, U>
|
||||
where
|
||||
T: Send + 'static,
|
||||
U: Send + 'static,
|
||||
{
|
||||
type Error = SendError<Result<U, Status>>;
|
||||
|
||||
async fn send(&mut self, item: Result<U, Status>) -> Result<(), Self::Error> {
|
||||
self.response_sender.send(item).await
|
||||
impl<T> Default for IdentityConverter<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ProtocolConverter for IdentityConverter<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
type Domain = T;
|
||||
type Transport = T;
|
||||
|
||||
fn convert(&self, item: Self::Domain) -> Self::Transport {
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Bi`] adapter backed by a tonic gRPC bidirectional stream.
|
||||
///
|
||||
/// The adapter owns converter instances for both directions:
|
||||
/// - receive converter: transport inbound -> protocol inbound
|
||||
/// - send converter: protocol outbound -> transport outbound
|
||||
///
|
||||
/// This keeps protocol actors decoupled from transport framing conventions in
|
||||
/// both directions.
|
||||
pub struct GrpcAdapter<InboundConverter: ProtocolConverter, OutboundConverter: ProtocolConverter> {
|
||||
sender: mpsc::Sender<OutboundConverter::Transport>,
|
||||
receiver: Streaming<InboundConverter::Domain>,
|
||||
inbound_converter: InboundConverter,
|
||||
outbound_converter: OutboundConverter,
|
||||
}
|
||||
|
||||
impl<InboundConverter, OutboundConverter> GrpcAdapter<InboundConverter, OutboundConverter>
|
||||
where
|
||||
InboundConverter: ProtocolConverter,
|
||||
OutboundConverter: ProtocolConverter,
|
||||
{
|
||||
/// Creates a new gRPC-backed [`Bi`] adapter.
|
||||
///
|
||||
/// The provided converters define:
|
||||
/// - the protocol outbound item and corresponding transport outbound item
|
||||
/// - the transport inbound item and corresponding protocol inbound item
|
||||
pub fn new(
|
||||
sender: mpsc::Sender<OutboundConverter::Transport>,
|
||||
receiver: Streaming<InboundConverter::Domain>,
|
||||
inbound_converter: InboundConverter,
|
||||
outbound_converter: OutboundConverter,
|
||||
) -> Self {
|
||||
Self {
|
||||
sender,
|
||||
receiver,
|
||||
inbound_converter,
|
||||
outbound_converter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<InboundConverter, OutboundConverter>
|
||||
Bi<InboundConverter::Transport, OutboundConverter::Domain>
|
||||
for GrpcAdapter<InboundConverter, OutboundConverter>
|
||||
where
|
||||
InboundConverter: ProtocolConverter,
|
||||
OutboundConverter: ProtocolConverter,
|
||||
OutboundConverter::Domain: Send + 'static,
|
||||
OutboundConverter::Transport: Send + 'static,
|
||||
InboundConverter::Transport: Send + 'static,
|
||||
InboundConverter::Domain: Send + 'static,
|
||||
{
|
||||
#[tracing::instrument(level = "trace", skip(self, item))]
|
||||
async fn send(&mut self, item: OutboundConverter::Domain) -> Result<(), Error> {
|
||||
let outbound: OutboundConverter::Transport = self.outbound_converter.convert(item);
|
||||
self.sender
|
||||
.send(outbound)
|
||||
.await
|
||||
.map_err(|_| Error::ChannelClosed)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
async fn recv(&mut self) -> Option<InboundConverter::Transport> {
|
||||
self.receiver
|
||||
.next()
|
||||
.await
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|item| self.inbound_converter.convert(item))
|
||||
}
|
||||
}
|
||||
|
||||
/// No-op [`Bi`] transport for tests and manual actor usage.
|
||||
///
|
||||
/// `send` drops all items and succeeds. [`Bi::recv`] never resolves and therefore
|
||||
/// does not busy-wait or spuriously close the stream.
|
||||
pub struct DummyTransport<Inbound, Outbound> {
|
||||
_marker: PhantomData<(Inbound, Outbound)>,
|
||||
}
|
||||
|
||||
impl<Inbound, Outbound> DummyTransport<Inbound, Outbound> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inbound, Outbound> Default for DummyTransport<Inbound, Outbound> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inbound, Outbound> Bi<Inbound, Outbound> for DummyTransport<Inbound, Outbound>
|
||||
where
|
||||
Inbound: Send + Sync + 'static,
|
||||
Outbound: Send + Sync + 'static,
|
||||
{
|
||||
async fn send(&mut self, _item: Outbound) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv(&mut self) -> impl std::future::Future<Output = Option<Inbound>> + Send {
|
||||
async {
|
||||
std::future::pending::<()>().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user