Skip to main content

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}