ruststream_lapin/
requester.rs1use std::collections::HashMap;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, Mutex, Weak};
6use std::time::Duration;
7
8use futures::StreamExt;
9use lapin::Channel;
10use lapin::options::{BasicConsumeOptions, BasicPublishOptions};
11use lapin::types::{FieldTable, ShortString};
12use ruststream::{OutgoingMessage, PairError, PublishPolicy, Publisher, RequestReply};
13use tokio::sync::{OnceCell, oneshot};
14
15use crate::broker::{AmqpConnection, ConnectedLapinBroker};
16use crate::convert;
17use crate::error::AmqpError;
18use crate::message::LapinMessage;
19use crate::publish_policy::{LapinPublishPolicy, PublishOptions};
20
21const REPLY_TO: &str = "amq.rabbitmq.reply-to";
23
24type Pending = Mutex<HashMap<String, oneshot::Sender<LapinMessage>>>;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
42#[must_use]
43pub struct LapinRequest(PublishOptions);
44
45impl Default for LapinRequest {
46 fn default() -> Self {
47 Self(PublishOptions {
48 persistent: false,
49 ..PublishOptions::default()
50 })
51 }
52}
53
54impl LapinRequest {
55 pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
57 self.0.exchange = exchange.into();
58 self
59 }
60
61 pub fn persistent(mut self, persistent: bool) -> Self {
63 self.0.persistent = persistent;
64 self
65 }
66}
67
68impl PublishPolicy<ConnectedLapinBroker> for LapinRequest {
69 type Live = LapinRequester;
70
71 async fn pair(self, connected: &ConnectedLapinBroker) -> Result<Self::Live, PairError> {
72 Ok(self.bind(connected))
73 }
74}
75
76impl LapinPublishPolicy for LapinRequest {
77 fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live {
78 LapinRequester {
79 conn: Arc::clone(connected.connection()),
80 options: self.0,
81 state: Arc::new(OnceCell::new()),
82 pending: Arc::new(Mutex::new(HashMap::new())),
83 next_id: Arc::new(AtomicU64::new(0)),
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
105pub struct LapinRequester {
106 conn: Arc<AmqpConnection>,
107 options: PublishOptions,
108 state: Arc<OnceCell<ReqState>>,
109 pending: Arc<Pending>,
110 next_id: Arc<AtomicU64>,
111}
112
113#[derive(Debug)]
114struct ReqState {
115 channel: Channel,
116}
117
118impl LapinRequester {
119 async fn state(&self, target: &str) -> Result<&ReqState, AmqpError> {
126 self.state
127 .get_or_try_init(|| async {
128 let channel = self
129 .conn
130 .live_connection(target)?
131 .create_channel()
132 .await
133 .map_err(AmqpError::request)?;
134 let consumer = channel
135 .basic_consume(
136 ShortString::from(REPLY_TO),
137 ShortString::default(),
138 BasicConsumeOptions {
139 no_ack: true,
140 ..BasicConsumeOptions::default()
141 },
142 FieldTable::default(),
143 )
144 .await
145 .map_err(AmqpError::request)?;
146
147 let pending = Arc::downgrade(&self.pending);
150 tokio::spawn(dispatch_replies(consumer, pending));
151
152 Ok(ReqState { channel })
153 })
154 .await
155 }
156}
157
158async fn dispatch_replies(mut consumer: lapin::Consumer, pending: Weak<Pending>) {
159 while let Some(delivery) = consumer.next().await {
160 let Ok(delivery) = delivery else {
161 return;
164 };
165 let Some(pending) = pending.upgrade() else {
166 return;
167 };
168 let correlation_id = delivery
169 .properties
170 .correlation_id()
171 .as_ref()
172 .map(ShortString::as_str);
173 let Some(correlation_id) = correlation_id else {
174 tracing::debug!("dropping direct reply-to delivery without a correlation-id");
175 continue;
176 };
177 let waiter = pending
178 .lock()
179 .expect("pending requests mutex poisoned")
180 .remove(correlation_id);
181 match waiter {
182 Some(tx) => drop(tx.send(LapinMessage::from_delivery_no_ack(delivery))),
184 None => {
185 tracing::debug!(
186 correlation_id,
187 "dropping direct reply-to delivery with no waiter"
188 );
189 }
190 }
191 }
192}
193
194impl Publisher for LapinRequester {
195 type Error = AmqpError;
196
197 async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
208 self.conn.ensure_live(msg.name())?;
209 let state = self.state(msg.name()).await?;
210 let properties = convert::properties_for_publish(msg.headers(), self.options.persistent)?;
211 let _confirm = state
212 .channel
213 .basic_publish(
214 convert::short(&self.options.exchange, "exchange name")?,
215 convert::short(msg.name(), "routing key")?,
216 BasicPublishOptions::default(),
217 msg.payload(),
218 properties,
219 )
220 .await
221 .map_err(AmqpError::publish)?;
222 Ok(())
223 }
224}
225
226impl RequestReply for LapinRequester {
227 type Reply = LapinMessage;
228
229 async fn request(
242 &self,
243 msg: OutgoingMessage<'_>,
244 timeout: Duration,
245 ) -> Result<Self::Reply, Self::Error> {
246 self.conn.ensure_live(msg.name())?;
247 let state = self.state(msg.name()).await?;
248
249 let correlation_id = format!("rs-{}", self.next_id.fetch_add(1, Ordering::Relaxed));
250 let (tx, rx) = oneshot::channel();
251 {
252 let mut pending = self
253 .pending
254 .lock()
255 .expect("pending requests mutex poisoned");
256 pending.insert(correlation_id.clone(), tx);
257 }
258 let cleanup = || {
260 let mut pending = self
261 .pending
262 .lock()
263 .expect("pending requests mutex poisoned");
264 pending.remove(&correlation_id);
265 };
266
267 let properties =
268 match convert::properties_for_publish(msg.headers(), self.options.persistent) {
269 Ok(properties) => properties
270 .with_reply_to(ShortString::from(REPLY_TO))
271 .with_correlation_id(ShortString::from(correlation_id.clone())),
272 Err(err) => {
273 cleanup();
274 return Err(err);
275 }
276 };
277 let exchange = match convert::short(&self.options.exchange, "exchange name") {
278 Ok(exchange) => exchange,
279 Err(err) => {
280 cleanup();
281 return Err(err);
282 }
283 };
284 let routing_key = match convert::short(msg.name(), "routing key") {
285 Ok(routing_key) => routing_key,
286 Err(err) => {
287 cleanup();
288 return Err(err);
289 }
290 };
291
292 let published = state
293 .channel
294 .basic_publish(
295 exchange,
296 routing_key,
297 BasicPublishOptions::default(),
298 msg.payload(),
299 properties,
300 )
301 .await;
302 if let Err(err) = published {
303 cleanup();
304 return Err(AmqpError::publish(err));
305 }
306
307 match tokio::time::timeout(timeout, rx).await {
308 Ok(Ok(reply)) => Ok(reply),
309 Ok(Err(_)) => {
311 cleanup();
312 Err(AmqpError::Request(
313 "the reply consumer stopped before a reply arrived".into(),
314 ))
315 }
316 Err(_) => {
317 cleanup();
318 Err(AmqpError::RequestTimeout(timeout))
319 }
320 }
321 }
322}