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, Publisher, RequestReply};
13use tokio::sync::{OnceCell, oneshot};
14
15use crate::broker::SharedConn;
16use crate::convert;
17use crate::error::AmqpError;
18use crate::message::LapinMessage;
19
20const REPLY_TO: &str = "amq.rabbitmq.reply-to";
22
23type Pending = Mutex<HashMap<String, oneshot::Sender<LapinMessage>>>;
24
25#[derive(Debug, Clone)]
44pub struct LapinRequester {
45 conn: SharedConn,
46 exchange: String,
47 persistent: bool,
48 state: Arc<OnceCell<ReqState>>,
49 pending: Arc<Pending>,
50 next_id: Arc<AtomicU64>,
51}
52
53#[derive(Debug)]
54struct ReqState {
55 channel: Channel,
56}
57
58impl LapinRequester {
59 pub(crate) fn new(conn: SharedConn) -> Self {
60 Self {
61 conn,
62 exchange: String::new(),
63 persistent: false,
64 state: Arc::new(OnceCell::new()),
65 pending: Arc::new(Mutex::new(HashMap::new())),
66 next_id: Arc::new(AtomicU64::new(0)),
67 }
68 }
69
70 #[must_use]
72 pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
73 self.exchange = exchange.into();
74 self
75 }
76
77 #[must_use]
79 pub fn persistent(mut self, persistent: bool) -> Self {
80 self.persistent = persistent;
81 self
82 }
83
84 async fn state(&self) -> Result<&ReqState, AmqpError> {
89 self.state
90 .get_or_try_init(|| async {
91 let state = self.conn.get().ok_or(AmqpError::NotConnected)?;
92 let channel = state
93 .connection()
94 .create_channel()
95 .await
96 .map_err(AmqpError::request)?;
97 let consumer = channel
98 .basic_consume(
99 ShortString::from(REPLY_TO),
100 ShortString::default(),
101 BasicConsumeOptions {
102 no_ack: true,
103 ..BasicConsumeOptions::default()
104 },
105 FieldTable::default(),
106 )
107 .await
108 .map_err(AmqpError::request)?;
109
110 let pending = Arc::downgrade(&self.pending);
113 tokio::spawn(dispatch_replies(consumer, pending));
114
115 Ok(ReqState { channel })
116 })
117 .await
118 }
119}
120
121async fn dispatch_replies(mut consumer: lapin::Consumer, pending: Weak<Pending>) {
122 while let Some(delivery) = consumer.next().await {
123 let Ok(delivery) = delivery else {
124 return;
127 };
128 let Some(pending) = pending.upgrade() else {
129 return;
130 };
131 let correlation_id = delivery
132 .properties
133 .correlation_id()
134 .as_ref()
135 .map(ShortString::as_str);
136 let Some(correlation_id) = correlation_id else {
137 tracing::debug!("dropping direct reply-to delivery without a correlation-id");
138 continue;
139 };
140 let waiter = pending
141 .lock()
142 .expect("pending requests mutex poisoned")
143 .remove(correlation_id);
144 match waiter {
145 Some(tx) => drop(tx.send(LapinMessage::from_delivery_no_ack(delivery))),
147 None => {
148 tracing::debug!(
149 correlation_id,
150 "dropping direct reply-to delivery with no waiter"
151 );
152 }
153 }
154 }
155}
156
157impl Publisher for LapinRequester {
158 type Error = AmqpError;
159
160 async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
171 let state = self.state().await?;
172 let properties = convert::properties_for_publish(msg.headers(), self.persistent)?;
173 let _confirm = state
174 .channel
175 .basic_publish(
176 convert::short(&self.exchange, "exchange name")?,
177 convert::short(msg.name(), "routing key")?,
178 BasicPublishOptions::default(),
179 msg.payload(),
180 properties,
181 )
182 .await
183 .map_err(AmqpError::publish)?;
184 Ok(())
185 }
186}
187
188impl RequestReply for LapinRequester {
189 type Reply = LapinMessage;
190
191 async fn request(
204 &self,
205 msg: OutgoingMessage<'_>,
206 timeout: Duration,
207 ) -> Result<Self::Reply, Self::Error> {
208 let state = self.state().await?;
209
210 let correlation_id = format!("rs-{}", self.next_id.fetch_add(1, Ordering::Relaxed));
211 let (tx, rx) = oneshot::channel();
212 {
213 let mut pending = self
214 .pending
215 .lock()
216 .expect("pending requests mutex poisoned");
217 pending.insert(correlation_id.clone(), tx);
218 }
219 let cleanup = || {
221 let mut pending = self
222 .pending
223 .lock()
224 .expect("pending requests mutex poisoned");
225 pending.remove(&correlation_id);
226 };
227
228 let properties = match convert::properties_for_publish(msg.headers(), self.persistent) {
229 Ok(properties) => properties
230 .with_reply_to(ShortString::from(REPLY_TO))
231 .with_correlation_id(ShortString::from(correlation_id.clone())),
232 Err(err) => {
233 cleanup();
234 return Err(err);
235 }
236 };
237 let exchange = match convert::short(&self.exchange, "exchange name") {
238 Ok(exchange) => exchange,
239 Err(err) => {
240 cleanup();
241 return Err(err);
242 }
243 };
244 let routing_key = match convert::short(msg.name(), "routing key") {
245 Ok(routing_key) => routing_key,
246 Err(err) => {
247 cleanup();
248 return Err(err);
249 }
250 };
251
252 let published = state
253 .channel
254 .basic_publish(
255 exchange,
256 routing_key,
257 BasicPublishOptions::default(),
258 msg.payload(),
259 properties,
260 )
261 .await;
262 if let Err(err) = published {
263 cleanup();
264 return Err(AmqpError::publish(err));
265 }
266
267 match tokio::time::timeout(timeout, rx).await {
268 Ok(Ok(reply)) => Ok(reply),
269 Ok(Err(_)) => {
271 cleanup();
272 Err(AmqpError::Request(
273 "the reply consumer stopped before a reply arrived".into(),
274 ))
275 }
276 Err(_) => {
277 cleanup();
278 Err(AmqpError::RequestTimeout(timeout))
279 }
280 }
281 }
282}