#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::collections::HashMap;
use std::future::{Future, ready};
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};
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::Bindings;
use ruststream::{Lend, OutgoingMessage, PairError, PublishPolicy, Publisher, RequestReply};
use tokio::sync::oneshot;
use crate::broker::{AmqpConnection, ConnectedLapinBroker, Link};
use crate::channel::{ChannelCell, Holds};
use crate::convert;
use crate::error::AmqpError;
#[cfg(feature = "testing")]
use crate::in_process;
use crate::message::LapinMessage;
use crate::publish_policy::{LapinPublishPolicy, PublishOptions, publish_policy_bindings};
use crate::publish_step::LapinPublishOptions;
pub(crate) 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 {
defaults: LapinPublishOptions {
persistent: Some(false),
..LapinPublishOptions::default()
},
..PublishOptions::default()
})
}
}
impl LapinRequest {
#[cfg(feature = "asyncapi")]
pub(crate) const fn publish_options(&self) -> &PublishOptions {
&self.0
}
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.defaults.persistent = Some(persistent);
self
}
pub fn priority(mut self, priority: u8) -> Self {
self.0.defaults.priority = Some(priority);
self
}
pub fn expiration(mut self, ttl: Duration) -> Self {
self.0.defaults.expiration = Some(ttl);
self
}
}
impl PublishPolicy<ConnectedLapinBroker> for LapinRequest {
type Live = LapinRequester;
fn pair(
self,
connected: &ConnectedLapinBroker,
) -> impl Future<Output = Result<Self::Live, PairError>> {
ready(Ok(self.bind(connected)))
}
publish_policy_bindings!();
}
impl LapinPublishPolicy for LapinRequest {
fn bind(self, connected: &ConnectedLapinBroker) -> Self::Live {
LapinRequester {
link: connected.link().clone(),
options: self.0,
state: Arc::new(ChannelCell::new()),
pending: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(AtomicU64::new(0)),
}
}
}
#[derive(Debug, Clone)]
pub struct LapinRequester {
link: Link,
options: PublishOptions,
state: Arc<ChannelCell<ReqState>>,
pending: Arc<Pending>,
next_id: Arc<AtomicU64>,
}
#[derive(Debug, Clone)]
struct ReqState {
channel: Channel,
}
impl Holds for ReqState {
fn channel(&self) -> &Channel {
&self.channel
}
}
impl LapinRequester {
async fn state(&self, conn: &AmqpConnection, target: &str) -> Result<ReqState, AmqpError> {
self.state
.get(|| async {
let channel = 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);
conn.runtime().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 Payload = Lend;
type Error = AmqpError;
type Options = LapinPublishOptions;
async fn publish(
&self,
msg: OutgoingMessage<'_>,
options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
let conn = match &self.link {
Link::Amqp(conn) => conn,
#[cfg(feature = "testing")]
Link::InProcess(bus) => {
return bus.publish(
&self.options.exchange,
msg.name(),
msg.payload(),
msg.headers(),
&self.options.resolve(options),
);
}
};
conn.ensure_live(msg.name())?;
let state = self.state(conn, msg.name()).await?;
let properties =
convert::properties_for_publish(msg.headers(), &self.options.resolve(options))?;
#[cfg(feature = "testing")]
conn.routes().published(&self.options.exchange, msg.name());
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> {
let conn = match &self.link {
Link::Amqp(conn) => conn,
#[cfg(feature = "testing")]
Link::InProcess(bus) => {
return in_process::request(bus, &self.options, msg, timeout).await;
}
};
conn.ensure_live(msg.name())?;
let state = self.state(conn, 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.resolve(None)) {
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);
}
};
#[cfg(feature = "testing")]
conn.routes().published(&self.options.exchange, msg.name());
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))
}
}
}
}