monocoque_core/inproc.rs
1//! In-process transport for zero-copy messaging within the same process.
2//!
3//! The inproc transport provides high-performance communication between sockets
4//! in the same process using channels, without TCP/IP overhead.
5//!
6//! # Features
7//!
8//! - **Zero-copy**: Messages are shared via `Arc<Vec<Bytes>>` between sockets
9//! - **Thread-safe**: Global registry protected by `DashMap`
10//! - **Fast**: No serialization, network, or syscall overhead
11//! - **`ZeroMQ` compatible**: Uses `inproc://` URI scheme
12//!
13//! # Usage
14//!
15//! ```ignore
16//! use monocoque_core::inproc::{bind_inproc, connect_inproc};
17//! use bytes::Bytes;
18//!
19//! // Bind to an inproc endpoint
20//! let (sender, receiver) = bind_inproc("inproc://my-endpoint").unwrap();
21//!
22//! // Connect from another task
23//! let client = connect_inproc("inproc://my-endpoint").unwrap();
24//!
25//! // Send messages
26//! client.send(vec![Bytes::from("Hello")]).unwrap();
27//!
28//! // Receive messages
29//! if let Ok(msg) = receiver.recv() {
30//! println!("Received: {:?}", msg);
31//! }
32//! ```
33
34use bytes::Bytes;
35use dashmap::DashMap;
36use flume::{Receiver, Sender};
37use std::io;
38
39/// Message type for inproc transport (multipart message)
40pub type InprocMessage = Vec<Bytes>;
41
42/// Sender half of an inproc connection
43pub type InprocSender = Sender<InprocMessage>;
44
45/// Receiver half of an inproc connection
46pub type InprocReceiver = Receiver<InprocMessage>;
47
48/// Global registry of inproc endpoints (server receives from clients)
49static INPROC_REGISTRY: std::sync::LazyLock<DashMap<String, InprocSender>> =
50 std::sync::LazyLock::new(DashMap::new);
51
52/// Registry of server→client reply receivers for bidirectional inproc connections.
53///
54/// When `bind_inproc_bidi` is called, the receiving half of the server→client
55/// channel is registered here so that `connect_inproc_bidi` can clone it and hand
56/// it to the client to receive the server's replies. `flume` receivers are MPMC,
57/// so a single bound endpoint can serve one client at a time cleanly (concurrent
58/// clients share the reply stream, which is why ZAP requests carry a request_id).
59static INPROC_REPLY_REGISTRY: std::sync::LazyLock<DashMap<String, InprocReceiver>> =
60 std::sync::LazyLock::new(DashMap::new);
61
62/// Bind to an inproc endpoint and return sender/receiver pair.
63///
64/// The endpoint is registered in the global registry. Multiple clients can
65/// connect to this endpoint using `connect_inproc()`.
66///
67/// # Arguments
68///
69/// * `endpoint` - The endpoint URI (must start with "inproc://")
70///
71/// # Returns
72///
73/// Returns a tuple of (sender, receiver):
74/// - `sender`: Used to send messages from this socket
75/// - `receiver`: Used to receive messages sent by connected clients
76///
77/// # Errors
78///
79/// Returns an error if:
80/// - The endpoint doesn't start with "inproc://"
81/// - The endpoint is already bound
82/// - The endpoint name is empty
83///
84/// # Example
85///
86/// ```
87/// use monocoque_core::inproc::bind_inproc;
88///
89/// let (sender, receiver) = bind_inproc("inproc://my-endpoint-bind").unwrap();
90/// // sender and receiver are ready for use
91/// ```
92pub fn bind_inproc(endpoint: &str) -> io::Result<(InprocSender, InprocReceiver)> {
93 // Validate endpoint format
94 let name = validate_and_extract_name(endpoint)?;
95
96 // Create unbounded channel for message passing
97 let (tx, rx) = flume::unbounded();
98
99 // Try to insert into registry
100 if INPROC_REGISTRY
101 .insert(name.to_string(), tx.clone())
102 .is_some()
103 {
104 return Err(io::Error::new(
105 io::ErrorKind::AddrInUse,
106 format!("inproc endpoint '{name}' is already bound"),
107 ));
108 }
109
110 Ok((tx, rx))
111}
112
113/// Connect to an inproc endpoint.
114///
115/// Returns a sender that can be used to send messages to the bound endpoint.
116/// This function blocks until the endpoint becomes available.
117///
118/// # Arguments
119///
120/// * `endpoint` - The endpoint URI (must start with "inproc://")
121///
122/// # Returns
123///
124/// Returns a sender that can send messages to the bound endpoint.
125///
126/// # Errors
127///
128/// Returns an error if:
129/// - The endpoint doesn't start with "inproc://"
130/// - The endpoint is not bound
131/// - The endpoint name is empty
132///
133/// # Example
134///
135/// ```rust,no_run
136/// use monocoque_core::inproc::connect_inproc;
137/// use bytes::Bytes;
138///
139/// # fn example() -> std::io::Result<()> {
140/// let sender = connect_inproc("inproc://my-endpoint")?;
141///
142/// // Send a message
143/// sender.send(vec![Bytes::from("Hello")]).map_err(|_| {
144/// std::io::Error::new(std::io::ErrorKind::BrokenPipe, "receiver dropped")
145/// })?;
146/// # Ok(())
147/// # }
148/// ```
149pub fn connect_inproc(endpoint: &str) -> io::Result<InprocSender> {
150 // Validate endpoint format
151 let name = validate_and_extract_name(endpoint)?;
152
153 // Look up the endpoint in the registry
154 if let Some(sender) = INPROC_REGISTRY.get(name) {
155 return Ok(sender.clone());
156 }
157
158 Err(io::Error::new(
159 io::ErrorKind::NotFound,
160 format!("inproc endpoint '{name}' not found (must bind before connect)"),
161 ))
162}
163
164/// Unbind an inproc endpoint, removing it from the global registry.
165///
166/// This should be called when a bound socket is closed to free up the endpoint name.
167///
168/// # Arguments
169///
170/// * `endpoint` - The endpoint URI (must start with "inproc://")
171///
172/// # Example
173///
174/// ```rust,no_run
175/// use monocoque_core::inproc::{bind_inproc, unbind_inproc};
176///
177/// # fn example() -> std::io::Result<()> {
178/// let (sender, receiver) = bind_inproc("inproc://my-endpoint")?;
179///
180/// // ... use the endpoint ...
181///
182/// // Clean up when done
183/// unbind_inproc("inproc://my-endpoint")?;
184/// # Ok(())
185/// # }
186/// ```
187pub fn unbind_inproc(endpoint: &str) -> io::Result<()> {
188 let name = validate_and_extract_name(endpoint)?;
189 INPROC_REGISTRY.remove(name);
190 INPROC_REPLY_REGISTRY.remove(name);
191 Ok(())
192}
193
194/// Bind to an inproc endpoint for bidirectional communication.
195///
196/// Returns `(to_clients_tx, from_clients_rx)`:
197/// - `to_clients_tx`: The server uses this to send replies back to the client.
198/// It is registered so that `connect_inproc_bidi` can retrieve it.
199/// - `from_clients_rx`: The server reads client messages from this.
200///
201/// The caller (server side) owns both halves. The client side
202/// (`connect_inproc_bidi`) gets a `(to_server_tx, from_server_rx)` pair.
203///
204/// # Errors
205///
206/// Returns an error if the endpoint is already bound.
207pub fn bind_inproc_bidi(endpoint: &str) -> io::Result<(InprocSender, InprocReceiver)> {
208 let name = validate_and_extract_name(endpoint)?;
209
210 // Channel: client → server
211 let (client_to_server_tx, client_to_server_rx) = flume::unbounded::<InprocMessage>();
212 // Channel: server → client
213 let (server_to_client_tx, server_to_client_rx) = flume::unbounded::<InprocMessage>();
214
215 // Register the client→server sender (connect_inproc_bidi clones it to send).
216 if INPROC_REGISTRY
217 .insert(name.to_string(), client_to_server_tx)
218 .is_some()
219 {
220 return Err(io::Error::new(
221 io::ErrorKind::AddrInUse,
222 format!("inproc endpoint '{name}' is already bound"),
223 ));
224 }
225
226 // Register the server→client receiver (connect_inproc_bidi clones it to
227 // receive the server's replies).
228 INPROC_REPLY_REGISTRY.insert(name.to_string(), server_to_client_rx);
229
230 // The server sends replies via server_to_client_tx and reads requests from
231 // client_to_server_rx.
232 Ok((server_to_client_tx, client_to_server_rx))
233}
234
235/// Connect to an inproc endpoint for bidirectional communication.
236///
237/// Returns `(to_server_tx, from_server_rx)` so the client can both send
238/// messages to the server and receive replies from it.
239///
240/// The server must have called `bind_inproc_bidi` before this is called.
241///
242/// # Errors
243///
244/// Returns an error if the endpoint is not bound.
245pub fn connect_inproc_bidi(endpoint: &str) -> io::Result<(InprocSender, InprocReceiver)> {
246 let name = validate_and_extract_name(endpoint)?;
247
248 // Sender the client uses to reach the server (registered by bind_inproc_bidi).
249 let to_server = INPROC_REGISTRY
250 .get(name)
251 .map(|r| r.clone())
252 .ok_or_else(|| {
253 io::Error::new(
254 io::ErrorKind::NotFound,
255 format!("inproc endpoint '{name}' not found (must bind before connect)"),
256 )
257 })?;
258
259 // Receiver the client uses to read the server's replies. bind_inproc_bidi
260 // registered the server→client receiving half here; cloning it (flume is
261 // MPMC) gives the client its own handle onto the reply stream.
262 let from_server = INPROC_REPLY_REGISTRY
263 .get(name)
264 .map(|r| r.clone())
265 .ok_or_else(|| {
266 io::Error::new(
267 io::ErrorKind::NotFound,
268 format!(
269 "inproc reply channel for '{name}' not found; \
270 use bind_inproc_bidi on the server side"
271 ),
272 )
273 })?;
274
275 Ok((to_server, from_server))
276}
277
278/// List all currently bound inproc endpoints.
279///
280/// This is primarily useful for debugging and testing.
281///
282/// # Returns
283///
284/// Returns a vector of endpoint names (without the "inproc://" prefix).
285pub fn list_inproc_endpoints() -> Vec<String> {
286 INPROC_REGISTRY
287 .iter()
288 .map(|entry| entry.key().clone())
289 .collect()
290}
291
292/// Validate endpoint format and extract the name.
293///
294/// # Arguments
295///
296/// * `endpoint` - The full endpoint URI (e.g., "<inproc://my-endpoint>")
297///
298/// # Returns
299///
300/// Returns the endpoint name without the "inproc://" prefix.
301///
302/// # Errors
303///
304/// Returns an error if the endpoint doesn't start with "inproc://" or has an empty name.
305fn validate_and_extract_name(endpoint: &str) -> io::Result<&str> {
306 const PREFIX: &str = "inproc://";
307
308 if !endpoint.starts_with(PREFIX) {
309 return Err(io::Error::new(
310 io::ErrorKind::InvalidInput,
311 format!("inproc endpoint must start with '{PREFIX}', got: '{endpoint}'"),
312 ));
313 }
314
315 let name = &endpoint[PREFIX.len()..];
316 if name.is_empty() {
317 return Err(io::Error::new(
318 io::ErrorKind::InvalidInput,
319 "inproc endpoint name cannot be empty",
320 ));
321 }
322 if name.chars().any(char::is_control) {
323 return Err(io::Error::new(
324 io::ErrorKind::InvalidInput,
325 "inproc endpoint name cannot contain control characters",
326 ));
327 }
328
329 Ok(name)
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_validate_endpoint() {
338 assert!(validate_and_extract_name("inproc://test").is_ok());
339 assert_eq!(validate_and_extract_name("inproc://test").unwrap(), "test");
340
341 assert!(validate_and_extract_name("tcp://test").is_err());
342 assert!(validate_and_extract_name("inproc://").is_err());
343 assert!(validate_and_extract_name("").is_err());
344 }
345
346 #[test]
347 fn test_validate_endpoint_rejects_control_characters() {
348 assert!(validate_and_extract_name("inproc://tenant\0shadow").is_err());
349 assert!(validate_and_extract_name("inproc://tenant\nshadow").is_err());
350 }
351
352 #[test]
353 fn test_bind_duplicate() {
354 let endpoint = "inproc://test-duplicate";
355
356 // First bind should succeed
357 let result1 = bind_inproc(endpoint);
358 assert!(result1.is_ok());
359
360 // Second bind should fail
361 let result2 = bind_inproc(endpoint);
362 assert!(result2.is_err());
363 assert_eq!(result2.unwrap_err().kind(), io::ErrorKind::AddrInUse);
364
365 // Cleanup
366 let _ = unbind_inproc(endpoint);
367 }
368
369 #[test]
370 fn test_bind_and_connect() {
371 let endpoint = "inproc://test-connect";
372
373 // Bind
374 let (_tx, rx) = bind_inproc(endpoint).unwrap();
375
376 // Connect
377 let client = connect_inproc(endpoint).unwrap();
378
379 // Send message from client
380 let msg = vec![Bytes::from("Hello, inproc!")];
381 client.send(msg.clone()).unwrap();
382
383 // Receive on bound socket (non-blocking recv_timeout)
384 let received = rx
385 .recv_timeout(std::time::Duration::from_millis(100))
386 .unwrap();
387 assert_eq!(received, msg);
388
389 // Cleanup
390 unbind_inproc(endpoint).unwrap();
391 }
392
393 #[test]
394 fn test_list_endpoints() {
395 let ep1 = "inproc://test-list-1";
396 let ep2 = "inproc://test-list-2";
397
398 let _bind1 = bind_inproc(ep1).unwrap();
399 let _bind2 = bind_inproc(ep2).unwrap();
400
401 let endpoints = list_inproc_endpoints();
402 assert!(endpoints.contains(&"test-list-1".to_string()));
403 assert!(endpoints.contains(&"test-list-2".to_string()));
404
405 // Cleanup
406 unbind_inproc(ep1).unwrap();
407 unbind_inproc(ep2).unwrap();
408 }
409}