Skip to main content

confium_net/transports/
inproc.rs

1//! In-process transport.
2//!
3//! `inproc://<name>` URLs address a named, in-memory channel. A
4//! [`InprocListener`] registered under a name accepts connections from
5//! [`InprocTransport`] peers that `connect` to the same name. Each
6//! accepted connection is a pair of [`std::sync::mpsc`] channels
7//! carrying owned byte vectors, giving a reliable, ordered, single-use
8//! byte stream.
9//!
10//! This transport exists for tests and single-process threshold-
11//! cryptography simulation — multiple party state machines running in
12//! one binary, exchanging protocol rounds through loopback channels.
13
14use std::collections::HashMap;
15use std::sync::Mutex;
16use std::sync::OnceLock;
17use std::sync::mpsc;
18
19use url::Url;
20
21use crate::Listener;
22use crate::Result;
23use crate::Transport;
24use crate::error::ClosedSnafu;
25use crate::error::MalformedUrlSnafu;
26use crate::registry::TransportKind;
27
28/// Global, process-wide table of named inproc listening endpoints.
29///
30/// A listener registers a `Sender<InprocHandshake>` here under its URL
31/// name; a connector pulls a matching receiver out to complete the
32/// rendezvous. The table is lazy-initialized on first use.
33static LISTENERS: OnceLock<Mutex<HashMap<String, mpsc::Sender<InprocHandshake>>>> = OnceLock::new();
34
35fn listeners() -> &'static Mutex<HashMap<String, mpsc::Sender<InprocHandshake>>> {
36    LISTENERS.get_or_init(|| Mutex::new(HashMap::new()))
37}
38
39/// One half of a freshly established inproc connection. The connector
40/// receives the pair of channels it will use to talk to the accepted
41/// peer.
42struct InprocHandshake {
43    /// Channel the connector writes into and the accepted peer reads
44    /// from.
45    connector_to_peer: mpsc::Receiver<Vec<u8>>,
46    /// Channel the accepted peer writes into and the connector reads
47    /// from.
48    peer_to_connector: mpsc::Sender<Vec<u8>>,
49}
50
51/// Extract the channel name from an `inproc://` URL.
52fn channel_name(url: &Url) -> Result<String> {
53    let name = url.host_str().unwrap_or("");
54    if name.is_empty() {
55        return MalformedUrlSnafu {
56            scheme: "inproc",
57            url: url.to_string(),
58            reason: "missing channel name (use inproc://<name>)",
59        }
60        .fail();
61    }
62    Ok(name.to_string())
63}
64
65/// Connected in-process transport. Messages are framed as owned
66/// `Vec<u8>`; `send` pushes one, `recv` pops one into the caller's
67/// buffer.
68pub struct InprocTransport {
69    tx: Option<mpsc::Sender<Vec<u8>>>,
70    rx: Option<mpsc::Receiver<Vec<u8>>>,
71    /// Bytes from the current message not yet drained into the caller's
72    /// buffer; the remainder is held until the next `recv`.
73    pending: Vec<u8>,
74}
75
76impl InprocTransport {
77    fn new(tx: mpsc::Sender<Vec<u8>>, rx: mpsc::Receiver<Vec<u8>>) -> Self {
78        Self {
79            tx: Some(tx),
80            rx: Some(rx),
81            pending: Vec::new(),
82        }
83    }
84}
85
86impl Transport for InprocTransport {
87    fn send(&mut self, data: &[u8]) -> Result<()> {
88        match &self.tx {
89            Some(tx) => tx.send(data.to_vec()).map_err(|_| ClosedSnafu.build()),
90            None => ClosedSnafu.fail(),
91        }
92    }
93
94    fn recv(&mut self, buf: &mut [u8]) -> Result<usize> {
95        if self.pending.is_empty() {
96            let rx = match &self.rx {
97                Some(rx) => rx,
98                None => return ClosedSnafu.fail(),
99            };
100            let msg = rx.recv().map_err(|_| ClosedSnafu.build())?;
101            self.pending = msg;
102        }
103        let n = std::cmp::min(self.pending.len(), buf.len());
104        buf[..n].copy_from_slice(&self.pending[..n]);
105        // If the caller's buffer was too small for the whole message,
106        // drop the remainder — inproc messages are atomic units (one
107        // `send` == one `recv` payload) and partial framing across
108        // calls would surprise callers. The dropped bytes are reported
109        // back via the returned count; a stricter buffer-too-small
110        // error is reserved for transports that preserve framing.
111        self.pending.drain(..n);
112        Ok(n)
113    }
114
115    fn close(&mut self) -> Result<()> {
116        self.tx = None;
117        self.rx = None;
118        self.pending.clear();
119        Ok(())
120    }
121}
122
123/// Listening endpoint for `inproc://` connections.
124///
125/// Holding this alive keeps the named channel registered in the global
126/// table; dropping it unregisters the name.
127pub struct InprocListener {
128    name: String,
129    inbox: Option<mpsc::Receiver<InprocHandshake>>,
130}
131
132impl InprocListener {
133    fn new(name: String, inbox: mpsc::Receiver<InprocHandshake>) -> Self {
134        Self {
135            name,
136            inbox: Some(inbox),
137        }
138    }
139}
140
141impl Listener for InprocListener {
142    fn accept(&mut self) -> Result<Box<dyn Transport>> {
143        let inbox = match &self.inbox {
144            Some(rx) => rx,
145            None => return ClosedSnafu.fail(),
146        };
147        let handshake = inbox.recv().map_err(|_| ClosedSnafu.build())?;
148        Ok(Box::new(InprocTransport::new(
149            handshake.peer_to_connector,
150            handshake.connector_to_peer,
151        )))
152    }
153}
154
155impl Drop for InprocListener {
156    fn drop(&mut self) {
157        if let Ok(mut table) = listeners().lock() {
158            table.remove(&self.name);
159        }
160    }
161}
162
163/// Registry kind for the in-process transport.
164pub struct InprocKind;
165
166impl TransportKind for InprocKind {
167    fn schemes(&self) -> &'static [&'static str] {
168        &["inproc"]
169    }
170
171    fn connect(&self, url: &Url) -> Result<Box<dyn Transport>> {
172        let name = channel_name(url)?;
173        let tx = {
174            let mut table = listeners().lock().expect("inproc listener table poisoned");
175            match table.remove(&name) {
176                Some(sender) => sender,
177                None => {
178                    return MalformedUrlSnafu {
179                        scheme: "inproc",
180                        url: url.to_string(),
181                        reason: "no listener registered for this channel name",
182                    }
183                    .fail();
184                }
185            }
186        };
187        let (connector_to_peer_tx, connector_to_peer_rx) = mpsc::channel::<Vec<u8>>();
188        let (peer_to_connector_tx, peer_to_connector_rx) = mpsc::channel::<Vec<u8>>();
189        tx.send(InprocHandshake {
190            connector_to_peer: connector_to_peer_rx,
191            peer_to_connector: peer_to_connector_tx,
192        })
193        .map_err(|_| ClosedSnafu.build())?;
194        Ok(Box::new(InprocTransport::new(
195            connector_to_peer_tx,
196            peer_to_connector_rx,
197        )))
198    }
199
200    fn listen(&self, url: &Url) -> Result<Box<dyn Listener>> {
201        let name = channel_name(url)?;
202        let (handshake_tx, handshake_rx) = mpsc::channel::<InprocHandshake>();
203        let mut table = listeners().lock().expect("inproc listener table poisoned");
204        if table.contains_key(&name) {
205            return MalformedUrlSnafu {
206                scheme: "inproc",
207                url: url.to_string(),
208                reason: "channel name already in use",
209            }
210            .fail();
211        }
212        table.insert(name.clone(), handshake_tx);
213        Ok(Box::new(InprocListener::new(name, handshake_rx)))
214    }
215}
216
217crate::register_transport!(InprocKind);
218
219#[cfg(test)]
220mod tests {
221    /// Use a unique name per test to avoid collisions in the global
222    /// table, since inventory registration is process-wide and tests
223    /// run in the same binary.
224    fn unique_name(tag: &str) -> String {
225        use std::sync::atomic::AtomicU64;
226        use std::sync::atomic::Ordering;
227        static SEQ: AtomicU64 = AtomicU64::new(0);
228        let n = SEQ.fetch_add(1, Ordering::Relaxed);
229        format!("test-{tag}-{n}")
230    }
231
232    #[test]
233    fn round_trip_single_message() {
234        let name = unique_name("rt");
235        let url = format!("inproc://{name}");
236        let mut listener = crate::listen(&url).unwrap();
237        // Listener must exist before connect consumes it from the table.
238        let mut client = crate::connect(&url).unwrap();
239        let mut server = listener.accept().unwrap();
240
241        let payload = b"hello threshold world";
242        client.send(payload).unwrap();
243        let mut buf = [0u8; 64];
244        let n = server.recv(&mut buf).unwrap();
245        assert_eq!(&buf[..n], payload);
246    }
247
248    #[test]
249    fn bidirectional_traffic() {
250        let name = unique_name("bi");
251        let url = format!("inproc://{name}");
252        let mut listener = crate::listen(&url).unwrap();
253        let mut client = crate::connect(&url).unwrap();
254        let mut server = listener.accept().unwrap();
255
256        client.send(b"c2s").unwrap();
257        server.send(b"s2c").unwrap();
258
259        let mut buf = [0u8; 16];
260        let n = server.recv(&mut buf).unwrap();
261        assert_eq!(&buf[..n], b"c2s");
262        let n = client.recv(&mut buf).unwrap();
263        assert_eq!(&buf[..n], b"s2c");
264    }
265
266    #[test]
267    fn recv_after_close_reports_closed() {
268        let name = unique_name("close");
269        let url = format!("inproc://{name}");
270        let mut listener = crate::listen(&url).unwrap();
271        let mut client = crate::connect(&url).unwrap();
272        let mut server = listener.accept().unwrap();
273
274        client.close().unwrap();
275        let mut buf = [0u8; 16];
276        let err = server.recv(&mut buf).unwrap_err();
277        assert!(matches!(err, crate::error::Error::Closed { .. }));
278    }
279
280    #[test]
281    fn connect_without_listener_fails() {
282        let url = "inproc://no-such-channel-zzz";
283        let err = crate::connect(url).err().unwrap();
284        assert!(matches!(
285            err,
286            crate::error::Error::MalformedUrl { scheme, .. } if scheme == "inproc"
287        ));
288    }
289
290    #[test]
291    fn listen_twice_same_name_collides() {
292        let name = unique_name("collide");
293        let url = format!("inproc://{name}");
294        let _l1 = crate::listen(&url).unwrap();
295        let err = crate::listen(&url).err().unwrap();
296        assert!(matches!(
297            err,
298            crate::error::Error::MalformedUrl { scheme, reason, .. }
299                if scheme == "inproc" && reason.contains("already in use")
300        ));
301    }
302}