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