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;