Skip to main content

dolang_rpc/
lib.rs

1#![deny(warnings)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3//! Framed, multiplexed RPC sessions over asynchronous byte streams.
4//!
5//! Define a [`Protocol`], negotiate a transport with [`Builder`], then bind
6//! the negotiated endpoint to that protocol. The client may issue concurrent
7//! calls; [`Server::serve`](server::Server::serve) dispatches concurrent request handlers.
8//!
9//! ```no_run
10//! use dolang_rpc::{Builder, Protocol, server::CallContext};
11//! use serde::{Deserialize, Serialize};
12//! use tokio_util::compat::TokioAsyncReadCompatExt as _;
13//!
14//! #[derive(Deserialize, Serialize)]
15//! enum Request { Ping }
16//! #[derive(Deserialize, Serialize)]
17//! enum Response { Pong }
18//! struct Example;
19//! impl Protocol for Example {
20//!     type Request = Request;
21//!     type Response = Response;
22//! }
23//!
24//! async fn run() -> Result<(), Box<dyn std::error::Error>> {
25//!     let (client_io, server_io) = tokio::io::duplex(16 * 1024);
26//!     let (client, server) = tokio::try_join!(
27//!         Builder::new("example", &[1]).client(client_io.compat()),
28//!         Builder::new("example", &[1]).server(server_io.compat()),
29//!     )?;
30//!
31//!     let server = async {
32//!         server.bind::<Example>().serve(async |mut context: CallContext<Example>, request| {
33//!             context.shutdown();
34//!             match request {
35//!                 Request::Ping => context.respond(Response::Pong),
36//!             }
37//!         }).await
38//!     };
39//!     let client = async {
40//!         let response = client.bind::<Example>().call(Request::Ping).await?.into_response();
41//!         assert!(matches!(response, Response::Pong));
42//!         Ok::<_, dolang_rpc::Error>(())
43//!     };
44//!     let (server, client) = tokio::join!(server, client);
45//!     server?;
46//!     client?;
47//!     Ok(())
48//! }
49//! ```
50
51pub mod auth;
52pub mod client;
53mod driver;
54#[cfg(target_os = "macos")]
55mod escrow;
56mod fragment;
57pub mod handle;
58mod serde;
59pub mod server;
60pub mod session;
61pub mod trailer;
62mod transport;
63mod unbound;
64mod window;
65
66use std::io;
67
68use ::serde::{Serialize, de::DeserializeOwned};
69pub use unbound::Builder;
70
71/// Configurable size and concurrency limits for a session. Not public — set
72/// via [`Builder`]'s chainable setters instead.
73#[derive(Clone, Copy, Debug)]
74pub(crate) struct Limits {
75    /// Maximum size of one whole wire fragment, header included. Bounds how
76    /// much of a large message is written per round-robin turn; the header
77    /// is subtracted from this to get the actual payload budget per write.
78    pub max_fragment_size: usize,
79    /// Maximum size of one complete (reassembled) message's postcard
80    /// payload, excluding any trailer.
81    pub max_payload_size: usize,
82    /// Maximum number of native handles attached to one wire fragment.
83    pub max_handles_per_fragment: usize,
84    /// Maximum number of native handles carried by one message.
85    pub max_handles_per_message: usize,
86    /// Maximum trailer fragment payload copied immediately by the receive
87    /// driver when the consumer has not yet requested that fragment. Set to
88    /// zero to disable copying nonempty fragments on this path.
89    pub trailer_recv_copy_threshold: usize,
90    /// Maximum trailer fragment payload copied immediately by the receive
91    /// driver when the consumer is already waiting for that fragment. Set to
92    /// zero to disable copying nonempty fragments on this path.
93    pub trailer_recv_demand_copy_threshold: usize,
94    /// Maximum trailer fragment payload copied into staging by
95    /// `TrailerSend::poll_write` without first waiting for a transport grant.
96    /// Set to zero to disable copying nonempty fragments on this path.
97    pub trailer_send_copy_threshold: usize,
98    /// Maximum number of concurrent calls, counted from the first fragment
99    /// of a request to its response.
100    ///
101    /// A call spends that span in two custodians — the reassembler while its
102    /// payload is still arriving, then the endpoint until it answers — and
103    /// the limit is on the sum, so a peer cannot get twice the budget by
104    /// keeping half its calls in each. Messages that have finished their
105    /// payload and entered their trailer phase are *not* counted: those are
106    /// bounded by the credit window below rather than by a count, because a
107    /// trailer may legitimately outlive its call.
108    ///
109    /// This is a count, not a memory bound. What bounds the memory those
110    /// calls hold is `max_outstanding_payload` below, which is why this can
111    /// be generous: a peer may keep a great many *small* calls in flight
112    /// without thereby being allowed to keep that many large ones.
113    pub max_concurrent_calls: usize,
114    /// Total charged postcard bytes across all calls that have not yet
115    /// released, in aggregate.
116    ///
117    /// `max_payload_size` bounds one message and cannot bound the sum:
118    /// multiplied by `max_concurrent_calls` it is the whole reassembly
119    /// footprint a peer can demand, reachable by opening that many messages
120    /// and sending one fragment of each. This bounds the sum directly, and
121    /// spans the *whole call lifecycle* rather than just reassembly — a
122    /// payload's memory does not end at dispatch, it ends when the
123    /// application is done with the call. That makes it a byte-denominated
124    /// concurrency bound, which is the point.
125    ///
126    /// Unlike a trailer, a postcard payload cannot be paced incrementally: it
127    /// has to be reassembled whole before it can be deserialized. So the two
128    /// rules invert — trailers have no size cap because they are streamable,
129    /// and payloads keep one because they are not.
130    ///
131    /// The pool measures **wire** bytes. The deserialized form is
132    /// `O(serialized size)`, so wire bytes are an adequate proxy, but a
133    /// struct-heavy payload can land at 4–8× its postcard size once padding
134    /// and per-node overhead are counted. Size the limit knowing that.
135    ///
136    /// Trailer bytes are not counted here; see `trailer_session_window`.
137    /// Negotiation keeps this at least `max_payload_size`, since otherwise a
138    /// legal single message could never be sent.
139    pub max_outstanding_payload: usize,
140    /// How much retired trailer credit this end accumulates before returning
141    /// it to the peer.
142    ///
143    /// Purely a local coalescing knob — it is not negotiated and bounds
144    /// nothing. Larger values mean fewer `Kind::Credit` fragments and a
145    /// coarser feedback signal; a few fragments' worth is the useful range.
146    /// Credit is always flushed regardless once the trailer ends, the pool is
147    /// exhausted, or a consumer is left waiting for bytes, so no value can
148    /// stall a sender — including a sender that budgets the pool across its
149    /// own trailers, which this end never learns about.
150    pub trailer_credit_interval: usize,
151    /// Bytes of unretired trailer data all trailers on the session may have
152    /// outstanding, in aggregate.
153    ///
154    /// "Unretired" means the consuming application has not yet released the
155    /// credit for it — deliberately later than having read it, so this bounds
156    /// staged bytes plus whatever the application still holds. A sender parks
157    /// once the pool is empty and resumes on the next `Kind::Credit`.
158    ///
159    /// This is the only credit limit trailers have, and the whole bound on
160    /// receiver memory attributable to them, however many are open. Payload
161    /// quota is a separate pool (`max_outstanding_payload`); sharing one
162    /// between the two deadlocks a handler that must consume a trailer before
163    /// it can release its payload. There is deliberately no per-trailer
164    /// window: a sender that lets one trailer consume the pool
165    /// starves only its own other trailers, so how the pool is divided is a
166    /// local scheduling choice rather than a protocol rule. Only zero is a
167    /// deadlock, and negotiation floors it at 1.
168    pub trailer_session_window: usize,
169}
170
171impl Default for Limits {
172    fn default() -> Self {
173        Self {
174            max_fragment_size: 512 * 1024,
175            max_payload_size: 2 * 1024 * 1024,
176            max_handles_per_fragment: 8,
177            max_handles_per_message: 8,
178            trailer_recv_copy_threshold: 64 * 1024,
179            trailer_recv_demand_copy_threshold: 256 * 1024,
180            trailer_send_copy_threshold: 64 * 1024,
181            max_concurrent_calls: 1024,
182            max_outstanding_payload: 16 * 1024 * 1024,
183            trailer_credit_interval: 256 * 1024,
184            trailer_session_window: 16 * 1024 * 1024,
185        }
186    }
187}
188
189/// Maximum size of a `fragment::Kind::Negotiate` fragment, tolerated by both ends of a
190/// connection regardless of their configured `Limits`. Negotiation must use a
191/// fixed, transport-independent bound rather than `Limits::max_fragment_size`
192/// because neither side knows what the peer will actually enforce until
193/// negotiation completes. Not configurable — a future refactor must not tie
194/// this to `Limits`.
195pub(crate) const NEGOTIATE_FRAGMENT_SIZE: usize = 1024;
196
197/// Maximum total size of a reassembled `fragment::Kind::Negotiate` message payload,
198/// across all of its fragments. Bounds how much a peer can make the
199/// receiving end buffer before negotiation (and with it, the negotiated
200/// `Limits`) is in force. A real handshake payload — version blobs plus an
201/// application-protocol name and version list — is at most a few hundred
202/// bytes; this leaves generous headroom without allowing unbounded growth.
203/// Not configurable, for the same reason as `NEGOTIATE_FRAGMENT_SIZE`.
204pub(crate) const NEGOTIATE_MAX_PAYLOAD_SIZE: usize = 64 * 1024;
205
206/// A family of messages exchanged by one RPC session.
207///
208/// Implement this marker trait once for each application protocol version
209/// represented by distinct Rust request and response types. Both peers must
210/// bind the negotiated connection to compatible implementations.
211pub trait Protocol: Send + Sync + 'static {
212    /// Messages sent by [`client::Client`] calls and received by
213    /// [`server::Server`] handlers.
214    type Request: Serialize + DeserializeOwned + Send + 'static;
215    /// Messages returned by [`server::Server`] handlers and yielded by
216    /// completed [`client::Call`]s.
217    type Response: Serialize + DeserializeOwned + Send + 'static;
218}
219
220/// An error from session establishment, transport, or an individual call.
221#[derive(Debug, thiserror::Error)]
222pub enum Error {
223    /// The underlying transport failed.
224    #[error("I/O error: {0}")]
225    Io(#[from] io::Error),
226    /// Serializing an outgoing request or response failed.
227    #[error("serialization error: {0}")]
228    Serialize(String),
229    /// Deserializing an incoming request or response failed.
230    #[error("deserialization error: {0}")]
231    Deserialize(String),
232    /// The peer sent data that violates the RPC protocol.
233    #[error("protocol error: {0}")]
234    Protocol(String),
235    /// A pre-shared key was rejected, missing, or unexpected.
236    ///
237    /// Covers both a locally supplied key that cannot be used (see
238    /// [`AuthKey::new`](crate::auth::AuthKey::new)) and a peer that failed the check during negotiation.
239    /// The message never includes key material.
240    #[error("authentication error: {0}")]
241    Auth(String),
242    /// The local or peer session closed before the operation completed.
243    #[error("connection closed")]
244    ConnectionClosed,
245    /// The peer cancelled this call before it received a response.
246    #[error("request cancelled")]
247    Cancelled,
248    /// A requested transport capability is unavailable.
249    ///
250    /// Returned when an [`OsHandle`](crate::handle::OsHandle) is serialized
251    /// over a session whose transport cannot carry handle attachments (a
252    /// generic byte-stream transport rather than a Unix-domain socket or a
253    /// Windows named pipe).
254    #[error("transport does not support direct handles")]
255    UnsupportedCapability,
256}
257
258impl Error {
259    pub(crate) fn copy(&self) -> Self {
260        match self {
261            Self::Io(e) => Self::Io(io::Error::new(e.kind(), e.to_string())),
262            Self::Serialize(e) => Self::Serialize(e.clone()),
263            Self::Deserialize(e) => Self::Deserialize(e.clone()),
264            Self::Protocol(e) => Self::Protocol(e.clone()),
265            Self::Auth(e) => Self::Auth(e.clone()),
266            Self::ConnectionClosed => Self::ConnectionClosed,
267            Self::Cancelled => Self::Cancelled,
268            Self::UnsupportedCapability => Self::UnsupportedCapability,
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests;