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, PairError, PublishPolicy, Publisher, RequestReply};
use tokio::sync::{OnceCell, oneshot};
use crate::broker::{AmqpConnection, ConnectedLapinBroker};
use crate::convert;
use crate::error::AmqpError;
use crate::message::LapinMessage;
use crate::publish_policy::{LapinPublishPolicy, PublishOptions};
const REPLY_TO: &str = "amq.rabbitmq.reply-to";
type Pending = Mutex<HashMap<String, oneshot::Sender<LapinMessage>>>;
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct LapinRequest(PublishOptions);
impl Default for LapinRequest {
fn default() -> Self {
Self(PublishOptions {
persistent: false,
..PublishOptions::default()
})
}
}
impl LapinRequest {
pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
self.0.exchange = exchange.into();
self
}
pub fn persistent(mut self, persistent: bool) -> Self {
self.0.persistent = persistent;
self
}
}
impl PublishPolicy<ConnectedLapinBroker> for LapinRequest {
type Live = LapinRequester;
async fn pair(self, connected: &ConnectedLapinBroker) -> Result<Self::Live, PairError> {
Ok(self.bind(connected))
}
}
impl LapinPublishPolicy for LapinRequest {
fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live {
LapinRequester {
conn: Arc::clone(connected.connection()),
options: self.0,
state: Arc::new(OnceCell::new()),
pending: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(AtomicU64::new(0)),
}
}
}
#[derive(Debug, Clone)]
pub struct LapinRequester {
conn: Arc<AmqpConnection>,
options: PublishOptions,
state: Arc<OnceCell<ReqState>>,
pending: Arc<Pending>,
next_id: Arc<AtomicU64>,
}
#[derive(Debug)]
struct ReqState {
channel: Channel,
}
impl LapinRequester {
async fn state(&self, target: &str) -> Result<&ReqState, AmqpError> {
self.state
.get_or_try_init(|| async {
let channel = self
.conn
.live_connection(target)?
.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> {
self.conn.ensure_live(msg.name())?;
let state = self.state(msg.name()).await?;
let properties = convert::properties_for_publish(msg.headers(), self.options.persistent)?;
let _confirm = state
.channel
.basic_publish(
convert::short(&self.options.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> {
self.conn.ensure_live(msg.name())?;
let state = self.state(msg.name()).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.options.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.options.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))
}
}
}
}