use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;
use futures::StreamExt;
use lapin::Channel;
use lapin::options::{BasicConsumeOptions, BasicPublishOptions};
use lapin::types::{FieldTable, ShortString};
use ruststream::{OutgoingMessage, Publisher, RequestReply};
use tokio::sync::{OnceCell, oneshot};
use crate::broker::SharedConn;
use crate::convert;
use crate::error::AmqpError;
use crate::message::LapinMessage;
const REPLY_TO: &str = "amq.rabbitmq.reply-to";
type Pending = Mutex<HashMap<String, oneshot::Sender<LapinMessage>>>;
#[derive(Debug, Clone)]
pub struct LapinRequester {
conn: SharedConn,
exchange: String,
persistent: bool,
state: Arc<OnceCell<ReqState>>,
pending: Arc<Pending>,
next_id: Arc<AtomicU64>,
}
#[derive(Debug)]
struct ReqState {
channel: Channel,
}
impl LapinRequester {
pub(crate) fn new(conn: SharedConn) -> Self {
Self {
conn,
exchange: String::new(),
persistent: false,
state: Arc::new(OnceCell::new()),
pending: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(AtomicU64::new(0)),
}
}
#[must_use]
pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
self.exchange = exchange.into();
self
}
#[must_use]
pub fn persistent(mut self, persistent: bool) -> Self {
self.persistent = persistent;
self
}
async fn state(&self) -> Result<&ReqState, AmqpError> {
self.state
.get_or_try_init(|| async {
let state = self.conn.get().ok_or(AmqpError::NotConnected)?;
let channel = state
.connection()
.create_channel()
.await
.map_err(AmqpError::request)?;
let consumer = channel
.basic_consume(
ShortString::from(REPLY_TO),
ShortString::default(),
BasicConsumeOptions {
no_ack: true,
..BasicConsumeOptions::default()
},
FieldTable::default(),
)
.await
.map_err(AmqpError::request)?;
let pending = Arc::downgrade(&self.pending);
tokio::spawn(dispatch_replies(consumer, pending));
Ok(ReqState { channel })
})
.await
}
}
async fn dispatch_replies(mut consumer: lapin::Consumer, pending: Weak<Pending>) {
while let Some(delivery) = consumer.next().await {
let Ok(delivery) = delivery else {
return;
};
let Some(pending) = pending.upgrade() else {
return;
};
let correlation_id = delivery
.properties
.correlation_id()
.as_ref()
.map(ShortString::as_str);
let Some(correlation_id) = correlation_id else {
tracing::debug!("dropping direct reply-to delivery without a correlation-id");
continue;
};
let waiter = pending
.lock()
.expect("pending requests mutex poisoned")
.remove(correlation_id);
match waiter {
Some(tx) => drop(tx.send(LapinMessage::from_delivery_no_ack(delivery))),
None => {
tracing::debug!(
correlation_id,
"dropping direct reply-to delivery with no waiter"
);
}
}
}
}
impl Publisher for LapinRequester {
type Error = AmqpError;
async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
let state = self.state().await?;
let properties = convert::properties_for_publish(msg.headers(), self.persistent)?;
let _confirm = state
.channel
.basic_publish(
convert::short(&self.exchange, "exchange name")?,
convert::short(msg.name(), "routing key")?,
BasicPublishOptions::default(),
msg.payload(),
properties,
)
.await
.map_err(AmqpError::publish)?;
Ok(())
}
}
impl RequestReply for LapinRequester {
type Reply = LapinMessage;
async fn request(
&self,
msg: OutgoingMessage<'_>,
timeout: Duration,
) -> Result<Self::Reply, Self::Error> {
let state = self.state().await?;
let correlation_id = format!("rs-{}", self.next_id.fetch_add(1, Ordering::Relaxed));
let (tx, rx) = oneshot::channel();
{
let mut pending = self
.pending
.lock()
.expect("pending requests mutex poisoned");
pending.insert(correlation_id.clone(), tx);
}
let cleanup = || {
let mut pending = self
.pending
.lock()
.expect("pending requests mutex poisoned");
pending.remove(&correlation_id);
};
let properties = match convert::properties_for_publish(msg.headers(), self.persistent) {
Ok(properties) => properties
.with_reply_to(ShortString::from(REPLY_TO))
.with_correlation_id(ShortString::from(correlation_id.clone())),
Err(err) => {
cleanup();
return Err(err);
}
};
let exchange = match convert::short(&self.exchange, "exchange name") {
Ok(exchange) => exchange,
Err(err) => {
cleanup();
return Err(err);
}
};
let routing_key = match convert::short(msg.name(), "routing key") {
Ok(routing_key) => routing_key,
Err(err) => {
cleanup();
return Err(err);
}
};
let published = state
.channel
.basic_publish(
exchange,
routing_key,
BasicPublishOptions::default(),
msg.payload(),
properties,
)
.await;
if let Err(err) = published {
cleanup();
return Err(AmqpError::publish(err));
}
match tokio::time::timeout(timeout, rx).await {
Ok(Ok(reply)) => Ok(reply),
Ok(Err(_)) => {
cleanup();
Err(AmqpError::Request(
"the reply consumer stopped before a reply arrived".into(),
))
}
Err(_) => {
cleanup();
Err(AmqpError::RequestTimeout(timeout))
}
}
}
}