Skip to main content

ruststream_lapin/testing/
publisher.rs

1//! The in-process publish pair: the [`LapinTestPublish`] policy and its live
2//! [`LapinTestPublisher`].
3
4use std::sync::{Arc, Mutex};
5
6use bytes::Bytes;
7use ruststream::{
8    Headers, OutgoingMessage, OwnedTransactions, PairError, PublishPolicy, Publisher, Transaction,
9    TransactionalPublisher,
10};
11use tracing::warn;
12
13use super::broker::{ConnectedLapinTestBroker, TestBrokerState};
14use crate::error::AmqpError;
15
16type Buffered = (String, Bytes, Headers);
17
18/// The in-process publish policy, mirroring [`LapinPublish`](crate::LapinPublish) on the real
19/// broker.
20///
21/// The router matches queue names exactly, so exchange and persistence carry no meaning here and
22/// the policy is a unit marker.
23///
24/// # Examples
25///
26/// ```
27/// use ruststream_lapin::testing::LapinTestPublish;
28///
29/// let policy = LapinTestPublish;
30/// # let _ = policy;
31/// ```
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33#[must_use]
34pub struct LapinTestPublish;
35
36impl LapinTestPublish {
37    /// Pairs the policy with the connected test broker.
38    #[must_use]
39    pub fn bind(self, connected: &ConnectedLapinTestBroker) -> LapinTestPublisher {
40        LapinTestPublisher {
41            state: connected.state(),
42            txn: Arc::new(Mutex::new(None)),
43        }
44    }
45}
46
47impl PublishPolicy<ConnectedLapinTestBroker> for LapinTestPublish {
48    type Live = LapinTestPublisher;
49
50    async fn pair(self, connected: &ConnectedLapinTestBroker) -> Result<Self::Live, PairError> {
51        Ok(self.bind(connected))
52    }
53}
54
55/// The live publisher into the in-process router.
56///
57/// Mirrors [`ConfirmsPublisher`](crate::ConfirmsPublisher) transaction semantics: publishes
58/// buffer between `begin_transaction` and `commit`, `abort` discards them, and a call with no
59/// open transaction errors. Clones share the transaction buffer. Like the real publishers it
60/// aliases the transport and may outlive it: after the broker shuts down every publish reports
61/// [`AmqpError::Closed`].
62#[derive(Debug, Clone)]
63pub struct LapinTestPublisher {
64    state: Arc<TestBrokerState>,
65    txn: Arc<Mutex<Option<Vec<Buffered>>>>,
66}
67
68impl LapinTestPublisher {
69    fn route(&self, queue: &str, payload: &Bytes, headers: &Headers) {
70        self.state
71            .router
72            .publish(queue, payload, headers, self.state.coordinator().as_ref());
73    }
74}
75
76impl Publisher for LapinTestPublisher {
77    type Error = AmqpError;
78
79    /// Routes `msg` to subscribers of the queue named by `msg.name()`.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`AmqpError::InvalidOptions`] when the routing key is empty and
84    /// [`AmqpError::Closed`] once the transport has shut down.
85    async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
86        if msg.name().is_empty() {
87            return Err(AmqpError::InvalidOptions(
88                "routing key must not be empty; on the default exchange it names the target queue"
89                    .to_owned(),
90            ));
91        }
92        self.state.ensure_live(msg.name())?;
93        {
94            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
95            if let Some(buffer) = txn.as_mut() {
96                buffer.push((
97                    msg.name().to_owned(),
98                    Bytes::copy_from_slice(msg.payload()),
99                    msg.headers().clone(),
100                ));
101                return Ok(());
102            }
103        }
104        self.route(
105            msg.name(),
106            &Bytes::copy_from_slice(msg.payload()),
107            msg.headers(),
108        );
109        Ok(())
110    }
111}
112
113impl TransactionalPublisher for LapinTestPublisher {
114    /// Opens the buffering transaction.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`AmqpError::Transaction`] when a transaction is already open; the open
119    /// transaction is left untouched.
120    async fn begin_transaction(&self) -> Result<(), Self::Error> {
121        let already_open = {
122            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
123            let open = txn.is_some();
124            if !open {
125                *txn = Some(Vec::new());
126            }
127            open
128        };
129        if already_open {
130            return Err(AmqpError::Transaction(
131                "a transaction is already open on this test publisher; commit or abort it before \
132                 beginning another"
133                    .to_owned(),
134            ));
135        }
136        Ok(())
137    }
138
139    /// Replays the buffered publishes in order.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`AmqpError::Transaction`] when no transaction is open and [`AmqpError::Closed`]
144    /// once the transport has shut down.
145    async fn commit(&self) -> Result<(), Self::Error> {
146        let buffered = {
147            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
148            txn.take()
149        };
150        let Some(buffered) = buffered else {
151            return Err(AmqpError::Transaction(
152                "commit with no open transaction on this test publisher".to_owned(),
153            ));
154        };
155        for (queue, payload, headers) in buffered {
156            self.state.ensure_live(&queue)?;
157            self.route(&queue, &payload, &headers);
158        }
159        Ok(())
160    }
161
162    /// Discards the buffered publishes.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`AmqpError::Transaction`] when no transaction is open.
167    async fn abort(&self) -> Result<(), Self::Error> {
168        let discarded = self
169            .txn
170            .lock()
171            .expect("transaction buffer mutex poisoned")
172            .take();
173        if discarded.is_none() {
174            return Err(AmqpError::Transaction(
175                "abort with no open transaction on this test publisher".to_owned(),
176            ));
177        }
178        Ok(())
179    }
180}
181
182/// Owned transactions, mirroring [`ConfirmsPublisher`](crate::ConfirmsPublisher): every call
183/// opens an independent buffer-owning [`LapinTestTransaction`], so any number can be open at
184/// once and the publisher keeps routing directly meanwhile.
185impl OwnedTransactions for LapinTestPublisher {
186    type Transaction = LapinTestTransaction;
187
188    /// Opens a transaction owned by the returned value.
189    ///
190    /// # Errors
191    ///
192    /// Never fails: opening allocates a buffer and never touches the router.
193    async fn transaction(&self) -> Result<Self::Transaction, Self::Error> {
194        Ok(LapinTestTransaction {
195            publisher: self.clone(),
196            buffered: Vec::new(),
197            settled: false,
198        })
199    }
200}
201
202/// An owned in-process transaction, opened by
203/// [`transaction`](OwnedTransactions::transaction) on a [`LapinTestPublisher`].
204///
205/// A private buffer routed in publish order on commit and discarded on abort, mirroring
206/// [`ConfirmsTransaction`](crate::ConfirmsTransaction).
207///
208/// # Examples
209///
210/// ```
211/// use ruststream::{Broker, OutgoingMessage, OwnedTransactions, Transaction};
212/// use ruststream_lapin::testing::{LapinTestBroker, LapinTestPublish};
213///
214/// # async fn demo() -> Result<(), ruststream_lapin::AmqpError> {
215/// let broker = LapinTestBroker::new().connect().await?;
216/// let mut txn = broker.publisher(LapinTestPublish).transaction().await?;
217/// txn.publish(OutgoingMessage::new("orders", b"{}".as_slice())).await?;
218/// txn.commit().await?;
219/// # Ok(())
220/// # }
221/// ```
222#[must_use = "a transaction does nothing until settled with commit() or abort()"]
223pub struct LapinTestTransaction {
224    publisher: LapinTestPublisher,
225    buffered: Vec<Buffered>,
226    settled: bool,
227}
228
229impl std::fmt::Debug for LapinTestTransaction {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        f.debug_struct("LapinTestTransaction")
232            .field("buffered", &self.buffered.len())
233            .field("settled", &self.settled)
234            .finish_non_exhaustive()
235    }
236}
237
238impl Drop for LapinTestTransaction {
239    fn drop(&mut self) {
240        // Same contract as the live transaction: a drop can only discard, and the warning marks
241        // that as an abort the caller never wrote.
242        if !self.settled {
243            warn!(
244                target: "ruststream_lapin",
245                buffered = self.buffered.len(),
246                "owned transaction dropped without commit or abort; its buffered messages are \
247                 discarded"
248            );
249        }
250    }
251}
252
253impl Transaction for LapinTestTransaction {
254    type Error = AmqpError;
255
256    /// Buffers `msg` in this transaction; nothing reaches the router before
257    /// [`commit`](Self::commit).
258    ///
259    /// # Errors
260    ///
261    /// Returns [`AmqpError::InvalidOptions`] when the routing key is empty, the one check the
262    /// live publisher also makes before the broker would.
263    async fn publish(&mut self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
264        if msg.name().is_empty() {
265            return Err(AmqpError::InvalidOptions(
266                "routing key must not be empty; on the default exchange it names the target queue"
267                    .to_owned(),
268            ));
269        }
270        self.buffered.push((
271            msg.name().to_owned(),
272            Bytes::copy_from_slice(msg.payload()),
273            msg.headers().clone(),
274        ));
275        Ok(())
276    }
277
278    /// Routes the buffered messages in order.
279    ///
280    /// # Errors
281    ///
282    /// Returns [`AmqpError::Closed`] once the transport has shut down; the transaction is
283    /// consumed either way.
284    async fn commit(mut self) -> Result<(), Self::Error> {
285        // Settled before the flush, like the live transaction: a failed commit has still
286        // consumed the value.
287        self.settled = true;
288        for (queue, payload, headers) in &self.buffered {
289            self.publisher.state.ensure_live(queue)?;
290            self.publisher.route(queue, payload, headers);
291        }
292        Ok(())
293    }
294
295    /// Discards the buffered messages.
296    ///
297    /// # Errors
298    ///
299    /// Never fails.
300    async fn abort(mut self) -> Result<(), Self::Error> {
301        self.settled = true;
302        Ok(())
303    }
304}