Skip to main content

ruststream_zeromq/
in_process.rs

1//! The in-process mode, behind the `testing` feature: each production broker of this crate runs
2//! inside the test process, over a transport that carries the frames a socket would carry.
3//!
4//! A broker connected in process keeps its own connected form, lifecycle bookkeeping and publish
5//! policies; only the socket underneath is replaced. A publish is framed the way the socket
6//! publisher frames it, and a subscription reads the frames the way its socket's driver reads
7//! them, so a header the wire cannot carry is refused and a request is stamped with its peer
8//! exactly as over ZMTP. Which subscriptions a message reaches is the socket pattern's own rule,
9//! named once in [`Pick`].
10//!
11//! Each broker is a transport of its own. A message that leaves for a peer outside the broker
12//! (a queue this service dials, a listener it binds before any subscription) is recorded in the
13//! published log and reaches no subscription, since that peer is another process.
14
15use std::collections::HashMap;
16use std::fmt;
17use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
18use std::time::Duration;
19
20use bytes::Bytes;
21use ruststream::testing::{Coordinator, InProcess, TestableBroker};
22use ruststream::{OutgoingMessage, RawMessage};
23use tokio::sync::mpsc;
24use zeromq::ZmqError as WireError;
25use zeromq::ZmqMessage as WireMessage;
26
27use crate::common::Lifecycle;
28use crate::endpoint::{Bind, Connect, EndpointRole, Side};
29use crate::error::ZmqError;
30use crate::fanout::{ConnectedZmqFanout, ZmqFanout};
31use crate::message::ZmqMessage;
32use crate::queue::{ConnectedZmqQueue, ZmqQueue};
33use crate::rpc::{ConnectedZmqRpc, REPLY_PREFIX, ZmqRpc, peer_identity};
34use crate::wire;
35
36/// The channel a subscription reads its deliveries from, the same one a socket driver feeds.
37pub(crate) type Deliveries = mpsc::UnboundedReceiver<Result<ZmqMessage, ZmqError>>;
38
39/// How a subscription reads the frames it receives: the reader its socket's driver uses.
40pub(crate) type Read = fn(WireMessage) -> Option<Result<ZmqMessage, ZmqError>>;
41
42/// Which subscriptions of a broker one message reaches: the socket pattern's own rule.
43#[derive(Debug, Clone, Copy)]
44pub(crate) enum Pick<'a> {
45    /// Every subscription. On an endpoint a subscription binds there is one, and its socket
46    /// takes whatever a peer sends there, whatever the name frame says.
47    All,
48    /// One subscription, taken in turn: a PUSH or a DEALER hands each message to one of the
49    /// sockets that dialed it, whatever the name frame says.
50    OneInTurn,
51    /// Every subscription whose name is a prefix of this name: the SUB filter.
52    Prefix(&'a str),
53    /// None: the message leaves for a peer outside this broker.
54    Nobody,
55}
56
57/// Who answers at a peer identity a request carried.
58#[derive(Debug)]
59enum Peer {
60    /// A request of this service, waiting on its DEALER for the answer.
61    Waiting(mpsc::UnboundedSender<WireMessage>),
62    /// A peer outside this service that sent a request the test injected. It stays connected,
63    /// so an answer to it is taken, and recorded.
64    Foreign,
65}
66
67struct Subscription {
68    id: u64,
69    name: String,
70    deliveries: mpsc::UnboundedSender<Result<ZmqMessage, ZmqError>>,
71    read: Read,
72}
73
74#[derive(Default)]
75struct State {
76    next_id: u64,
77    /// In the order they opened, which is the order a peer takes them in turn.
78    subscriptions: Vec<Subscription>,
79    turn: usize,
80    log: HashMap<String, Vec<RawMessage>>,
81    peers: HashMap<Bytes, Peer>,
82}
83
84/// One broker's in-process transport: its subscriptions, the peers its requests wait at, and a
85/// log of every message it carried.
86#[derive(Default)]
87pub(crate) struct Bus {
88    state: Mutex<State>,
89    coordinator: OnceLock<Coordinator>,
90}
91
92impl fmt::Debug for Bus {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        f.debug_struct("Bus").finish_non_exhaustive()
95    }
96}
97
98/// A subscription's place on the in-process transport, which its driver handle gives up when it
99/// drops.
100pub(crate) struct Registration {
101    bus: Arc<Bus>,
102    id: u64,
103}
104
105impl Registration {
106    /// Takes the subscription off the transport: a message sent later does not reach it.
107    pub(crate) fn give_up(&self) {
108        self.bus
109            .state()
110            .subscriptions
111            .retain(|sub| sub.id != self.id);
112    }
113}
114
115/// The harness's count of one delivery in flight, handed back when the delivery goes.
116///
117/// A delivery is settled or dropped exactly once, and either way its `Drop` runs, so this is
118/// where the count comes down. A delivery the harness does not drive carries none.
119#[derive(Debug, Default)]
120pub(crate) struct Release(Option<Coordinator>);
121
122impl Drop for Release {
123    fn drop(&mut self) {
124        if let Some(coordinator) = &self.0 {
125            coordinator.consumed();
126        }
127    }
128}
129
130impl Bus {
131    fn state(&self) -> MutexGuard<'_, State> {
132        // A panic under this lock leaves no invariant half-kept: every critical section is a
133        // single insert, remove or lookup.
134        self.state.lock().unwrap_or_else(PoisonError::into_inner)
135    }
136
137    fn install(&self, coordinator: Coordinator) {
138        let _ = self.coordinator.set(coordinator);
139    }
140
141    /// Opens a subscription named `name`, whose deliveries are read with `read`.
142    pub(crate) fn subscribe(
143        self: &Arc<Self>,
144        name: &str,
145        read: Read,
146    ) -> (Deliveries, Registration) {
147        let (tx, rx) = mpsc::unbounded_channel();
148        let mut state = self.state();
149        let id = state.next_id;
150        state.next_id += 1;
151        state.subscriptions.push(Subscription {
152            id,
153            name: name.to_owned(),
154            deliveries: tx,
155            read,
156        });
157        drop(state);
158        (
159            rx,
160            Registration {
161                bus: Arc::clone(self),
162                id,
163            },
164        )
165    }
166
167    /// Carries `frames`, published to `destination`, to the subscriptions `pick` names, behind
168    /// the peer `identity` when a ROUTER is to read it, and records it in the log.
169    pub(crate) fn send(
170        &self,
171        destination: &str,
172        frames: WireMessage,
173        identity: Option<&Bytes>,
174        pick: Pick<'_>,
175    ) -> usize {
176        let mut state = self.state();
177        record(&mut state, destination, &frames);
178        let chosen: Vec<_> = match pick {
179            Pick::Nobody => Vec::new(),
180            Pick::All => state.subscriptions.iter().collect(),
181            Pick::Prefix(name) => state
182                .subscriptions
183                .iter()
184                .filter(|sub| name.starts_with(sub.name.as_str()))
185                .collect(),
186            Pick::OneInTurn if state.subscriptions.is_empty() => Vec::new(),
187            Pick::OneInTurn => {
188                let at = state.turn % state.subscriptions.len();
189                state.turn = state.turn.wrapping_add(1);
190                vec![&state.subscriptions[at]]
191            }
192        };
193        let chosen: Vec<_> = chosen
194            .into_iter()
195            .map(|sub| (sub.deliveries.clone(), sub.read))
196            .collect();
197        // The sends are the subscribers' business, not the registry's.
198        drop(state);
199
200        let mut received = frames;
201        if let Some(identity) = identity {
202            received.push_front(identity.clone());
203        }
204        let reached = chosen.len();
205        for (deliveries, read) in chosen {
206            let Some(delivery) = read(received.clone()) else {
207                continue;
208            };
209            let delivery = match (delivery, self.coordinator.get()) {
210                (Ok(message), Some(coordinator)) => {
211                    // Counted before it is handed over, so the harness never sees the delivery
212                    // consumed before it saw it enqueued.
213                    coordinator.enqueued();
214                    Ok(message.counted(Release(Some(coordinator.clone()))))
215                }
216                (delivery, _) => delivery,
217            };
218            // A subscription that closed between the pick and the send takes nothing; its
219            // delivery drops here and gives its count back.
220            let _ = deliveries.send(delivery);
221        }
222        reached
223    }
224
225    /// Answers the peer whose identity leads `frames`, the way a ROUTER routes a reply: to a
226    /// request of this service that is waiting, to a foreign peer that asked, and to nobody
227    /// else.
228    ///
229    /// # Errors
230    ///
231    /// Returns [`ZmqError::Send`] in the ROUTER's own words when no peer carries the identity.
232    pub(crate) fn reply(&self, destination: &str, frames: WireMessage) -> Result<(), ZmqError> {
233        let mut frames = frames.into_vecdeque();
234        let identity = frames.pop_front().unwrap_or_default();
235        let frames = WireMessage::try_from(frames)
236            .map_err(|_| ZmqError::Wire("a reply needs name and payload frames".into()))?;
237        let mut state = self.state();
238        let waiting = match state.peers.get(&identity) {
239            Some(Peer::Waiting(inbox)) => Some(inbox.clone()),
240            Some(Peer::Foreign) => {
241                // A foreign peer the harness connected asks once, and leaves with its answer.
242                state.peers.remove(&identity);
243                None
244            }
245            None => {
246                return Err(ZmqError::Send {
247                    name: destination.to_owned(),
248                    reason: WireError::Other("Destination client not found by identity")
249                        .to_string(),
250                });
251            }
252        };
253        record(&mut state, destination, &frames);
254        drop(state);
255        if let Some(inbox) = waiting {
256            let _ = inbox.send(frames);
257        }
258        Ok(())
259    }
260
261    /// Connects a DEALER of this service at `identity`, and returns what it receives.
262    pub(crate) fn open_inbox(&self, identity: Bytes) -> mpsc::UnboundedReceiver<WireMessage> {
263        let (tx, rx) = mpsc::unbounded_channel();
264        self.state().peers.insert(identity, Peer::Waiting(tx));
265        rx
266    }
267
268    /// Disconnects the DEALER at `identity`: an answer that comes later reaches no peer.
269    pub(crate) fn close_inbox(&self, identity: &Bytes) {
270        self.state().peers.remove(identity);
271    }
272
273    /// Connects a foreign DEALER and returns its identity.
274    fn foreign_peer(&self) -> Bytes {
275        let identity = peer_identity();
276        self.state().peers.insert(identity.clone(), Peer::Foreign);
277        identity
278    }
279
280    fn published(&self, name: &str) -> Vec<RawMessage> {
281        self.state().log.get(name).cloned().unwrap_or_default()
282    }
283}
284
285/// Records what a peer receiving `frames` reads: the payload frame as it was written, the headers
286/// the frame decodes to. A message whose frames no peer could read is recorded as its bytes.
287fn record(state: &mut State, destination: &str, frames: &WireMessage) {
288    let entry = match wire::decode(frames.clone()) {
289        Ok((_, headers, payload)) => RawMessage::new(destination, payload).with_headers(headers),
290        Err(_) => RawMessage::new(destination, frames.get(2).cloned().unwrap_or_default()),
291    };
292    state
293        .log
294        .entry(destination.to_owned())
295        .or_default()
296        .push(entry);
297}
298
299/// Builds the frames a foreign peer sends for `message`.
300fn foreign(message: &OutgoingMessage<'_>) -> WireMessage {
301    wire::encode_foreign(
302        message.name(),
303        message.headers(),
304        Bytes::copy_from_slice(message.payload()),
305    )
306}
307
308/// The positions of the subscriptions `pick` reaches, answered from the names alone.
309fn positions(subscriptions: &[&str], pick: Pick<'_>) -> Vec<usize> {
310    match pick {
311        Pick::Nobody | Pick::OneInTurn => Vec::new(),
312        Pick::All => (0..subscriptions.len()).collect(),
313        Pick::Prefix(name) => subscriptions
314            .iter()
315            .enumerate()
316            .filter(|(_, subscription)| name.starts_with(**subscription))
317            .map(|(position, _)| position)
318            .collect(),
319    }
320}
321
322impl<Role: EndpointRole> InProcess for ZmqQueue<Role> {
323    async fn connect_in_process(self) -> Result<Self::Connected, Self::Error> {
324        self.connect_with(Lifecycle::in_process).await
325    }
326}
327
328impl<Role: EndpointRole> InProcess for ZmqFanout<Role> {
329    async fn connect_in_process(self) -> Result<Self::Connected, Self::Error> {
330        self.connect_with(Lifecycle::in_process).await
331    }
332}
333
334impl InProcess for ZmqRpc {
335    async fn connect_in_process(self) -> Result<Self::Connected, Self::Error> {
336        self.connect_with(Lifecycle::in_process).await
337    }
338}
339
340/// The queue's routing: a peer that pushes into an endpoint a subscription binds reaches that
341/// subscription, and one that subscriptions dial hands each message to one of them in turn,
342/// whatever its name frame says. A publish of this service reaches its own subscription only
343/// through the listener that subscription bound.
344impl<Role: EndpointRole> TestableBroker for ConnectedZmqQueue<Role> {
345    fn install_coordinator(&self, coordinator: Coordinator) {
346        if let Some(bus) = self.lifecycle().in_process_bus() {
347            bus.install(coordinator);
348        }
349    }
350
351    fn inject(&self, message: OutgoingMessage<'_>) {
352        if let Some(bus) = self.lifecycle().in_process_bus() {
353            let pick = match self.lifecycle().endpoint.side() {
354                Side::Bind => Pick::All,
355                Side::Connect => Pick::OneInTurn,
356            };
357            bus.send(message.name(), foreign(&message), None, pick);
358        }
359    }
360
361    fn published(&self, name: &str) -> Vec<RawMessage> {
362        self.lifecycle()
363            .in_process_bus()
364            .map(|bus| bus.published(name))
365            .unwrap_or_default()
366    }
367
368    fn routes(&self, _destination: &str, subscriptions: &[&str]) -> Vec<usize> {
369        let pick = match self.lifecycle().endpoint.side() {
370            Side::Bind => Pick::All,
371            // A publish here goes to the peer the subscriptions read from, which is not one of
372            // them; a subscription that dialed refuses the publish outright.
373            Side::Connect => Pick::Nobody,
374        };
375        positions(subscriptions, pick)
376    }
377}
378
379/// The fan-out's routing: every subscription whose name prefixes the message's name, on either
380/// side, and for a publish of this service only through the listener its subscription bound.
381impl<Role: EndpointRole> TestableBroker for ConnectedZmqFanout<Role> {
382    fn install_coordinator(&self, coordinator: Coordinator) {
383        if let Some(bus) = self.lifecycle().in_process_bus() {
384            bus.install(coordinator);
385        }
386    }
387
388    fn inject(&self, message: OutgoingMessage<'_>) {
389        if let Some(bus) = self.lifecycle().in_process_bus() {
390            bus.send(
391                message.name(),
392                foreign(&message),
393                None,
394                Pick::Prefix(message.name()),
395            );
396        }
397    }
398
399    fn published(&self, name: &str) -> Vec<RawMessage> {
400        self.lifecycle()
401            .in_process_bus()
402            .map(|bus| bus.published(name))
403            .unwrap_or_default()
404    }
405
406    fn routes(&self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
407        let pick = match self.lifecycle().endpoint.side() {
408            Side::Bind => Pick::Prefix(destination),
409            Side::Connect => Pick::Nobody,
410        };
411        positions(subscriptions, pick)
412    }
413}
414
415/// The exchange's routing: a request reaches the responder that binds the endpoint, or one of the
416/// responders that dial it in turn, stamped with the peer that asked; an answer reaches that peer
417/// and no subscription.
418impl TestableBroker for ConnectedZmqRpc {
419    fn install_coordinator(&self, coordinator: Coordinator) {
420        if let Some(bus) = self.lifecycle().in_process_bus() {
421            bus.install(coordinator);
422        }
423    }
424
425    fn inject(&self, message: OutgoingMessage<'_>) {
426        if let Some(bus) = self.lifecycle().in_process_bus() {
427            let pick = match self.lifecycle().endpoint.side() {
428                Side::Bind => Pick::All,
429                Side::Connect => Pick::OneInTurn,
430            };
431            let identity = bus.foreign_peer();
432            if bus.send(message.name(), foreign(&message), Some(&identity), pick) == 0 {
433                // No responder took the request, so no answer will come for the peer.
434                bus.close_inbox(&identity);
435            }
436        }
437    }
438
439    fn published(&self, name: &str) -> Vec<RawMessage> {
440        self.lifecycle()
441            .in_process_bus()
442            .map(|bus| bus.published(name))
443            .unwrap_or_default()
444    }
445
446    fn routes(&self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
447        let pick = match self.lifecycle().endpoint.side() {
448            Side::Bind if !destination.starts_with(REPLY_PREFIX) => Pick::All,
449            Side::Bind | Side::Connect => Pick::Nobody,
450        };
451        positions(subscriptions, pick)
452    }
453}
454
455ruststream::register_testable_broker!(ZmqQueue<Bind>);
456ruststream::register_testable_broker!(ZmqQueue<Connect>);
457ruststream::register_testable_broker!(ZmqFanout<Bind>);
458ruststream::register_testable_broker!(ZmqFanout<Connect>);
459ruststream::register_testable_broker!(ZmqRpc);
460
461/// A request of this service over the in-process transport: the request leaves the way the
462/// DEALER sends it, reaches the responder this service binds when there is one, and waits for
463/// the answer that echoes its correlation id.
464pub(crate) async fn request(
465    bus: &Bus,
466    local: bool,
467    name: &str,
468    frames: WireMessage,
469    correlation: &str,
470    timeout: Duration,
471) -> Result<ZmqMessage, ZmqError> {
472    let identity = peer_identity();
473    let mut inbox = bus.open_inbox(identity.clone());
474    // Disconnects the DEALER however the request ends, a caller dropping it included.
475    let _connected = Connected {
476        bus,
477        identity: &identity,
478    };
479    let pick = if local { Pick::All } else { Pick::Nobody };
480    let _ = bus.send(name, frames, Some(&identity), pick);
481    let exchange = async {
482        while let Some(reply) = inbox.recv().await {
483            let (name, headers, payload) = wire::decode(reply)?;
484            if headers.correlation_id() == Some(correlation) {
485                return Ok(ZmqMessage::new(name, headers, payload));
486            }
487        }
488        Err(ZmqError::RequestTimeout)
489    };
490    tokio::time::timeout(timeout, exchange)
491        .await
492        .unwrap_or(Err(ZmqError::RequestTimeout))
493}
494
495/// A DEALER of this service connected at `identity` for one request.
496struct Connected<'a> {
497    bus: &'a Bus,
498    identity: &'a Bytes,
499}
500
501impl Drop for Connected<'_> {
502    fn drop(&mut self) {
503        self.bus.close_inbox(self.identity);
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use futures::FutureExt;
510    use ruststream::HeaderMap;
511
512    use super::*;
513    use crate::endpoint::ZmqEndpoint;
514
515    impl Bus {
516        fn peers(&self) -> usize {
517            self.state().peers.len()
518        }
519    }
520
521    #[tokio::test]
522    async fn an_injected_request_no_responder_takes_leaves_no_peer() {
523        let rpc = ZmqRpc::new(ZmqEndpoint::bind("tcp://127.0.0.1:0"))
524            .connect_in_process()
525            .await
526            .expect("connects in process");
527        TestableBroker::inject(&rpc, OutgoingMessage::new("orders", b"x".as_slice()));
528        let bus = rpc.lifecycle().in_process_bus().expect("in process");
529        assert_eq!(bus.peers(), 0);
530    }
531
532    #[tokio::test]
533    async fn a_request_dropped_while_it_waits_leaves_no_peer() {
534        let bus = Bus::default();
535        let frames = wire::encode_to(
536            "orders",
537            "orders",
538            &HeaderMap::new(),
539            Bytes::from_static(b"x"),
540        )
541        .expect("frames");
542        let pending =
543            request(&bus, false, "orders", frames, "c-1", Duration::from_mins(1)).now_or_never();
544        assert!(pending.is_none(), "the request was still waiting");
545        assert_eq!(bus.peers(), 0);
546    }
547}