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}