ruststream_lapin/testing/
publisher.rs1use 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33#[must_use]
34pub struct LapinTestPublish;
35
36impl LapinTestPublish {
37 #[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#[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 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 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 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 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
182impl OwnedTransactions for LapinTestPublisher {
186 type Transaction = LapinTestTransaction;
187
188 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#[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 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 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 async fn commit(mut self) -> Result<(), Self::Error> {
285 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 async fn abort(mut self) -> Result<(), Self::Error> {
301 self.settled = true;
302 Ok(())
303 }
304}