docs(research): add iroh suite deep-dive references for iroh, irpc, iroh-blobs, iroh-gossip, iroh-live, and iroh-docs
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# irpc: Overview and Architecture
|
||||
|
||||
## What is irpc?
|
||||
|
||||
`irpc` is a **streaming RPC system** built for [iroh](https://docs.rs/iroh) and [noq](https://docs.rs/noq) (QUIC-based transports). It provides a framework for defining RPC protocols in Rust that work identically whether the communication is **in-process** (via tokio channels) or **cross-process/cross-network** (via QUIC streams).
|
||||
|
||||
**Key design goals:**
|
||||
|
||||
1. **Zero-overhead local use** — When used in-process, irpc should be as lightweight as raw tokio channels, replacing the common pattern of a giant `enum` over an `mpsc` channel with typed backchannels.
|
||||
2. **Transparent local/remote abstraction** — The same protocol definition and client API works for both in-process and remote communication.
|
||||
3. **Streaming-first** — Full support for unary RPC, server streaming, client streaming, and bidirectional streaming interaction patterns.
|
||||
4. **QUIC-native** — Does not abstract over stream types; directly uses noq/iroh QUIC streams, enabling per-request stream tuning (priorities, etc.).
|
||||
|
||||
**Non-goals:**
|
||||
|
||||
- Cross-language interop (Rust-to-Rust only)
|
||||
- Versioning (users must handle this themselves)
|
||||
- Making remote calls look like local async function calls
|
||||
- Runtime agnosticism (tokio only)
|
||||
|
||||
## Crate Structure
|
||||
|
||||
```
|
||||
irpc/
|
||||
├── src/lib.rs # Core library: traits, channels, Client, RPC module
|
||||
├── src/util.rs # Varint utilities, noq endpoint setup helpers
|
||||
├── src/tests.rs # Channel filter/map tests
|
||||
├── irpc-derive/ # Procedural macro crate (rpc_requests)
|
||||
├── irpc-iroh/ # Iroh transport integration
|
||||
├── examples/ # Working examples (storage, compute, derive, local)
|
||||
└── tests/ # Integration tests (channels, derive)
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
| Feature | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `rpc` | ✅ | Enables remote RPC (noq transport, postcard serialization) |
|
||||
| `derive` | ✅ | Enables the `#[rpc_requests]` macro |
|
||||
| `spans` | ✅ | Preserves tracing spans across message passing |
|
||||
| `stream` | ✅ | Enables `into_stream()` on mpsc receivers |
|
||||
| `noq_endpoint_setup` | ✅ | Utilities to create noq endpoints (testing, localhost) |
|
||||
| `varint-util` | ❌ | Varint read/write utilities without full RPC |
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Application │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ Client │─────│ Protocol │─────│ Actor/ │ │
|
||||
│ │<S> │ │ Enum (S) │ │ Handler │ │
|
||||
│ └────┬─────┘ └───────────┘ └─────┬─────┘ │
|
||||
│ │ │ │
|
||||
│ ┌────▼─────────────────────────────────────▼─────┐ │
|
||||
│ │ WithChannels<I, S> │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌─────┐ │ │
|
||||
│ │ │ inner │ │ tx │ │ rx │ │span │ │ │
|
||||
│ │ │ (I) │ │(Sender)│ │(Recv) │ │ │ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └─────┘ │ │
|
||||
│ └────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ Local Path │ │ Remote Path (rpc feat) │ │
|
||||
│ │ tokio::mpsc │ │ noq QUIC streams │ │
|
||||
│ │ tokio::oneshot │ │ postcard serialization │ │
|
||||
│ └────────────────────┘ └─────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Core Flow
|
||||
|
||||
1. **Define a protocol** — An enum where each variant represents an RPC method, annotated with `#[rpc(tx=..., rx=...)]`.
|
||||
2. **The `rpc_requests` macro** generates:
|
||||
- `Channels<S>` impl for each request type
|
||||
- A message enum wrapping each request in `WithChannels<I, S>`
|
||||
- `Service` and `RemoteService` trait implementations
|
||||
- `From` conversions between request types, protocol enum, and message enum
|
||||
3. **Client sends messages** — `Client<S>` either sends over a local `mpsc` channel or serializes and sends over a QUIC stream.
|
||||
4. **Actor/handler processes messages** — Matches on the message enum, extracts `WithChannels { inner, tx, rx, .. }`, and uses `tx`/`rx` to communicate back.
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
irpc (core)
|
||||
├── serde (always)
|
||||
├── tokio (sync, macros)
|
||||
├── tokio-util
|
||||
├── n0-error
|
||||
├── n0-future
|
||||
├── postcard (rpc feature)
|
||||
├── noq (rpc feature)
|
||||
├── smallvec (rpc feature)
|
||||
├── tracing (spans feature)
|
||||
└── irpc-derive (derive feature)
|
||||
|
||||
irpc-iroh
|
||||
├── irpc
|
||||
├── iroh
|
||||
├── iroh-base
|
||||
├── postcard
|
||||
└── n0-error, n0-future, tokio, tracing, serde
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Dual-licensed: Apache-2.0 OR MIT
|
||||
239
docs/research/references/iroh/irpc/02-types-and-traits.md
Normal file
239
docs/research/references/iroh/irpc/02-types-and-traits.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# irpc: Key Types and Traits
|
||||
|
||||
## Core Traits
|
||||
|
||||
### `RpcMessage`
|
||||
|
||||
```rust
|
||||
pub trait RpcMessage: Debug + Serialize + DeserializeOwned + Send + Sync + Unpin + 'static {}
|
||||
```
|
||||
|
||||
A blanket trait implemented for all types that satisfy the bounds. Every message sent through irpc (both local and remote) must implement this. The `Serialize + DeserializeOwned` requirement exists even without the `rpc` feature because the same protocol definition should work in both modes.
|
||||
|
||||
### `Service`
|
||||
|
||||
```rust
|
||||
pub trait Service: Serialize + DeserializeOwned + Send + Sync + Debug + 'static {
|
||||
type Message: Send + Unpin + 'static;
|
||||
}
|
||||
```
|
||||
|
||||
Implemented on the **protocol enum** (e.g., `StorageProtocol`). The `Message` associated type is the **message enum** — an enum with identical variant names but whose single field is `WithChannels<InnerType, Self>`.
|
||||
|
||||
The `Service` trait acts as a **scope** for channel type definitions, allowing the same inner request type to be used with multiple services.
|
||||
|
||||
### `Channels<S>`
|
||||
|
||||
```rust
|
||||
pub trait Channels<S: Service>: Send + 'static {
|
||||
type Tx: Sender;
|
||||
type Rx: Receiver;
|
||||
}
|
||||
```
|
||||
|
||||
Implemented on each **request type** (e.g., `Get`, `Set`). Specifies what kind of channels accompany that request when sent through service `S`. The `Tx` type is the response channel (server → client); the `Rx` type is the update channel (client → server).
|
||||
|
||||
### `Sender` and `Receiver`
|
||||
|
||||
```rust
|
||||
pub trait Sender: Debug + Sealed {}
|
||||
pub trait Receiver: Debug + Sealed {}
|
||||
```
|
||||
|
||||
Sealed marker traits. Only the types in `irpc::channel` implement these: `oneshot::Sender`, `oneshot::Receiver`, `mpsc::Sender`, `mpsc::Receiver`, `NoSender`, `NoReceiver`.
|
||||
|
||||
### `RemoteService` (rpc feature)
|
||||
|
||||
```rust
|
||||
pub trait RemoteService: Service + Sized {
|
||||
fn with_remote_channels(self, rx: noq::RecvStream, tx: noq::SendStream) -> Self::Message;
|
||||
|
||||
fn remote_handler(local_sender: LocalSender<Self>) -> Handler<Self> {
|
||||
// Default: convert deserialized protocol enum + streams → Message, send to local sender
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Implemented on the protocol enum. Maps a deserialized protocol variant + a pair of QUIC streams into a `WithChannels` message, which is then forwarded to the local actor.
|
||||
|
||||
### `RemoteConnection` (rpc feature)
|
||||
|
||||
```rust
|
||||
pub trait RemoteConnection: Send + Sync + Debug + 'static {
|
||||
fn clone_boxed(&self) -> Box<dyn RemoteConnection>;
|
||||
fn open_bi(&self) -> BoxFuture<Result<(noq::SendStream, noq::RecvStream), RequestError>>;
|
||||
fn zero_rtt_accepted(&self) -> BoxFuture<bool>;
|
||||
}
|
||||
```
|
||||
|
||||
Abstraction over how to open a bidirectional QUIC stream. Implemented for:
|
||||
- `noq::Connection` — direct noq connection
|
||||
- `NoqLazyRemoteConnection` — lazy connection that caches the underlying QUIC connection
|
||||
- `IrohRemoteConnection` — iroh connection (in `irpc-iroh`)
|
||||
- `IrohLazyRemoteConnection` — lazy iroh connection (in `irpc-iroh`)
|
||||
- `IrohZrttRemoteConnection` — 0-RTT iroh connection (in `irpc-iroh`)
|
||||
|
||||
## Key Structs
|
||||
|
||||
### `WithChannels<I, S>`
|
||||
|
||||
```rust
|
||||
pub struct WithChannels<I: Channels<S>, S: Service> {
|
||||
pub inner: I,
|
||||
pub tx: <I as Channels<S>>::Tx,
|
||||
pub rx: <I as Channels<S>>::Rx,
|
||||
#[cfg(feature = "spans")]
|
||||
pub span: tracing::Span,
|
||||
}
|
||||
```
|
||||
|
||||
The central message wrapper. Wraps a request type `I` with its typed channels for service `S`. Implements `Deref` to `I` for convenient field access.
|
||||
|
||||
**Construction** via tuple conversions:
|
||||
- `(inner, tx, rx)` → full channels
|
||||
- `(inner, tx)` → when `Rx = NoReceiver` (most common for RPC/server-streaming)
|
||||
- `(inner,)` → when `Tx = NoSender, Rx = NoReceiver` (notify)
|
||||
|
||||
### `Client<S>`
|
||||
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub struct Client<S: Service>(ClientInner<S::Message>, PhantomData<S>);
|
||||
```
|
||||
|
||||
The primary client type. Generic over a service `S`. Can be either local or remote.
|
||||
|
||||
**Construction:**
|
||||
- `Client::local(mpsc_sender)` — from a tokio mpsc sender
|
||||
- `Client::noq(endpoint, addr)` — from a noq endpoint + address (rpc feature)
|
||||
- `Client::boxed(remote_connection)` — from any `RemoteConnection` impl
|
||||
|
||||
**Key methods** (all handle both local and remote transparently):
|
||||
|
||||
| Method | Pattern | Tx Type | Rx Type |
|
||||
|---|---|---|---|
|
||||
| `rpc()` | Unary RPC | `oneshot::Sender<Res>` | `NoReceiver` |
|
||||
| `server_streaming()` | Server streaming | `mpsc::Sender<Res>` | `NoReceiver` |
|
||||
| `client_streaming()` | Client streaming | `oneshot::Sender<Res>` | `mpsc::Receiver<Update>` |
|
||||
| `bidi_streaming()` | Bidirectional | `mpsc::Sender<Res>` | `mpsc::Receiver<Update>` |
|
||||
| `notify()` | Fire-and-forget | `NoSender` | `NoReceiver` |
|
||||
| `rpc_0rtt()` | 0-RTT unary | `oneshot::Sender<Res>` | `NoReceiver` |
|
||||
| `server_streaming_0rtt()` | 0-RTT server streaming | `mpsc::Sender<Res>` | `NoReceiver` |
|
||||
| `notify_0rtt()` | 0-RTT fire-and-forget | `NoSender` | `NoReceiver` |
|
||||
|
||||
Each method creates the appropriate channel pair, wraps the message into `WithChannels`, and sends it.
|
||||
|
||||
### `LocalSender<S>`
|
||||
|
||||
```rust
|
||||
#[repr(transparent)]
|
||||
pub struct LocalSender<S: Service>(crate::channel::mpsc::Sender<S::Message>);
|
||||
```
|
||||
|
||||
A thin wrapper around `mpsc::Sender<S::Message>` for sending messages to a local actor. Provides:
|
||||
|
||||
```rust
|
||||
impl<S: Service> LocalSender<S> {
|
||||
pub fn send<T>(&self, value: impl Into<WithChannels<T, S>>) -> impl Future<Output = Result<(), SendError>>
|
||||
where
|
||||
T: Channels<S>,
|
||||
S::Message: From<WithChannels<T, S>>;
|
||||
|
||||
pub fn send_raw(&self, value: S::Message) -> impl Future<Output = Result<(), SendError>>;
|
||||
}
|
||||
```
|
||||
|
||||
### `Request<L, R>`
|
||||
|
||||
```rust
|
||||
pub enum Request<L, R> {
|
||||
Local(L),
|
||||
Remote(R),
|
||||
}
|
||||
```
|
||||
|
||||
A generic enum distinguishing local vs remote requests. `Client::request()` returns `Request<LocalSender<S>, RemoteSender<S>>`.
|
||||
|
||||
### `RemoteSender<S>` (rpc feature)
|
||||
|
||||
```rust
|
||||
pub struct RemoteSender<S>(noq::SendStream, noq::RecvStream, PhantomData<S>);
|
||||
```
|
||||
|
||||
Holds a QUIC stream pair after opening a bidirectional stream. The `write()` method serializes the protocol message with postcard + varint length prefix and sends it over the send stream.
|
||||
|
||||
### `Handler<R>` (rpc feature)
|
||||
|
||||
```rust
|
||||
pub type Handler<R> = Arc<
|
||||
dyn Fn(R, noq::RecvStream, noq::SendStream) -> BoxFuture<Result<(), SendError>>
|
||||
+ Send + Sync + 'static,
|
||||
>;
|
||||
```
|
||||
|
||||
A shared handler function that processes incoming remote requests. Typically created via `Protocol::remote_handler(local_sender)`.
|
||||
|
||||
## Error Types
|
||||
|
||||
### `RequestError`
|
||||
|
||||
```rust
|
||||
pub enum RequestError {
|
||||
Connect { source: noq::ConnectError }, // Connection establishment failed
|
||||
Connection { source: noq::ConnectionError }, // Stream open failed
|
||||
Other { source: AnyError }, // Generic error for non-noq transports
|
||||
}
|
||||
```
|
||||
|
||||
### `SendError` (in `channel` module)
|
||||
|
||||
```rust
|
||||
pub enum SendError {
|
||||
ReceiverClosed, // Local: receiver dropped
|
||||
MaxMessageSizeExceeded, // Remote: message > 16 MiB
|
||||
Io { source: io::Error }, // Remote: network/serialization error
|
||||
}
|
||||
```
|
||||
|
||||
### `RecvError` (oneshot and mpsc variants)
|
||||
|
||||
```rust
|
||||
// oneshot::RecvError
|
||||
pub enum RecvError {
|
||||
SenderClosed, // Local: sender dropped
|
||||
MaxMessageSizeExceeded, // Remote: message > 16 MiB
|
||||
Io { source: io::Error }, // Remote: network/deserialization error
|
||||
}
|
||||
|
||||
// mpsc::RecvError
|
||||
pub enum RecvError {
|
||||
MaxMessageSizeExceeded, // Remote: message > 16 MiB
|
||||
Io { source: io::Error }, // Remote: network/deserialization error
|
||||
}
|
||||
```
|
||||
|
||||
Note: `mpsc::RecvError` does **not** have `SenderClosed` — mpsc receivers return `Ok(None)` when the sender is dropped.
|
||||
|
||||
### `WriteError` (rpc feature)
|
||||
|
||||
```rust
|
||||
pub enum WriteError {
|
||||
Noq { source: noq::WriteError }, // QUIC stream write error
|
||||
MaxMessageSizeExceeded, // Message > 16 MiB
|
||||
Io { source: io::Error }, // Serialization error
|
||||
}
|
||||
```
|
||||
|
||||
### `Error` (top-level umbrella)
|
||||
|
||||
```rust
|
||||
pub enum Error {
|
||||
Request { source: RequestError },
|
||||
Send { source: SendError },
|
||||
MpscRecv { source: mpsc::RecvError },
|
||||
OneshotRecv { source: oneshot::RecvError },
|
||||
Write { source: rpc::WriteError }, // rpc feature only
|
||||
}
|
||||
```
|
||||
|
||||
All error types implement `From<Error>` for `io::Error`, allowing integration with `?` in `io::Result` contexts.
|
||||
168
docs/research/references/iroh/irpc/03-channel-system.md
Normal file
168
docs/research/references/iroh/irpc/03-channel-system.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# irpc: Channel System
|
||||
|
||||
The channel system is the heart of irpc. It provides channel types that abstract over local (tokio) and remote (QUIC stream) communication, with the same API surface regardless of transport.
|
||||
|
||||
## Channel Kinds
|
||||
|
||||
irpc provides three kinds of channels, each with local and remote variants:
|
||||
|
||||
### Oneshot Channels (`channel::oneshot`)
|
||||
|
||||
Single-value, single-use channels for RPC responses.
|
||||
|
||||
| Type | Local Backend | Remote Backend |
|
||||
|---|---|---|
|
||||
| `oneshot::Sender<T>` | `tokio::sync::oneshot::Sender` | `BoxedSender<T>` (FnOnce over QUIC write) |
|
||||
| `oneshot::Receiver<T>` | `FusedOneshotReceiver<T>` | `BoxedReceiver<T>` (boxed future over QUIC read) |
|
||||
|
||||
**Creation:** `oneshot::channel::<T>()` returns `(Sender<T>, Receiver<T>)`
|
||||
|
||||
**Sender behavior:**
|
||||
- Local: `send(value)` is synchronous-ish, fails only if receiver dropped
|
||||
- Remote: `send(value)` is async — serializes with postcard, length-prefixes with varint, writes to QUIC stream
|
||||
|
||||
**Receiver behavior:**
|
||||
- Implements `Future<Output = Result<T, RecvError>>`
|
||||
- Local: resolves to the value or `SenderClosed` error
|
||||
- Remote: reads varint length prefix, reads that many bytes, deserializes with postcard
|
||||
|
||||
**Filtering/Mapping** (on `Sender<T>` where `T: Send + Sync + 'static`):
|
||||
```rust
|
||||
sender.with_filter(|v| v > 0) // Drop messages failing predicate
|
||||
sender.with_map(|v: U| v.into()) // Transform before sending
|
||||
sender.with_filter_map(|v| ...) // Combined filter + map
|
||||
```
|
||||
|
||||
### MPSC Channels (`channel::mpsc`)
|
||||
|
||||
Multi-producer, single-consumer streaming channels for server-streaming, client-streaming, and bidirectional patterns.
|
||||
|
||||
| Type | Local Backend | Remote Backend |
|
||||
|---|---|---|
|
||||
| `mpsc::Sender<T>` | `tokio::sync::mpsc::Sender` | `Arc<DynSender<T>>` (NoqSender) |
|
||||
| `mpsc::Receiver<T>` | `tokio::sync::mpsc::Receiver` | `Box<dyn DynReceiver<T>>` (NoqReceiver) |
|
||||
|
||||
**Creation:** `mpsc::channel::<T>(buffer)` returns `(Sender<T>, Receiver<T>)`
|
||||
|
||||
**Sender behavior:**
|
||||
- `send(value).await` — sends, yielding if full (remote: serializes + writes to stream)
|
||||
- `try_send(value).await` — non-blocking attempt; returns `Ok(false)` if would block
|
||||
- `closed().await` — waits until all receivers are dropped
|
||||
- `is_rpc()` — returns `true` for remote senders
|
||||
|
||||
**Receiver behavior:**
|
||||
- `recv().await` → `Result<Option<T>, RecvError>` — `None` means sender closed/cleanly finished
|
||||
- `filter(pred)`, `map(fn)`, `filter_map(fn)` — chainable transformations
|
||||
- `into_stream()` (with `stream` feature) — converts to `Stream<Item = Result<T, RecvError>>`
|
||||
|
||||
**Cloning:** `mpsc::Sender<T>` implements `Clone`. Local senders clone the underlying tokio sender; remote senders clone the `Arc`.
|
||||
|
||||
### None Channels (`channel::none`)
|
||||
|
||||
Placeholder channels for when no communication is needed.
|
||||
|
||||
```rust
|
||||
pub struct NoSender; // Implements Sender, does nothing
|
||||
pub struct NoReceiver; // Implements Receiver, does nothing
|
||||
```
|
||||
|
||||
Used as defaults when `#[rpc(tx=...)]` or `#[rpc(rx=...)]` are omitted.
|
||||
|
||||
## Remote Channel Internals
|
||||
|
||||
### NoqSender<T>
|
||||
|
||||
```rust
|
||||
struct NoqSender<T>(tokio::sync::Mutex<NoqSenderState<T>>);
|
||||
|
||||
enum NoqSenderState<T> {
|
||||
Open(NoqSenderInner<T>),
|
||||
Closed,
|
||||
}
|
||||
|
||||
struct NoqSenderInner<T> {
|
||||
send: noq::SendStream,
|
||||
buffer: SmallVec<[u8; 128]>, // Stack-allocated buffer for small messages
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
```
|
||||
|
||||
Key behaviors:
|
||||
- **Mutex-protected state**: The inner state is `Mutex`-protected because `DynSender::send()` takes `&self`. When a send fails, the state transitions to `Closed` and all subsequent sends return `BrokenPipe`.
|
||||
- **Buffer reuse**: Uses `SmallVec<[u8; 128]>` to avoid heap allocation for messages that serialize to ≤128 bytes.
|
||||
- **Serialization**: Each message is postcard-serialized with a varint length prefix. If serialization exceeds `MAX_MESSAGE_SIZE` (16 MiB), the stream is reset with error code `ERROR_CODE_MAX_MESSAGE_SIZE_EXCEEDED` (1).
|
||||
- **Serialization errors**: If postcard serialization fails, the stream is reset with `ERROR_CODE_INVALID_POSTCARD` (2).
|
||||
|
||||
### NoqReceiver<T>
|
||||
|
||||
```rust
|
||||
struct NoqReceiver<T> {
|
||||
recv: noq::RecvStream,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
```
|
||||
|
||||
Reads a varint length prefix, allocates a buffer of that size, reads the data, and deserializes with postcard. If the length exceeds `MAX_MESSAGE_SIZE`, stops the stream with the appropriate error code.
|
||||
|
||||
### Oneshot Remote Sender
|
||||
|
||||
For `oneshot::Sender<T>` over QUIC, the sender is a `BoxedSender<T>` — a `Box<dyn FnOnce(T) -> BoxFuture<Result<(), SendError>>>`. This captures the `noq::SendStream` and on invocation:
|
||||
1. Computes `postcard::experimental::serialized_size(&value)`
|
||||
2. Checks against `MAX_MESSAGE_SIZE`
|
||||
3. Writes length-prefixed postcard data to the stream
|
||||
|
||||
### Oneshot Remote Receiver
|
||||
|
||||
For `oneshot::Receiver<T>` over QUIC, the receiver is constructed from a `noq::RecvStream`:
|
||||
1. Reads a varint length prefix
|
||||
2. Reads that many bytes
|
||||
3. Deserializes with postcard
|
||||
4. Returns the value
|
||||
|
||||
## Channel Conversion Table
|
||||
|
||||
When a QUIC stream pair `(SendStream, RecvStream)` is received for a request:
|
||||
|
||||
| Channel Kind | `Tx` (SendStream →) | `Rx` (RecvStream →) |
|
||||
|---|---|---|
|
||||
| `oneshot::Sender<T>` | Serialize + write, then finish | Read length-prefixed data |
|
||||
| `mpsc::Sender<T>` | Repeatedly serialize + write | N/A |
|
||||
| `oneshot::Receiver<T>` | N/A | Read single length-prefixed value |
|
||||
| `mpsc::Receiver<T>` | N/A | Repeatedly read length-prefixed values |
|
||||
| `NoSender` | Drop the stream | N/A |
|
||||
| `NoReceiver` | N/A | Drop the stream |
|
||||
|
||||
The `From<noq::RecvStream>` and `From<noq::SendStream>` impls handle these conversions automatically based on the target type.
|
||||
|
||||
## DynSender and DynReceiver Traits
|
||||
|
||||
The `mpsc` module exposes traits for dynamic dispatch:
|
||||
|
||||
```rust
|
||||
pub trait DynSender<T>: Debug + Send + Sync + 'static {
|
||||
fn send(&self, value: T) -> Pin<Box<dyn Future<Output = Result<(), SendError>> + Send + '_>>;
|
||||
fn try_send(&self, value: T) -> Pin<Box<dyn Future<Output = Result<bool, SendError>> + Send + '_>>;
|
||||
fn closed(&self) -> Pin<Box<dyn Future<Output = ()> + Send + Sync + '_>>;
|
||||
fn is_rpc(&self) -> bool;
|
||||
}
|
||||
|
||||
pub trait DynReceiver<T>: Debug + Send + Sync + 'static {
|
||||
fn recv(&mut self) -> Pin<Box<dyn Future<Output = Result<Option<T>, RecvError>> + Send + Sync + '_>>;
|
||||
}
|
||||
```
|
||||
|
||||
These enable boxing of remote senders/receivers while keeping the local variants unboxed for zero overhead.
|
||||
|
||||
## FusedOneshotReceiver
|
||||
|
||||
A thin wrapper around `tokio::sync::oneshot::Receiver` that prevents panics when polling an already-completed receiver. It tracks completion state and returns `Poll::Pending` indefinitely after resolution, matching the `FusedFuture` pattern.
|
||||
|
||||
## Cancellation Safety
|
||||
|
||||
For remote `mpsc::Sender`:
|
||||
- If a `send()` future is dropped before completion, the underlying QUIC stream is closed.
|
||||
- All clones of the sender will receive `SendError::Io(BrokenPipe)` on subsequent send attempts.
|
||||
- This is documented behavior: **always poll send futures to completion if you want to reuse the sender**.
|
||||
|
||||
For remote `oneshot::Sender`:
|
||||
- Since it's `FnOnce`, dropping the future before sending simply means the value is never sent. The receiver will get `SenderClosed`.
|
||||
@@ -0,0 +1,272 @@
|
||||
# irpc: Protocol and Message Flow
|
||||
|
||||
## Wire Protocol
|
||||
|
||||
When the `rpc` feature is enabled, irpc uses the following wire format over QUIC streams:
|
||||
|
||||
### Message Framing
|
||||
|
||||
Every message on the wire is **length-prefixed using postcard varints** (LEB128 encoding):
|
||||
|
||||
```
|
||||
┌─────────────────┬──────────────────────┐
|
||||
│ varint length │ postcard-serialized │
|
||||
│ (1-10 bytes) │ message data │
|
||||
└─────────────────┴──────────────────────┘
|
||||
```
|
||||
|
||||
- **Length prefix**: LEB128 varint encoding of `u64` length. Each byte uses 7 bits for the value and the MSB as a continuation bit. Maximum 10 bytes for a full `u64`.
|
||||
- **Payload**: Postcard-encoded (compact, no-schema serde format) Rust message.
|
||||
|
||||
### Maximum Message Size
|
||||
|
||||
`MAX_MESSAGE_SIZE = 16 MiB (16 * 1024 * 1024)`
|
||||
|
||||
Messages exceeding this limit are rejected:
|
||||
- **Send side**: The sender checks `postcard::experimental::serialized_size()` before sending. If exceeded, the stream is reset with error code `1` (`ERROR_CODE_MAX_MESSAGE_SIZE_EXCEEDED`).
|
||||
- **Receive side**: After reading the varint length, if it exceeds `MAX_MESSAGE_SIZE`, the stream is stopped with error code `1`.
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | Constant | Meaning |
|
||||
|---|---|---|
|
||||
| `1` | `ERROR_CODE_MAX_MESSAGE_SIZE_EXCEEDED` | Message larger than 16 MiB |
|
||||
| `2` | `ERROR_CODE_INVALID_POSTCARD` | Postcard serialization failed |
|
||||
|
||||
These are used as QUIC stream reset/stop error codes.
|
||||
|
||||
### Connection Closure
|
||||
|
||||
Error code `0` on the QUIC connection means "clean close" — the remote side intentionally shut down. This is distinguished from actual errors.
|
||||
|
||||
## Message Flow: Local Path
|
||||
|
||||
```
|
||||
Client Actor
|
||||
│ │
|
||||
│ Client::rpc(Get { key: "x" }) │
|
||||
│ │
|
||||
│ 1. Create oneshot channel pair │
|
||||
│ (tx, rx) = oneshot::channel() │
|
||||
│ │
|
||||
│ 2. Wrap into WithChannels │
|
||||
│ WithChannels { │
|
||||
│ inner: Get { key: "x" }, │
|
||||
│ tx: oneshot::Sender<Res>, │
|
||||
│ rx: NoReceiver, │
|
||||
│ span: current_span, │
|
||||
│ } │
|
||||
│ │
|
||||
│ 3. Convert to Message enum │
|
||||
│ StorageMessage::Get(wc) │
|
||||
│ │
|
||||
│ 4. Send over mpsc channel ────────►│
|
||||
│ │
|
||||
│ 5. Await on oneshot receiver │
|
||||
│ rx.await ◄─────────────────────│
|
||||
│ tx.send(res)│
|
||||
│ │
|
||||
│ Result: res │
|
||||
```
|
||||
|
||||
For bidirectional streaming:
|
||||
```
|
||||
Client Actor
|
||||
│ │
|
||||
│ Client::bidi_streaming(Sum, 4, 4) │
|
||||
│ │
|
||||
│ 1. Create channel pairs │
|
||||
│ (update_tx, update_rx) │
|
||||
│ (res_tx, res_rx) │
|
||||
│ │
|
||||
│ 2. WithChannels { │
|
||||
│ inner: Sum, │
|
||||
│ tx: mpsc::Sender<i64>, │
|
||||
│ rx: mpsc::Receiver<i64>, │
|
||||
│ } │
|
||||
│ │
|
||||
│ 3. Send message ──────────────────►│
|
||||
│ │
|
||||
│ 4. Use update_tx.send(val) ───────►│
|
||||
│ Use res_rx.recv() ◄─────────│
|
||||
│ res_tx.send(val)
|
||||
│ │
|
||||
```
|
||||
|
||||
## Message Flow: Remote Path
|
||||
|
||||
```
|
||||
Client Server
|
||||
│ │
|
||||
│ Client::rpc(Get { key: "x" }) │
|
||||
│ │
|
||||
│ 1. open_bi() → (SendStream, RecvStream)
|
||||
│ │
|
||||
│ 2. Serialize StorageProtocol::Get(Get { key: "x" })
|
||||
│ with postcard + varint prefix │
|
||||
│ │
|
||||
│ 3. Write to SendStream ───────────►│
|
||||
│ │
|
||||
│ │ 4. Accept bi stream
|
||||
│ │ 5. Read varint + deserialize
|
||||
│ │ 6. RemoteService::with_remote_channels()
|
||||
│ │ → WithChannels { inner, tx, rx }
|
||||
│ │ 7. Forward to local actor
|
||||
│ │
|
||||
│ │ Actor processes, sends response
|
||||
│ │ on the SendStream (which is the
|
||||
│ │ oneshot::Sender<T> backed by QUIC)
|
||||
│ │
|
||||
│ 8. Read from RecvStream ◄──────────│
|
||||
│ 9. Deserialize response │
|
||||
│ │
|
||||
│ Result: res │
|
||||
```
|
||||
|
||||
For bidirectional streaming over remote:
|
||||
```
|
||||
Client Server
|
||||
│ │
|
||||
│ Client::bidi_streaming(Sum, 4, 4) │
|
||||
│ │
|
||||
│ open_bi() → (SendStream, RecvStream)
|
||||
│ │
|
||||
│ SendStream → mpsc::Sender<Update> │ RecvStream → mpsc::Receiver<Update>
|
||||
│ RecvStream → oneshot::Receiver<Res>│ SendStream → oneshot::Sender<Res>
|
||||
│ (or mpsc::Receiver<Res> for │
|
||||
│ server-streaming with mpsc tx) │
|
||||
│ │
|
||||
│ The initial message is sent on │
|
||||
│ SendStream with varint prefix. │
|
||||
│ │
|
||||
│ Subsequent updates are sent on │
|
||||
│ the same SendStream as varint- │
|
||||
│ prefixed postcard messages. │
|
||||
│ │
|
||||
│ The response stream is read from │
|
||||
│ the RecvStream as varint-prefixed │
|
||||
│ postcard messages. │
|
||||
```
|
||||
|
||||
## Stream Direction Convention
|
||||
|
||||
In irpc's QUIC stream model:
|
||||
- **Client opens** a bidirectional stream (`open_bi()`)
|
||||
- **SendStream** (client → server): carries the initial request message, plus any client-streaming updates
|
||||
- **RecvStream** (server → client): carries the response(s) from the server
|
||||
|
||||
The `RemoteService::with_remote_channels()` method decides how to map streams to channels:
|
||||
|
||||
```rust
|
||||
// For a simple RPC (tx=oneshot, rx=none):
|
||||
fn with_remote_channels(self, rx: RecvStream, tx: SendStream) -> Self::Message {
|
||||
// rx stream is unused (NoReceiver), tx carries response
|
||||
WithChannels::from((msg, tx.into(), rx.into()))
|
||||
// tx → oneshot::Sender<Res> (or mpsc::Sender<Res>)
|
||||
// rx → NoReceiver
|
||||
}
|
||||
```
|
||||
|
||||
Wait — looking at the actual implementation more carefully:
|
||||
|
||||
The `RemoteService::with_remote_channels` method takes `(self, rx: RecvStream, tx: SendStream)` where:
|
||||
- `rx` = the `RecvStream` from the bidirectional stream (client reads from this)
|
||||
- `tx` = the `SendStream` from the bidirectional stream (client writes to this)
|
||||
|
||||
But for the **server side**, the `RecvStream` is what the server reads from (client updates), and `SendStream` is what the server writes to (server responses).
|
||||
|
||||
In the `with_remote_channels` generated code:
|
||||
```rust
|
||||
// For rpc(tx=oneshot::Sender<Res>, rx=mpsc::Receiver<Update>):
|
||||
WithChannels::from((msg, tx.into(), rx.into()))
|
||||
// tx (SendStream) → oneshot::Sender<Res> — server writes response
|
||||
// rx (RecvStream) → mpsc::Receiver<Update> — server reads client updates
|
||||
```
|
||||
|
||||
So the naming in `with_remote_channels` is from the **server's perspective**:
|
||||
- `rx` parameter = RecvStream = what server receives (client → server updates)
|
||||
- `tx` parameter = SendStream = what server sends (server → client responses)
|
||||
|
||||
## Connection Management
|
||||
|
||||
### NoqLazyRemoteConnection
|
||||
|
||||
```rust
|
||||
struct NoqLazyRemoteConnection(Arc<NoqLazyRemoteConnectionInner>);
|
||||
|
||||
struct NoqLazyRemoteConnectionInner {
|
||||
endpoint: noq::Endpoint,
|
||||
addr: SocketAddr,
|
||||
connection: Mutex<Option<noq::Connection>>,
|
||||
}
|
||||
```
|
||||
|
||||
- Lazily establishes connection on first use
|
||||
- Caches the `noq::Connection` inside a `Mutex<Option<...>>`
|
||||
- On `open_bi()`: if cached connection exists, tries to reuse it; if it fails, clears cache and reconnects once
|
||||
- Thread-safe via `Arc` + `Mutex`
|
||||
|
||||
### IrohLazyRemoteConnection (irpc-iroh)
|
||||
|
||||
Same pattern but for iroh endpoints, with an additional `alpn` field for protocol identification.
|
||||
|
||||
### 0-RTT Support
|
||||
|
||||
irpc supports QUIC 0-RTT for reduced latency on reconnections:
|
||||
|
||||
- `Client::rpc_0rtt()` — sends request immediately with 0-RTT data; if the server rejects 0-RTT, re-sends
|
||||
- `Client::server_streaming_0rtt()` — same for server-streaming
|
||||
- `Client::notify_0rtt()` — same for fire-and-forget
|
||||
|
||||
The 0-RTT flow:
|
||||
1. Client serializes the message into a buffer (`prepare_write()`)
|
||||
2. Sends the buffer over a 0-RTT connection
|
||||
3. Awaits `zero_rtt_accepted()` to check if 0-RTT was accepted
|
||||
4. If not accepted, opens a new connection and re-sends the same buffer
|
||||
|
||||
`RemoteConnection::zero_rtt_accepted()` returns `true` for regular connections and for lazy connections. For `IrohZrttRemoteConnection`, it checks the actual 0-RTT status via `handshake_completed()`.
|
||||
|
||||
## Server-Side: Accepting Connections
|
||||
|
||||
### Using noq (direct QUIC)
|
||||
|
||||
```rust
|
||||
irpc::rpc::listen(endpoint, handler)
|
||||
```
|
||||
|
||||
This function:
|
||||
1. Loops on `endpoint.accept()` to accept incoming connections
|
||||
2. For each connection, spawns a task running `handle_connection()`
|
||||
3. `handle_connection()` loops on `read_request_raw()` to read requests from bidirectional streams
|
||||
4. Each request is deserialized and passed to the `Handler`
|
||||
|
||||
### Using iroh
|
||||
|
||||
```rust
|
||||
IrohProtocol::with_sender(local_sender)
|
||||
```
|
||||
|
||||
This creates a `ProtocolHandler` that can be registered with `iroh::protocol::Router`. When a connection arrives, it calls `handle_connection()` from irpc-iroh, which handles the protocol handshake and reads requests.
|
||||
|
||||
For 0-RTT support:
|
||||
```rust
|
||||
Iroh0RttProtocol::with_sender(local_sender)
|
||||
```
|
||||
|
||||
This implements `ProtocolHandler::on_accepting()` to handle 0-RTT connections.
|
||||
|
||||
### Handler Function
|
||||
|
||||
```rust
|
||||
type Handler<R> = Arc<
|
||||
dyn Fn(R, noq::RecvStream, noq::SendStream) -> BoxFuture<Result<(), SendError>>
|
||||
+ Send + Sync + 'static,
|
||||
>;
|
||||
```
|
||||
|
||||
The handler receives:
|
||||
1. The deserialized protocol message (`R`)
|
||||
2. The `RecvStream` (for client → server updates)
|
||||
3. The `SendStream` (for server → client responses)
|
||||
|
||||
Typically created via `Protocol::remote_handler(local_sender)`, which converts streams to typed channels and forwards the `WithChannels` message to a local actor.
|
||||
278
docs/research/references/iroh/irpc/05-rpc-requests-macro.md
Normal file
278
docs/research/references/iroh/irpc/05-rpc-requests-macro.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# irpc: The rpc_requests Macro
|
||||
|
||||
The `#[rpc_requests]` attribute macro is the primary way to define an irpc protocol. It generates the boilerplate for channel typing, message wrapping, and service trait implementations.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```rust
|
||||
use irpc::{channel::{mpsc, oneshot}, rpc_requests, Client, WithChannels};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[rpc_requests(message = ComputeMessage)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum ComputeProtocol {
|
||||
/// Unary RPC: one request, one response
|
||||
#[rpc(tx=oneshot::Sender<i64>)]
|
||||
#[wrap(Multiply)]
|
||||
Multiply(i64, i64),
|
||||
|
||||
/// Bidirectional streaming
|
||||
#[rpc(tx=mpsc::Sender<i64>, rx=mpsc::Receiver<i64>)]
|
||||
#[wrap(Sum)]
|
||||
Sum,
|
||||
}
|
||||
```
|
||||
|
||||
This single macro invocation generates:
|
||||
|
||||
1. **Wrapper structs** (from `#[wrap]`): `Multiply` and `Sum` struct types
|
||||
2. **`Channels<ComputeProtocol>` impls**: For each variant's inner type, specifying `Tx` and `Rx`
|
||||
3. **`Service` impl**: `impl Service for ComputeProtocol { type Message = ComputeMessage; }`
|
||||
4. **`RemoteService` impl** (rpc feature): Maps protocol variants + QUIC streams to messages
|
||||
5. **`ComputeMessage` enum**: Wraps each request in `WithChannels`
|
||||
6. **`From` conversions**: Between inner types, `ComputeProtocol`, and `ComputeMessage`
|
||||
|
||||
## Macro Arguments
|
||||
|
||||
### Top-level (on the enum)
|
||||
|
||||
| Argument | Required | Description |
|
||||
|---|---|---|
|
||||
| `message = Name` | Recommended | Name of the generated message enum. Also generates `Service` and `RemoteService` impls. |
|
||||
| `alias = "Suffix"` | Optional | Generates type aliases like `MultiplyMsg = WithChannels<Multiply, ComputeProtocol>` |
|
||||
| `rpc_feature = "feat"` | Optional | Feature-gates the `RemoteService` impl with `#[cfg(feature = "feat")]` |
|
||||
| `no_rpc` | Optional | Skips generating `RemoteService` impl entirely |
|
||||
| `no_spans` | Optional | Skips span-related code (for use without the `spans` feature) |
|
||||
|
||||
### Per-variant
|
||||
|
||||
#### `#[rpc(tx=Type, rx=Type)]`
|
||||
|
||||
Specifies channel types for each request:
|
||||
- `tx` — response channel type (server → client). Defaults to `NoSender`.
|
||||
- `rx` — update channel type (client → server). Defaults to `NoReceiver`.
|
||||
|
||||
Valid types:
|
||||
- `oneshot::Sender<T>` — single response
|
||||
- `mpsc::Sender<T>` — streaming response
|
||||
- `oneshot::Receiver<T>` — not valid as tx (use for rx pattern)
|
||||
- `mpsc::Receiver<T>` — streaming updates (client → server)
|
||||
- `NoSender` / `NoReceiver` — no channel in that direction
|
||||
|
||||
#### `#[wrap(TypeName, derive(Traits))]`
|
||||
|
||||
Generates a struct from the variant's fields:
|
||||
- `TypeName` — name of the generated struct
|
||||
- Optional visibility prefix (e.g., `pub(crate) TypeName`)
|
||||
- `derive(...)` — additional derive macros beyond the default `Serialize, Deserialize, Debug`
|
||||
|
||||
If `#[wrap]` is not used, each variant must have exactly one unnamed field (a named type).
|
||||
|
||||
## Generated Code Walkthrough
|
||||
|
||||
Given this input:
|
||||
```rust
|
||||
#[rpc_requests(message = StoreMessage)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum StoreProtocol {
|
||||
#[rpc(tx=oneshot::Sender<String>)]
|
||||
#[wrap(GetRequest, derive(Clone))]
|
||||
Get(String),
|
||||
|
||||
#[rpc(tx=oneshot::Sender<()>)]
|
||||
#[wrap(SetRequest)]
|
||||
Set { key: String, value: String },
|
||||
}
|
||||
```
|
||||
|
||||
The macro generates:
|
||||
|
||||
### 1. Wrapper Structs
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub GetRequest(pub String);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub SetRequest { pub key: String, pub value: String }
|
||||
```
|
||||
|
||||
The variants are rewritten to use these:
|
||||
```rust
|
||||
enum StoreProtocol {
|
||||
Get(GetRequest),
|
||||
Set(SetRequest),
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Channels Implementations
|
||||
|
||||
```rust
|
||||
impl Channels<StoreProtocol> for GetRequest {
|
||||
type Tx = oneshot::Sender<String>;
|
||||
type Rx = NoReceiver;
|
||||
}
|
||||
|
||||
impl Channels<StoreProtocol> for SetRequest {
|
||||
type Tx = oneshot::Sender<()>;
|
||||
type Rx = NoReceiver;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Message Enum
|
||||
|
||||
```rust
|
||||
#[doc = "Message enum for [`StoreProtocol`]"]
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug)]
|
||||
pub enum StoreMessage {
|
||||
Get(WithChannels<GetRequest, StoreProtocol>),
|
||||
Set(WithChannels<SetRequest, StoreProtocol>),
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Service Implementation
|
||||
|
||||
```rust
|
||||
impl Service for StoreProtocol {
|
||||
type Message = StoreMessage;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. RemoteService Implementation (rpc feature)
|
||||
|
||||
```rust
|
||||
impl RemoteService for StoreProtocol {
|
||||
fn with_remote_channels(
|
||||
self,
|
||||
rx: noq::RecvStream,
|
||||
tx: noq::SendStream,
|
||||
) -> Self::Message {
|
||||
match self {
|
||||
StoreProtocol::Get(msg) => {
|
||||
StoreMessage::from(WithChannels::from((msg, tx, rx)))
|
||||
}
|
||||
StoreProtocol::Set(msg) => {
|
||||
StoreMessage::from(WithChannels::from((msg, tx, rx)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. From Conversions
|
||||
|
||||
```rust
|
||||
// Inner type → Protocol enum
|
||||
impl From<GetRequest> for StoreProtocol { ... }
|
||||
impl From<SetRequest> for StoreProtocol { ... }
|
||||
|
||||
// WithChannels → Message enum
|
||||
impl From<WithChannels<GetRequest, StoreProtocol>> for StoreMessage { ... }
|
||||
impl From<WithChannels<SetRequest, StoreProtocol>> for StoreMessage { ... }
|
||||
```
|
||||
|
||||
### 7. parent_span Method (spans feature)
|
||||
|
||||
```rust
|
||||
impl StoreMessage {
|
||||
pub fn parent_span(&self) -> tracing::Span {
|
||||
let span = match self {
|
||||
StoreMessage::Get(inner) => inner.parent_span_opt(),
|
||||
StoreMessage::Set(inner) => inner.parent_span_opt(),
|
||||
};
|
||||
span.cloned().unwrap_or_else(|| tracing::Span::current())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interaction Pattern Mapping
|
||||
|
||||
The `#[rpc]` attribute maps directly to gRPC-like patterns:
|
||||
|
||||
| Pattern | `tx` type | `rx` type | Example |
|
||||
|---|---|---|---|
|
||||
| **Unary RPC** | `oneshot::Sender<R>` | `NoReceiver` | Get by key, return value |
|
||||
| **Server streaming** | `mpsc::Sender<R>` | `NoReceiver` | List all items |
|
||||
| **Client streaming** | `oneshot::Sender<R>` | `mpsc::Receiver<U>` | Upload items, get count |
|
||||
| **Bidirectional** | `mpsc::Sender<R>` | `mpsc::Receiver<U>` | Chat, live updates |
|
||||
| **Notify (fire & forget)** | `NoSender` | `NoReceiver` | Log event |
|
||||
|
||||
## Client Methods Generated by Patterns
|
||||
|
||||
The `Client<S>` methods correspond to channel types:
|
||||
|
||||
```rust
|
||||
// Unary RPC: tx=oneshot::Sender<Res>, rx=NoReceiver
|
||||
client.rpc(Get { key: "x" }).await // → Result<Res>
|
||||
|
||||
// Server streaming: tx=mpsc::Sender<Res>, rx=NoReceiver
|
||||
client.server_streaming(List, 16).await // → Result<mpsc::Receiver<Res>>
|
||||
|
||||
// Client streaming: tx=oneshot::Sender<Res>, rx=mpsc::Receiver<Update>
|
||||
client.client_streaming(SetMany, 4).await // → Result<(mpsc::Sender<Update>, oneshot::Receiver<Res>)>
|
||||
|
||||
// Bidirectional: tx=mpsc::Sender<Res>, rx=mpsc::Receiver<Update>
|
||||
client.bidi_streaming(Sum, 4, 4).await // → Result<(mpsc::Sender<Update>, mpsc::Receiver<Res>)>
|
||||
|
||||
// Notify: tx=NoSender, rx=NoReceiver
|
||||
client.notify(Log { msg: "hi" }).await // → Result<()>
|
||||
```
|
||||
|
||||
## Manual Protocol Definition (Without Macro)
|
||||
|
||||
You can define protocols manually instead of using the macro:
|
||||
|
||||
```rust
|
||||
use irpc::{channel::{mpsc, none::NoReceiver, oneshot}, Channels, Service, WithChannels};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// 1. Define request types
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Get { key: String }
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Set { key: String, value: String }
|
||||
|
||||
// 2. Implement Channels for each type
|
||||
impl Channels<StorageProtocol> for Get {
|
||||
type Tx = oneshot::Sender<Option<String>>;
|
||||
type Rx = NoReceiver;
|
||||
}
|
||||
|
||||
impl Channels<StorageProtocol> for Set {
|
||||
type Tx = oneshot::Sender<()>;
|
||||
type Rx = NoReceiver;
|
||||
}
|
||||
|
||||
// 3. Define protocol enum
|
||||
#[derive(derive_more::From, Serialize, Deserialize, Debug)]
|
||||
enum StorageProtocol {
|
||||
Get(Get),
|
||||
Set(Set),
|
||||
}
|
||||
|
||||
// 4. Define message enum
|
||||
#[derive(derive_more::From)]
|
||||
enum StorageMessage {
|
||||
Get(WithChannels<Get, StorageProtocol>),
|
||||
Set(WithChannels<Set, StorageProtocol>),
|
||||
}
|
||||
|
||||
// 5. Implement Service
|
||||
impl Service for StorageProtocol {
|
||||
type Message = StorageMessage;
|
||||
}
|
||||
|
||||
// 6. Implement RemoteService (rpc feature)
|
||||
impl RemoteService for StorageProtocol {
|
||||
fn with_remote_channels(self, rx: noq::RecvStream, tx: noq::SendStream) -> Self::Message {
|
||||
match self {
|
||||
StorageProtocol::Get(msg) => WithChannels::from((msg, tx, rx)).into(),
|
||||
StorageProtocol::Set(msg) => WithChannels::from((msg, tx, rx)).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This manual approach gives full control but requires more boilerplate. The macro generates all of this automatically.
|
||||
@@ -0,0 +1,274 @@
|
||||
# irpc: RPC Module and Remote Transport
|
||||
|
||||
The `rpc` module (enabled by the `rpc` feature) contains all cross-process RPC functionality: QUIC stream handling, connection management, serialization, and server-side request processing.
|
||||
|
||||
## Module Structure
|
||||
|
||||
```rust
|
||||
pub mod rpc {
|
||||
pub const MAX_MESSAGE_SIZE: u64 = 1024 * 1024 * 16;
|
||||
pub const ERROR_CODE_MAX_MESSAGE_SIZE_EXCEEDED: u32 = 1;
|
||||
pub const ERROR_CODE_INVALID_POSTCARD: u32 = 2;
|
||||
|
||||
pub enum WriteError { Noq, MaxMessageSizeExceeded, Io }
|
||||
pub trait RemoteConnection: Send + Sync + Debug + 'static { ... }
|
||||
pub struct RemoteSender<S>(SendStream, RecvStream, PhantomData<S>);
|
||||
pub type Handler<R> = Arc<dyn Fn(R, RecvStream, SendStream) -> BoxFuture<Result<(), SendError>> + Send + Sync>;
|
||||
pub trait RemoteService: Service + Sized { ... }
|
||||
pub async fn listen<R>(endpoint, handler);
|
||||
pub async fn handle_connection<R>(connection, handler) -> io::Result<()>;
|
||||
pub async fn read_request<S: RemoteService>(connection) -> io::Result<Option<S::Message>>;
|
||||
pub async fn read_request_raw<R>(connection) -> io::Result<Option<(R, RecvStream, SendStream)>>;
|
||||
}
|
||||
```
|
||||
|
||||
## RemoteConnection Implementations
|
||||
|
||||
### NoqLazyRemoteConnection
|
||||
|
||||
The default remote connection for noq (QUIC-by-socket-address):
|
||||
|
||||
```rust
|
||||
struct NoqLazyRemoteConnection(Arc<NoqLazyRemoteConnectionInner>);
|
||||
|
||||
struct NoqLazyRemoteConnectionInner {
|
||||
endpoint: noq::Endpoint,
|
||||
addr: SocketAddr,
|
||||
connection: Mutex<Option<noq::Connection>>,
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- `open_bi()`:
|
||||
1. Locks the `Mutex<Option<Connection>>`
|
||||
2. If a cached connection exists, tries `conn.open_bi()`
|
||||
3. If that fails, clears the cache and establishes a new connection
|
||||
4. If no cached connection, establishes a new one
|
||||
5. Returns `(SendStream, RecvStream)` pair
|
||||
- `zero_rtt_accepted()`: Always returns `true` (noq doesn't have 0-RTT concept in this context)
|
||||
- `clone_boxed()`: Clones the `Arc`, sharing the same connection cache
|
||||
|
||||
### Direct noq::Connection
|
||||
|
||||
```rust
|
||||
impl RemoteConnection for noq::Connection {
|
||||
fn open_bi(&self) -> BoxFuture<Result<(SendStream, RecvStream), RequestError>> {
|
||||
// Directly opens a bidirectional stream on the connection
|
||||
}
|
||||
fn zero_rtt_accepted(&self) -> BoxFuture<bool> { Box::pin(async { true }) }
|
||||
}
|
||||
```
|
||||
|
||||
## RemoteSender
|
||||
|
||||
```rust
|
||||
pub struct RemoteSender<S>(noq::SendStream, noq::RecvStream, PhantomData<S>);
|
||||
```
|
||||
|
||||
Created by `Client::request()` when the client is remote. Holds both sides of a QUIC bidirectional stream.
|
||||
|
||||
### Key Methods
|
||||
|
||||
```rust
|
||||
impl<S: Service> RemoteSender<S> {
|
||||
pub fn new(send: SendStream, recv: RecvStream) -> Self;
|
||||
|
||||
pub async fn write(self, msg: impl Into<S>) -> Result<(SendStream, RecvStream), WriteError> {
|
||||
let buf = prepare_write(msg)?;
|
||||
self.write_raw(&buf).await
|
||||
}
|
||||
|
||||
// Internal: writes pre-serialized buffer
|
||||
pub(crate) async fn write_raw(self, buf: &[u8]) -> Result<(SendStream, RecvStream), WriteError>;
|
||||
}
|
||||
```
|
||||
|
||||
The `write()` method:
|
||||
1. Converts `msg` into the protocol enum `S` via `Into`
|
||||
2. Checks serialized size against `MAX_MESSAGE_SIZE`
|
||||
3. Length-prefixes with varint + postcard serialization
|
||||
4. Writes to the `SendStream`
|
||||
5. Returns the stream pair (now usable for response channels)
|
||||
|
||||
The `write_raw()` method is used for 0-RTT where the message is pre-serialized to allow re-sending without re-serialization.
|
||||
|
||||
### prepare_write
|
||||
|
||||
```rust
|
||||
fn prepare_write<S: Service>(msg: impl Into<S>) -> Result<SmallVec<[u8; 128]>, WriteError> {
|
||||
let msg = msg.into();
|
||||
if postcard::experimental::serialized_size(&msg)? as u64 > MAX_MESSAGE_SIZE {
|
||||
return Err(WriteError::MaxMessageSizeExceeded);
|
||||
}
|
||||
let mut buf = SmallVec::<[u8; 128]>::new();
|
||||
buf.write_length_prefixed(&msg)?;
|
||||
Ok(buf)
|
||||
}
|
||||
```
|
||||
|
||||
Uses `SmallVec<[u8; 128]>` to avoid heap allocation for small messages.
|
||||
|
||||
## Stream-to-Channel Conversions
|
||||
|
||||
When a QUIC stream pair is received on the server side, it needs to be converted into typed channels. The `From` implementations handle this:
|
||||
|
||||
### SendStream → Channel Tx
|
||||
|
||||
```rust
|
||||
// NoSender: drop the stream
|
||||
impl From<SendStream> for NoSender { ... }
|
||||
|
||||
// Oneshot: serialize and send single value, then done
|
||||
impl<T: RpcMessage> From<SendStream> for oneshot::Sender<T> { ... }
|
||||
|
||||
// MPSC: repeatedly serialize and send values
|
||||
impl<T: RpcMessage> From<SendStream> for mpsc::Sender<T> { ... }
|
||||
```
|
||||
|
||||
### RecvStream → Channel Rx
|
||||
|
||||
```rust
|
||||
// NoReceiver: drop the stream
|
||||
impl From<RecvStream> for NoReceiver { ... }
|
||||
|
||||
// Oneshot: read single length-prefixed value
|
||||
impl<T: DeserializeOwned> From<RecvStream> for oneshot::Receiver<T> { ... }
|
||||
|
||||
// MPSC: repeatedly read length-prefixed values
|
||||
impl<T: RpcMessage> From<RecvStream> for mpsc::Receiver<T> { ... }
|
||||
```
|
||||
|
||||
## Server-Side Request Processing
|
||||
|
||||
### read_request_raw
|
||||
|
||||
```rust
|
||||
pub async fn read_request_raw<R: DeserializeOwned + 'static>(
|
||||
connection: &noq::Connection,
|
||||
) -> io::Result<Option<(R, RecvStream, SendStream)>>
|
||||
```
|
||||
|
||||
1. Calls `connection.accept_bi()` to accept an incoming bidirectional stream
|
||||
2. If `ApplicationClosed(0)`, returns `Ok(None)` (clean shutdown)
|
||||
3. Reads a varint length prefix from the `RecvStream`
|
||||
4. Checks against `MAX_MESSAGE_SIZE`
|
||||
5. Reads `length` bytes from the stream
|
||||
6. Deserializes with `postcard::from_bytes::<R>()`
|
||||
7. Returns `(deserialized_message, RecvStream, SendStream)`
|
||||
|
||||
### read_request (typed)
|
||||
|
||||
```rust
|
||||
pub async fn read_request<S: RemoteService>(
|
||||
connection: &noq::Connection,
|
||||
) -> io::Result<Option<S::Message>>
|
||||
```
|
||||
|
||||
Calls `read_request_raw()` and then applies `S::with_remote_channels()` to convert the raw protocol message + stream pair into a `WithChannels`-wrapped `Message`.
|
||||
|
||||
### handle_connection
|
||||
|
||||
```rust
|
||||
pub async fn handle_connection<R: DeserializeOwned + 'static>(
|
||||
connection: noq::Connection,
|
||||
handler: Handler<R>,
|
||||
) -> io::Result<()>
|
||||
```
|
||||
|
||||
Loops:
|
||||
1. Calls `read_request_raw()` to get the next request
|
||||
2. If `None`, returns `Ok(())` (connection closed)
|
||||
3. Invokes `handler(msg, rx, tx)` to process the request
|
||||
4. Continues until the connection closes or an error occurs
|
||||
|
||||
Each connection is handled in a separate task (spawned by `listen()`).
|
||||
|
||||
### listen
|
||||
|
||||
```rust
|
||||
pub async fn listen<R: DeserializeOwned + 'static>(
|
||||
endpoint: noq::Endpoint,
|
||||
handler: Handler<R>,
|
||||
)
|
||||
```
|
||||
|
||||
The top-level server loop:
|
||||
1. Accepts incoming connections from the `noq::Endpoint`
|
||||
2. Spawns a task for each connection
|
||||
3. Each task calls `handle_connection()`
|
||||
4. Uses a `JoinSet` to manage and clean up completed tasks
|
||||
|
||||
## The Handler and Local Forwarding
|
||||
|
||||
The typical handler is created by `Protocol::remote_handler(local_sender)`:
|
||||
|
||||
```rust
|
||||
fn remote_handler(local_sender: LocalSender<Self>) -> Handler<Self> {
|
||||
Arc::new(move |msg, rx, tx| {
|
||||
let msg = Self::with_remote_channels(msg, rx, tx);
|
||||
Box::pin(local_sender.send_raw(msg))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This converts the raw (deserialized protocol message, RecvStream, SendStream) tuple into a typed `WithChannels` message and forwards it to the local actor via the mpsc channel. The local actor can then use the typed channels without knowing whether they're local or remote.
|
||||
|
||||
## Full Request Lifecycle (Remote)
|
||||
|
||||
```
|
||||
CLIENT SERVER
|
||||
│ │
|
||||
│ 1. Client::request() │
|
||||
│ → open_bi() on connection │
|
||||
│ │
|
||||
│ 2. RemoteSender::write(protocol_msg) │
|
||||
│ → serialize + send on SendStream ────►│
|
||||
│ │ 3. accept_bi()
|
||||
│ │ 4. read_request_raw()
|
||||
│ │ → read varint + data
|
||||
│ │ → deserialize protocol_msg
|
||||
│ │
|
||||
│ │ 5. RemoteService::with_remote_channels()
|
||||
│ │ → creates WithChannels
|
||||
│ │ → SendStream → tx channel
|
||||
│ │ → RecvStream → rx channel
|
||||
│ │
|
||||
│ │ 6. handler(msg, rx, tx)
|
||||
│ │ → local_sender.send_raw(message)
|
||||
│ │ → message goes to actor
|
||||
│ │
|
||||
│ │ 7. Actor processes:
|
||||
│ │ match message {
|
||||
│ │ Msg::Get(wc) => {
|
||||
│ │ let res = db.get(wc.inner.key);
|
||||
│ │ wc.tx.send(res).await;
|
||||
│ │ // tx.send() writes to SendStream
|
||||
│ │ }
|
||||
│ │ }
|
||||
│ │
|
||||
│ 8. RecvStream reads response ◄───────────│
|
||||
│ 9. Deserialize response │
|
||||
│ 10. Return to caller │
|
||||
```
|
||||
|
||||
## 0-RTT Flow
|
||||
|
||||
```
|
||||
CLIENT SERVER
|
||||
│ │
|
||||
│ 1. Serialize message into buffer │
|
||||
│ (prepare_write) │
|
||||
│ │
|
||||
│ 2. Open 0-RTT connection │
|
||||
│ → write buffer immediately ─────────►│
|
||||
│ │
|
||||
│ 3. Check zero_rtt_accepted() │
|
||||
│ → If true: done, read response │
|
||||
│ → If false: │
|
||||
│ 4. Open new (full) connection │
|
||||
│ 5. Re-send same buffer ────────────►│
|
||||
│ │
|
||||
│ 6. Read response ◄──────────────────────│
|
||||
```
|
||||
|
||||
The key insight: the message buffer is pre-serialized so it can be re-sent without re-serialization if 0-RTT is rejected.
|
||||
271
docs/research/references/iroh/irpc/07-irpc-iroh.md
Normal file
271
docs/research/references/iroh/irpc/07-irpc-iroh.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# irpc: irpc-iroh — Iroh Transport Integration
|
||||
|
||||
The `irpc-iroh` crate provides transport integration for iroh, enabling irpc to work with iroh's QUIC connections that use endpoint IDs (rather than socket addresses) for routing.
|
||||
|
||||
## Crate Overview
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "irpc-iroh"
|
||||
version = "0.13.0"
|
||||
description = "Iroh transport for irpc"
|
||||
```
|
||||
|
||||
Dependencies: `iroh`, `irpc`, `tokio`, `tracing`, `serde`, `postcard`, `n0-error`, `n0-future`
|
||||
|
||||
## Key Types
|
||||
|
||||
### IrohRemoteConnection
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IrohRemoteConnection(Connection);
|
||||
```
|
||||
|
||||
Wraps an existing iroh `Connection`. Simplest way to use irpc with iroh — create a connection externally and wrap it.
|
||||
|
||||
```rust
|
||||
impl RemoteConnection for IrohRemoteConnection {
|
||||
fn clone_boxed(&self) -> Box<dyn RemoteConnection> { ... }
|
||||
fn open_bi(&self) -> BoxFuture<Result<(SendStream, RecvStream), RequestError>> {
|
||||
// Delegates to connection.open_bi()
|
||||
}
|
||||
fn zero_rtt_accepted(&self) -> BoxFuture<bool> {
|
||||
// Always true — fully authenticated connection
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** This stops working when the underlying connection is closed. For automatic reconnection, use `IrohLazyRemoteConnection`.
|
||||
|
||||
### IrohZrttRemoteConnection
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IrohZrttRemoteConnection(OutgoingZeroRttConnection);
|
||||
```
|
||||
|
||||
Wraps an iroh 0-RTT (Zero Round Trip Time) connection. This enables sending data before the full handshake completes for reduced latency on reconnections.
|
||||
|
||||
```rust
|
||||
impl RemoteConnection for IrohZrttRemoteConnection {
|
||||
fn open_bi(&self) -> BoxFuture<Result<(SendStream, RecvStream), RequestError>> {
|
||||
// Delegates to the 0-RTT connection's open_bi()
|
||||
}
|
||||
fn zero_rtt_accepted(&self) -> BoxFuture<bool> {
|
||||
// Actually checks handshake_completed() to determine
|
||||
// if 0-RTT data was accepted
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `zero_rtt_accepted()` method:
|
||||
- Returns `true` if `ZeroRttStatus::Accepted`
|
||||
- Returns `false` if `ZeroRttStatus::Rejected` or on error
|
||||
- This allows the `Client` to decide whether to re-send data
|
||||
|
||||
### IrohLazyRemoteConnection
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IrohLazyRemoteConnection(Arc<IrohRemoteConnectionInner>);
|
||||
|
||||
struct IrohRemoteConnectionInner {
|
||||
endpoint: iroh::Endpoint,
|
||||
addr: iroh::EndpointAddr,
|
||||
connection: tokio::sync::Mutex<Option<Connection>>,
|
||||
alpn: Vec<u8>,
|
||||
}
|
||||
```
|
||||
|
||||
The lazy connection caches the underlying iroh `Connection` and reconnects automatically:
|
||||
|
||||
1. On first `open_bi()`, establishes a connection via `endpoint.connect(addr, alpn)`
|
||||
2. Caches the connection in a `Mutex<Option<Connection>>`
|
||||
3. On subsequent `open_bi()`, tries to reuse the cached connection
|
||||
4. If the cached connection fails, clears the cache and reconnects once
|
||||
|
||||
The `alpn` field is required because iroh connections need an ALPN protocol identifier.
|
||||
|
||||
### `client()` Function
|
||||
|
||||
```rust
|
||||
pub fn client<S: irpc::Service>(
|
||||
endpoint: iroh::Endpoint,
|
||||
addr: impl Into<iroh::EndpointAddr>,
|
||||
alpn: impl AsRef<[u8]>,
|
||||
) -> irpc::Client<S>
|
||||
```
|
||||
|
||||
Convenience function to create a `Client<S>` using iroh. Creates an `IrohLazyRemoteConnection` and wraps it with `Client::boxed()`.
|
||||
|
||||
## Server-Side: IrohProtocol
|
||||
|
||||
### IrohProtocol
|
||||
|
||||
```rust
|
||||
pub struct IrohProtocol<R> {
|
||||
handler: Handler<R>,
|
||||
request_id: AtomicU64,
|
||||
}
|
||||
```
|
||||
|
||||
Implements `iroh::protocol::ProtocolHandler`, allowing it to be registered with iroh's `Router`:
|
||||
|
||||
```rust
|
||||
impl<R: DeserializeOwned + Send + 'static> ProtocolHandler for IrohProtocol<R> {
|
||||
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
|
||||
// Handle the connection using irpc's handle_connection
|
||||
let handler = self.handler.clone();
|
||||
let fut = handle_connection(&connection, handler).map_err(AcceptError::from_err);
|
||||
fut.instrument(span).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```rust
|
||||
let protocol = IrohProtocol::with_sender(local_sender);
|
||||
// or
|
||||
let protocol = IrohProtocol::new(handler);
|
||||
|
||||
let router = Router::builder(endpoint)
|
||||
.accept(ALPN, protocol)
|
||||
.spawn();
|
||||
```
|
||||
|
||||
### Iroh0RttProtocol
|
||||
|
||||
```rust
|
||||
pub struct Iroh0RttProtocol<R> { ... }
|
||||
```
|
||||
|
||||
Supports 0-RTT connections by implementing `ProtocolHandler::on_accepting()`:
|
||||
|
||||
```rust
|
||||
impl<R: DeserializeOwned + Send + 'static> ProtocolHandler for Iroh0RttProtocol<R> {
|
||||
async fn on_accepting(&self, accepting: Accepting) -> Result<Connection, AcceptError> {
|
||||
let zrtt_conn = accepting.into_0rtt();
|
||||
// Handle 0-RTT data immediately
|
||||
handle_connection(&zrtt_conn, handler).await?;
|
||||
// Wait for handshake completion
|
||||
let conn = zrtt_conn.handshake_completed().await?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
async fn accept(&self, _connection: Connection) -> Result<(), AcceptError> {
|
||||
// Noop — handled in on_accepting
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Warning:** 0-RTT data is replayable. Only use for idempotent operations. See <https://www.iroh.computer/blog/0rtt-api>.
|
||||
|
||||
### IncomingRemoteConnection Trait
|
||||
|
||||
```rust
|
||||
pub trait IncomingRemoteConnection {
|
||||
fn accept_bi(&self) -> impl Future<Output = Result<(SendStream, RecvStream), ConnectionError>> + Send;
|
||||
fn close(&self, error_code: VarInt, reason: &[u8]);
|
||||
fn remote_id(&self) -> Result<EndpointId, RemoteEndpointIdError>;
|
||||
}
|
||||
```
|
||||
|
||||
Abstraction over `Connection` and `IncomingZeroRttConnection`, enabling `handle_connection` and `read_request` to work with both regular and 0-RTT connections.
|
||||
|
||||
Implemented for:
|
||||
- `Connection` — regular iroh connection
|
||||
- `IncomingZeroRttConnection` — 0-RTT connection
|
||||
|
||||
## handle_connection (iroh variant)
|
||||
|
||||
```rust
|
||||
pub async fn handle_connection<R: DeserializeOwned + 'static>(
|
||||
connection: &impl IncomingRemoteConnection,
|
||||
handler: Handler<R>,
|
||||
) -> io::Result<()>
|
||||
```
|
||||
|
||||
Similar to the noq version but works with iroh's `IncomingRemoteConnection` trait. Records the remote endpoint ID in the tracing span.
|
||||
|
||||
## read_request and read_request_raw (iroh variants)
|
||||
|
||||
Same logic as the noq versions but using `IncomingRemoteConnection` instead of `noq::Connection`:
|
||||
|
||||
```rust
|
||||
pub async fn read_request<S: RemoteService>(
|
||||
connection: &impl IncomingRemoteConnection,
|
||||
) -> io::Result<Option<S::Message>>
|
||||
|
||||
pub async fn read_request_raw<R: DeserializeOwned + 'static>(
|
||||
connection: &impl IncomingRemoteConnection,
|
||||
) -> io::Result<Option<(R, RecvStream, SendStream)>>
|
||||
```
|
||||
|
||||
## listen (iroh variant)
|
||||
|
||||
```rust
|
||||
pub async fn listen<R: DeserializeOwned + 'static>(endpoint: iroh::Endpoint, handler: Handler<R>)
|
||||
```
|
||||
|
||||
Accepts connections from an iroh `Endpoint` and handles them with the provided handler. Uses `n0_future::task::JoinSet` for task management.
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Server
|
||||
|
||||
```rust
|
||||
use irpc::{rpc_requests, channel::oneshot, Client, WithChannels};
|
||||
use irpc_iroh::IrohProtocol;
|
||||
use iroh::{endpoint::presets, protocol::Router, Endpoint};
|
||||
|
||||
#[rpc_requests(message = FooMessage)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum FooProtocol {
|
||||
#[rpc(tx=oneshot::Sender<String>)]
|
||||
Get(String),
|
||||
}
|
||||
|
||||
async fn server() -> Result<()> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||
tokio::task::spawn(actor(rx));
|
||||
let client = Client::<FooProtocol>::local(tx);
|
||||
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
let protocol = IrohProtocol::with_sender(client.as_local().unwrap());
|
||||
let router = Router::builder(endpoint).accept(ALPN, protocol).spawn();
|
||||
// ... keep running
|
||||
}
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```rust
|
||||
async fn connect(endpoint_id: EndpointId) -> Result<Client<FooProtocol>> {
|
||||
let endpoint = Endpoint::bind(presets::N0).await?;
|
||||
let client = irpc_iroh::client(endpoint, endpoint_id, ALPN);
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
// Or with direct connection:
|
||||
async fn connect_direct(endpoint: Endpoint, addr: EndpointAddr) -> Result<Client<FooProtocol>> {
|
||||
let conn = endpoint.connect(addr, ALPN).await?;
|
||||
Ok(Client::boxed(IrohRemoteConnection::new(conn)))
|
||||
}
|
||||
```
|
||||
|
||||
### 0-RTT Client
|
||||
|
||||
```rust
|
||||
async fn connect_0rtt(endpoint: Endpoint, addr: EndpointAddr) -> Result<Client<EchoProtocol>> {
|
||||
let connecting = endpoint.connect_with_opts(addr, ALPN, Default::default()).await?;
|
||||
match connecting.into_0rtt() {
|
||||
Ok(conn) => Ok(Client::boxed(IrohZrttRemoteConnection::new(conn))),
|
||||
Err(connecting) => {
|
||||
let conn = connecting.await?;
|
||||
Ok(Client::boxed(IrohRemoteConnection::new(conn)))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,134 @@
|
||||
# irpc: Serialization and Utility Modules
|
||||
|
||||
## Varint Utilities
|
||||
|
||||
The `varint-util` module (available with `rpc` or `varint-util` feature) provides LEB128 varint encoding/decoding compatible with postcard's format.
|
||||
|
||||
### Async Reading
|
||||
|
||||
```rust
|
||||
pub async fn read_varint_u64<R: AsyncRead + Unpin>(reader: &mut R) -> io::Result<Option<u64>>
|
||||
```
|
||||
|
||||
Reads a LEB128-encoded `u64` from an async reader. Returns `Ok(None)` on `UnexpectedEof` at the first byte position (clean stream end).
|
||||
|
||||
**Format:** Each byte uses 7 bits for the value, MSB as continuation bit. Values stored little-endian (least significant group first).
|
||||
|
||||
### Sync Writing
|
||||
|
||||
```rust
|
||||
pub fn write_varint_u64_sync<W: io::Write>(writer: &mut W, value: u64) -> io::Result<usize>
|
||||
```
|
||||
|
||||
Writes a `u64` as LEB128 to a synchronous writer.
|
||||
|
||||
### Length-Prefixed Encoding
|
||||
|
||||
```rust
|
||||
// Sync:
|
||||
pub fn write_length_prefixed<T: Serialize>(write: impl io::Write, value: T) -> io::Result<()>
|
||||
pub trait WriteVarintExt: io::Write {
|
||||
fn write_varint_u64(&mut self, value: u64) -> io::Result<usize>;
|
||||
fn write_length_prefixed<T: Serialize>(&mut self, value: T) -> io::Result<()>;
|
||||
}
|
||||
|
||||
// Async:
|
||||
pub trait AsyncReadVarintExt: AsyncRead + Unpin {
|
||||
fn read_varint_u64(&mut self) -> impl Future<Output = io::Result<Option<u64>>>;
|
||||
fn read_length_prefixed<T: DeserializeOwned>(&mut self, max_size: usize) -> impl Future<Output = io::Result<T>>;
|
||||
}
|
||||
|
||||
pub trait AsyncWriteVarintExt: AsyncWrite + Unpin {
|
||||
fn write_varint_u64(&mut self, value: u64) -> impl Future<Output = io::Result<usize>>;
|
||||
fn write_length_prefixed<T: Serialize>(&mut self, value: V) -> impl Future<Output = io::Result<usize>>;
|
||||
}
|
||||
```
|
||||
|
||||
The length-prefix format is:
|
||||
```
|
||||
[varint-encoded-length][postcard-serialized-data]
|
||||
```
|
||||
|
||||
Used internally by irpc for framing all messages on QUIC streams. The `max_size` parameter in `read_length_prefixed` prevents memory exhaustion from malicious length values.
|
||||
|
||||
## noq Endpoint Setup
|
||||
|
||||
The `noq_endpoint_setup` feature provides helpers for creating noq endpoints with TLS configuration:
|
||||
|
||||
```rust
|
||||
pub fn configure_client(server_certs: &[&[u8]]) -> Result<ClientConfig>
|
||||
pub fn configure_server() -> Result<(ServerConfig, Vec<u8>)>
|
||||
pub fn configure_client_insecure() -> Result<ClientConfig>
|
||||
|
||||
// Non-WASM only:
|
||||
pub fn make_client_endpoint(bind_addr: SocketAddr, server_certs: &[&[u8]]) -> Result<Endpoint>
|
||||
pub fn make_insecure_client_endpoint(bind_addr: SocketAddr) -> Result<Endpoint>
|
||||
pub fn make_server_endpoint(bind_addr: SocketAddr) -> Result<(Endpoint, Vec<u8>)>
|
||||
```
|
||||
|
||||
- `configure_server()`: Creates a self-signed certificate with rcgen and configures the server with TLS 1.3. Returns the DER-encoded certificate for clients to trust.
|
||||
- `configure_client()`: Configures a client to trust specific DER certificates.
|
||||
- `configure_client_insecure()`: Skips certificate verification (for testing only).
|
||||
- Server endpoints set `max_concurrent_uni_streams(0)` to disable unidirectional streams (only bidirectional streams are used).
|
||||
- Keep-alive interval is set to 1 second on client configs.
|
||||
|
||||
## FusedOneshotReceiver
|
||||
|
||||
```rust
|
||||
pub(crate) struct FusedOneshotReceiver<T>(pub tokio::sync::oneshot::Receiver<T>);
|
||||
```
|
||||
|
||||
A wrapper that prevents panics when polling an already-completed oneshot receiver. After the inner receiver resolves, subsequent polls return `Poll::Pending` indefinitely instead of panicking.
|
||||
|
||||
This is important because irpc's `oneshot::Receiver` can be wrapped in `Receiver::Boxed` (a `BoxFuture`), and the inner future might be polled multiple times in certain select patterns.
|
||||
|
||||
## now_or_never
|
||||
|
||||
```rust
|
||||
pub(crate) fn now_or_never<F: Future>(future: F) -> Option<F::Output>
|
||||
```
|
||||
|
||||
Attempts to complete a future immediately without blocking. If the future would block, returns `None`. Used internally by `NoqSenderInner::try_send()` to attempt an immediate write to the QUIC stream without yielding.
|
||||
|
||||
Implementation uses a no-op waker to poll the future once.
|
||||
|
||||
## Spans Feature
|
||||
|
||||
When the `spans` feature is enabled (default), `WithChannels` includes a `span: tracing::Span` field:
|
||||
|
||||
```rust
|
||||
pub struct WithChannels<I: Channels<S>, S: Service> {
|
||||
pub inner: I,
|
||||
pub tx: <I as Channels<S>>::Tx,
|
||||
pub rx: <I as Channels<S>>::Rx,
|
||||
#[cfg(feature = "spans")]
|
||||
pub span: tracing::Span,
|
||||
}
|
||||
```
|
||||
|
||||
The span is captured from `tracing::Span::current()` at the time of `WithChannels` construction (via `From` implementations). This preserves tracing context across async message-passing boundaries.
|
||||
|
||||
The `rpc_requests` macro generates a `parent_span()` method on the message enum when `no_spans` is not set:
|
||||
|
||||
```rust
|
||||
impl ComputeMessage {
|
||||
pub fn parent_span(&self) -> tracing::Span {
|
||||
let span = match self {
|
||||
ComputeMessage::Multiply(inner) => inner.parent_span_opt(),
|
||||
ComputeMessage::Sum(inner) => inner.parent_span_opt(),
|
||||
};
|
||||
span.cloned().unwrap_or_else(|| tracing::Span::current())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows server-side handlers to enter the client's tracing span:
|
||||
|
||||
```rust
|
||||
async fn handle(msg: ComputeMessage) {
|
||||
let _entered = msg.parent_span().enter();
|
||||
// ... processing happens in the client's tracing context
|
||||
}
|
||||
```
|
||||
|
||||
When `no_spans` is set in the macro, no span-related code is generated, making it compatible with builds that don't have the `spans` feature enabled.
|
||||
@@ -0,0 +1,249 @@
|
||||
# irpc: Design Patterns and Usage Examples
|
||||
|
||||
## Pattern 1: Actor Model (Most Common)
|
||||
|
||||
The primary usage pattern is an actor that receives messages and processes them sequentially:
|
||||
|
||||
```rust
|
||||
struct StorageActor {
|
||||
recv: tokio::sync::mpsc::Receiver<StorageMessage>,
|
||||
state: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl StorageActor {
|
||||
pub fn spawn() -> StorageApi {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||
let actor = Self { recv: rx, state: BTreeMap::new() };
|
||||
tokio::task::spawn(actor.run());
|
||||
StorageApi { inner: Client::local(tx) }
|
||||
}
|
||||
|
||||
async fn run(mut self) {
|
||||
while let Some(msg) = self.recv.recv().await {
|
||||
self.handle(msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(&mut self, msg: StorageMessage) {
|
||||
match msg {
|
||||
StorageMessage::Get(wc) => {
|
||||
let WithChannels { inner, tx, .. } = wc;
|
||||
tx.send(self.state.get(&inner.key).cloned()).await.ok();
|
||||
}
|
||||
StorageMessage::Set(wc) => {
|
||||
let WithChannels { inner, tx, .. } = wc;
|
||||
self.state.insert(inner.key, inner.value);
|
||||
tx.send(()).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- The actor owns state and processes messages sequentially
|
||||
- `Client::local(tx)` wraps the sender side of the mpsc channel
|
||||
- `WithChannels` destructuring gives access to `inner` (the request data), `tx` (response channel), and `rx` (update channel)
|
||||
- The `..` pattern ignores `rx` when it's `NoReceiver` and `span` (with `spans` feature)
|
||||
|
||||
## Pattern 2: Concurrent Task Per Request
|
||||
|
||||
For long-running or independent requests, spawn a task per message:
|
||||
|
||||
```rust
|
||||
async fn run(mut self) {
|
||||
while let Ok(Some(msg)) = self.recv.recv().await {
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(cause) = Self::handle(msg).await {
|
||||
eprintln!("Error: {cause}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is useful for CPU-intensive or I/O-bound requests that shouldn't block other requests.
|
||||
|
||||
## Pattern 3: Local-Only Usage
|
||||
|
||||
irpc can be used without any RPC feature for pure in-process communication:
|
||||
|
||||
```rust
|
||||
// Cargo.toml: default-features = false, features = ["derive"]
|
||||
#[rpc_requests(message = StorageMessage, no_rpc, no_spans)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum StorageProtocol {
|
||||
#[rpc(tx=oneshot::Sender<Option<String>>)]
|
||||
Get(Get),
|
||||
#[rpc(tx=oneshot::Sender<()>)]
|
||||
Set(Set),
|
||||
}
|
||||
```
|
||||
|
||||
The `no_rpc` flag prevents `RemoteService` from being generated, and `no_spans` removes the tracing dependency. This leaves only the local channel mechanism, with minimal dependencies (serde, tokio, tokio-util).
|
||||
|
||||
## Pattern 4: API Type Wrapping Client
|
||||
|
||||
The recommended pattern is to wrap `Client<S>` in a higher-level API type:
|
||||
|
||||
```rust
|
||||
struct StorageApi {
|
||||
inner: Client<StorageProtocol>,
|
||||
}
|
||||
|
||||
impl StorageApi {
|
||||
// Local
|
||||
pub fn spawn() -> Self {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||
tokio::task::spawn(StorageActor::new(rx).run());
|
||||
Self { inner: Client::local(tx) }
|
||||
}
|
||||
|
||||
// Remote (noq)
|
||||
pub fn connect(endpoint: noq::Endpoint, addr: SocketAddr) -> Self {
|
||||
Self { inner: Client::noq(endpoint, addr) }
|
||||
}
|
||||
|
||||
// Remote (iroh)
|
||||
pub fn connect_iroh(endpoint: iroh::Endpoint, addr: EndpointAddr) -> Self {
|
||||
Self { inner: irpc_iroh::client(endpoint, addr, ALPN) }
|
||||
}
|
||||
|
||||
// Type-safe methods that work for both local and remote
|
||||
pub async fn get(&self, key: String) -> irpc::Result<Option<String>> {
|
||||
self.inner.rpc(Get { key }).await
|
||||
}
|
||||
|
||||
pub async fn set(&self, key: String, value: String) -> irpc::Result<()> {
|
||||
self.inner.rpc(Set { key, value }).await
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> irpc::Result<mpsc::Receiver<String>> {
|
||||
self.inner.server_streaming(List, 16).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This encapsulates the protocol details and provides a clean, type-safe API. The same `StorageApi` works identically whether connected locally or remotely.
|
||||
|
||||
## Pattern 5: Server Setup
|
||||
|
||||
### With noq
|
||||
|
||||
```rust
|
||||
fn serve(api: &StorageApi, endpoint: noq::Endpoint) -> Result<JoinHandle<()>> {
|
||||
let local = api.inner.as_local().context("cannot listen on remote service")?;
|
||||
let handler = StorageProtocol::remote_handler(local);
|
||||
Ok(tokio::task::spawn(irpc::rpc::listen(endpoint, handler)))
|
||||
}
|
||||
```
|
||||
|
||||
### With iroh
|
||||
|
||||
```rust
|
||||
fn serve(api: &StorageApi, endpoint: iroh::Endpoint) -> Result<Router> {
|
||||
let local = api.inner.as_local().context("cannot listen on remote service")?;
|
||||
let protocol = IrohProtocol::with_sender(local);
|
||||
Ok(Router::builder(endpoint).accept(ALPN, protocol).spawn())
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 6: Low-Level Request Handling
|
||||
|
||||
For more control than the `Client` methods provide, use `request()` directly:
|
||||
|
||||
```rust
|
||||
async fn custom_request(&self, msg: Get) -> anyhow::Result<oneshot::Receiver<Option<String>>> {
|
||||
match self.inner.request().await? {
|
||||
Request::Local(request) => {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
request.send((msg, tx)).await?;
|
||||
Ok(rx)
|
||||
}
|
||||
Request::Remote(request) => {
|
||||
let (_tx, rx) = request.write(msg).await?;
|
||||
Ok(rx.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows custom channel creation logic, e.g., different buffer sizes for local vs remote.
|
||||
|
||||
## Pattern 7: Channel Filtering and Mapping
|
||||
|
||||
irpc channels support filtering and mapping, which work for both local and remote channels:
|
||||
|
||||
```rust
|
||||
// Server-side: filter responses to only include values > 10
|
||||
let filtered_tx = wc.tx.with_filter(|v: &i64| *v > 10);
|
||||
|
||||
// Server-side: transform responses
|
||||
let mapped_tx = wc.tx.with_map(|v: i64| v * 2);
|
||||
|
||||
// Client-side: filter received updates
|
||||
let filtered_rx = rx.filter(|update: &Update| update.is_relevant());
|
||||
```
|
||||
|
||||
For remote channels, these create boxed wrappers. For local channels, they also create boxed wrappers. The overhead is negligible for remote (network latency dominates) but present for local.
|
||||
|
||||
## Pattern 8: Using the `wrap` Attribute
|
||||
|
||||
The `#[wrap]` attribute generates named structs from variant fields:
|
||||
|
||||
```rust
|
||||
#[rpc_requests(message = StoreMessage)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum StoreProtocol {
|
||||
#[rpc(tx=oneshot::Sender<Option<String>>)]
|
||||
#[wrap(GetRequest, derive(Clone))]
|
||||
Get(String), // Generates: pub struct GetRequest(pub String);
|
||||
|
||||
#[rpc(tx=oneshot::Sender<()>)]
|
||||
#[wrap(SetRequest)]
|
||||
Set { key: String, value: String }, // Generates: pub struct SetRequest { pub key: String, pub value: String }
|
||||
}
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Named request types can be imported and constructed by name
|
||||
- Additional derives (e.g., `Clone`) can be added
|
||||
- Custom visibility can be specified: `#[wrap(pub(crate) GetRequest)]`
|
||||
- The generated struct inherits the enum's visibility by default
|
||||
|
||||
## Pattern 9: 0-RTT Connections
|
||||
|
||||
For reduced latency on reconnections with iroh:
|
||||
|
||||
```rust
|
||||
// Client side
|
||||
let result = client.rpc_0rtt(Get { key: "x".into() }).await?;
|
||||
|
||||
// Server side (iroh)
|
||||
let protocol = Iroh0RttProtocol::with_sender(local_sender);
|
||||
let router = Router::builder(endpoint).accept(ALPN, protocol).spawn();
|
||||
```
|
||||
|
||||
**Important:** Only use 0-RTT for idempotent operations, as the data may be replayed by an attacker.
|
||||
|
||||
## Pattern 10: Shared State in Actor
|
||||
|
||||
For actors that need shared state accessible from multiple handlers:
|
||||
|
||||
```rust
|
||||
struct Actor {
|
||||
recv: tokio::sync::mpsc::Receiver<Message>,
|
||||
state: Arc<Mutex<SharedState>>,
|
||||
}
|
||||
```
|
||||
|
||||
Or use the actor pattern with internal mutation:
|
||||
|
||||
```rust
|
||||
struct Actor {
|
||||
recv: tokio::sync::mpsc::Receiver<Message>,
|
||||
db: HashMap<String, String>, // owned state
|
||||
}
|
||||
```
|
||||
|
||||
Since the actor processes messages sequentially, no internal synchronization is needed.
|
||||
230
docs/research/references/iroh/irpc/10-quick-reference.md
Normal file
230
docs/research/references/iroh/irpc/10-quick-reference.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# irpc: Quick Reference
|
||||
|
||||
## Crate Info
|
||||
|
||||
- **Name:** `irpc`
|
||||
- **Version:** 0.13.0
|
||||
- **License:** Apache-2.0 OR MIT
|
||||
- **Repository:** https://github.com/n0-computer/irpc
|
||||
- **MSRV:** 1.89
|
||||
|
||||
## Feature Flags
|
||||
|
||||
| Feature | Default | Dependencies Added |
|
||||
|---|---|---|
|
||||
| `rpc` | ✅ | noq, postcard, smallvec, tracing, tokio/io-util |
|
||||
| `derive` | ✅ | irpc-derive |
|
||||
| `spans` | ✅ | tracing |
|
||||
| `stream` | ✅ | futures-util |
|
||||
| `noq_endpoint_setup` | ✅ | rustls, rcgen, futures-buffered |
|
||||
| `varint-util` | ❌ | postcard, smallvec, tokio/io-util |
|
||||
|
||||
## Type Quick Reference
|
||||
|
||||
### Core Types
|
||||
|
||||
```
|
||||
Service trait — implemented on protocol enum, defines Message type
|
||||
Channels<S> trait — implemented on request types, defines Tx/Rx types
|
||||
RpcMessage trait — blanket impl for Debug+Serialize+DeserializeOwned+Send+Sync+Unpin+'static
|
||||
Sender trait — sealed marker for sender types
|
||||
Receiver trait — sealed marker for receiver types
|
||||
WithChannels<I,S> struct — wraps request I with tx/rx/span for service S
|
||||
Client<S> struct — client to service S (local or remote)
|
||||
LocalSender<S> struct — local sender wrapping mpsc::Sender<S::Message>
|
||||
Request<L,R> enum — Local(L) or Remote(R) request
|
||||
RemoteSender<S> struct — holds QUIC stream pair for sending initial message
|
||||
```
|
||||
|
||||
### Channel Types
|
||||
|
||||
```
|
||||
oneshot::Sender<T> — Tokio or Boxed; single value; async send
|
||||
oneshot::Receiver<T> — Tokio or Boxed; single value; Future impl
|
||||
mpsc::Sender<T> — Tokio or Arc<DynSender>; stream; async send/try_send
|
||||
mpsc::Receiver<T> — Tokio or Box<DynReceiver>; stream; async recv
|
||||
NoSender — No-op sender
|
||||
NoReceiver — No-op receiver
|
||||
```
|
||||
|
||||
### Remote Types (rpc feature)
|
||||
|
||||
```
|
||||
RemoteConnection trait — open_bi(), zero_rtt_accepted(), clone_boxed()
|
||||
NoqLazyRemoteConnection — lazy noq connection with cache
|
||||
Handler<R> type — Arc<dyn Fn(R, RecvStream, SendStream) -> ...>
|
||||
```
|
||||
|
||||
### irpc-iroh Types
|
||||
|
||||
```
|
||||
IrohRemoteConnection — wraps iroh::Connection
|
||||
IrohZrttRemoteConnection — wraps iroh::OutgoingZeroRttConnection
|
||||
IrohLazyRemoteConnection — lazy iroh connection with cache
|
||||
IrohProtocol<R> — ProtocolHandler for iroh Router
|
||||
Iroh0RttProtocol<R> — ProtocolHandler with 0-RTT support
|
||||
IncomingRemoteConnection trait — abstraction over Connection and ZeroRttConnection
|
||||
```
|
||||
|
||||
## Interaction Patterns Cheatsheet
|
||||
|
||||
```rust
|
||||
// ═══════════════════════════════════════════
|
||||
// Protocol Definition
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
#[rpc_requests(message = MyMessage)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum MyProtocol {
|
||||
// Unary RPC
|
||||
#[rpc(tx=oneshot::Sender<Response>)]
|
||||
#[wrap(GetReq)]
|
||||
Get(String),
|
||||
|
||||
// Server streaming
|
||||
#[rpc(tx=mpsc::Sender<Item>)]
|
||||
#[wrap(ListReq)]
|
||||
List(ListParams),
|
||||
|
||||
// Client streaming
|
||||
#[rpc(tx=oneshot::Sender<Count>, rx=mpsc::Receiver<Item>)]
|
||||
#[wrap(UploadReq)]
|
||||
Upload,
|
||||
|
||||
// Bidirectional streaming
|
||||
#[rpc(tx=mpsc::Sender<Result>, rx=mpsc::Receiver<Update>)]
|
||||
#[wrap(ProcessReq)]
|
||||
Process(ProcessConfig),
|
||||
|
||||
// Fire and forget
|
||||
#[rpc]
|
||||
#[wrap(LogReq)]
|
||||
Log(String),
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Client Usage
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
// Local
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||
tokio::task::spawn(actor(rx));
|
||||
let client: Client<MyProtocol> = Client::local(tx);
|
||||
|
||||
// Remote (noq)
|
||||
let client: Client<MyProtocol> = Client::noq(endpoint, addr);
|
||||
|
||||
// Remote (iroh)
|
||||
let client: Client<MyProtocol> = irpc_iroh::client(endpoint, addr, alpn);
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Making Requests
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
// Unary
|
||||
let result: Response = client.rpc(GetReq("key".into())).await?;
|
||||
|
||||
// Server streaming
|
||||
let mut rx: mpsc::Receiver<Item> = client.server_streaming(ListReq(params), 16).await?;
|
||||
while let Some(item) = rx.recv().await? { ... }
|
||||
|
||||
// Client streaming
|
||||
let (update_tx, response_rx): (mpsc::Sender<Item>, oneshot::Receiver<Count>) =
|
||||
client.client_streaming(Upload, 4).await?;
|
||||
update_tx.send(item).await?;
|
||||
let count = response_rx.await?;
|
||||
|
||||
// Bidirectional
|
||||
let (update_tx, mut result_rx): (mpsc::Sender<Update>, mpsc::Receiver<Result>) =
|
||||
client.bidi_streaming(ProcessReq(config), 4, 16).await?;
|
||||
update_tx.send(update).await?;
|
||||
while let Some(result) = result_rx.recv().await? { ... }
|
||||
|
||||
// Fire and forget
|
||||
client.notify(LogReq("message".into())).await?;
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Server Setup
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
// noq
|
||||
let handler = MyProtocol::remote_handler(local_sender);
|
||||
irpc::rpc::listen(endpoint, handler).await;
|
||||
|
||||
// iroh
|
||||
let protocol = IrohProtocol::with_sender(local_sender);
|
||||
Router::builder(endpoint).accept(ALPN, protocol).spawn();
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Actor Message Handling
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
async fn handle(&mut self, msg: MyMessage) {
|
||||
match msg {
|
||||
MyMessage::Get(wc) => {
|
||||
let WithChannels { inner, tx, .. } = wc;
|
||||
let result = self.db.get(&inner.0).cloned();
|
||||
tx.send(result).await.ok();
|
||||
}
|
||||
MyMessage::List(wc) => {
|
||||
let WithChannels { tx, .. } = wc;
|
||||
for item in &self.items {
|
||||
if tx.send(item.clone()).await.is_err() { break; }
|
||||
}
|
||||
}
|
||||
MyMessage::Upload(wc) => {
|
||||
let WithChannels { tx, mut rx, .. } = wc;
|
||||
let mut count = 0;
|
||||
while let Ok(Some(item)) = rx.recv().await {
|
||||
self.process(item);
|
||||
count += 1;
|
||||
}
|
||||
tx.send(count).await.ok();
|
||||
}
|
||||
MyMessage::Process(wc) => {
|
||||
let WithChannels { tx, mut rx, inner, .. } = wc;
|
||||
tokio::task::spawn(async move {
|
||||
while let Ok(Some(update)) = rx.recv().await {
|
||||
if let Some(result) = process(update, &inner) {
|
||||
if tx.send(result).await.is_err() { break; }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
MyMessage::Log(wc) => {
|
||||
let WithChannels { inner, .. } = wc;
|
||||
println!("{}", inner.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Quick Reference
|
||||
|
||||
```rust
|
||||
// Client-side errors
|
||||
use irpc::{Error, RequestError, Result};
|
||||
|
||||
// Request errors (connection/stream open failures)
|
||||
match client.rpc(GetReq("key".into())).await {
|
||||
Ok(result) => { ... }
|
||||
Err(Error::Request { source }) => { ... } // Connection failed
|
||||
Err(Error::OneshotRecv { source }) => { ... } // Response channel error
|
||||
}
|
||||
|
||||
// Channel errors
|
||||
use irpc::channel::{SendError, mpsc::RecvError, oneshot::RecvError};
|
||||
|
||||
// SendError: ReceiverClosed | MaxMessageSizeExceeded | Io
|
||||
// RecvError (oneshot): SenderClosed | MaxMessageSizeExceeded | Io
|
||||
// RecvError (mpsc): MaxMessageSizeExceeded | Io
|
||||
```
|
||||
|
||||
## Constants
|
||||
|
||||
```rust
|
||||
pub const MAX_MESSAGE_SIZE: u64 = 16 * 1024 * 1024; // 16 MiB
|
||||
pub const ERROR_CODE_MAX_MESSAGE_SIZE_EXCEEDED: u32 = 1;
|
||||
pub const ERROR_CODE_INVALID_POSTCARD: u32 = 2;
|
||||
// Connection close code 0 = clean shutdown
|
||||
```
|
||||
Reference in New Issue
Block a user