use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use lapin::message::Delivery;
use lapin::options::{BasicAckOptions, BasicRejectOptions};
use lapin::types::ShortString;
use lapin::{Acker, BasicProperties};
use ruststream::{AckError, HeaderMap, IncomingMessage, Partitioned};
use crate::broker::AmqpConnection;
use crate::convert;
use crate::delay::DelayContext;
#[cfg(feature = "testing")]
use crate::in_process::{BusDelivery, Settlement};
pub const PARTITION_KEY_HEADER: &str = "amqp-partition-key";
pub(crate) const DELIVERY_COUNT_HEADER: &str = "x-delivery-count";
#[derive(Debug)]
pub struct LapinMessage {
payload: Bytes,
headers: HeaderMap,
exchange: String,
routing_key: String,
redelivered: bool,
delivery_tag: u64,
returns: Option<u64>,
acker: Option<Acker>,
connection: Option<Arc<AmqpConnection>>,
delay: Option<DelayContext>,
#[cfg(feature = "testing")]
in_process: Option<Box<Settlement>>,
}
impl LapinMessage {
pub(crate) fn from_delivery(
delivery: Delivery,
connection: Option<Arc<AmqpConnection>>,
delay: Option<DelayContext>,
) -> Self {
let headers = convert::headers_from_properties(&delivery.properties);
Self {
payload: Bytes::from(delivery.data),
headers,
exchange: delivery.exchange.to_string(),
routing_key: delivery.routing_key.to_string(),
redelivered: delivery.redelivered,
delivery_tag: delivery.delivery_tag,
returns: returns_of(&delivery.properties),
acker: Some(delivery.acker),
connection,
delay,
#[cfg(feature = "testing")]
in_process: None,
}
}
#[cfg(feature = "testing")]
pub(crate) fn in_process(delivery: BusDelivery, delivery_tag: u64, settle: Settlement) -> Self {
Self {
payload: delivery.payload,
headers: delivery.headers,
exchange: delivery.exchange,
routing_key: delivery.routing_key,
redelivered: delivery.redelivered,
delivery_tag,
returns: delivery.returns,
acker: None,
connection: None,
delay: None,
in_process: Some(Box::new(settle)),
}
}
#[cfg(feature = "testing")]
fn into_bus_delivery(self) -> BusDelivery {
BusDelivery {
payload: self.payload,
headers: self.headers,
exchange: self.exchange,
routing_key: self.routing_key,
redelivered: self.redelivered,
returns: self.returns,
}
}
pub(crate) fn from_delivery_no_ack(delivery: Delivery) -> Self {
let mut msg = Self::from_delivery(delivery, None, None);
msg.acker = None;
msg
}
#[must_use]
pub fn exchange(&self) -> &str {
&self.exchange
}
#[must_use]
pub fn routing_key(&self) -> &str {
&self.routing_key
}
#[must_use]
pub fn redelivered(&self) -> bool {
self.redelivered
}
#[must_use]
pub fn delivery_tag(&self) -> u64 {
self.delivery_tag
}
fn refuse_after_shutdown(&self, what: &str) -> Result<(), AckError> {
if self
.connection
.as_ref()
.is_some_and(|connection| connection.is_closed())
{
return Err(AckError::Broker(
format!("{what} was not sent: the broker has shut down").into(),
));
}
Ok(())
}
async fn settle<F, Fut>(mut self, op: F, what: &'static str) -> Result<(), AckError>
where
F: FnOnce(Acker) -> Fut,
Fut: Future<Output = lapin::Result<bool>>,
{
let Some(acker) = self.acker.take() else {
return Ok(());
};
self.refuse_after_shutdown(what)?;
match op(acker).await {
Ok(true) => Ok(()),
Ok(false) => Err(AckError::Broker(
format!("{what} was not sent: the delivery channel is closed or errored").into(),
)),
Err(err) => Err(AckError::Broker(Box::new(err))),
}
}
}
impl IncomingMessage for LapinMessage {
fn payload(&self) -> &[u8] {
&self.payload
}
fn headers(&self) -> &HeaderMap {
&self.headers
}
async fn ack(
#[cfg_attr(not(feature = "testing"), allow(unused_mut))] mut self,
) -> Result<(), AckError> {
#[cfg(feature = "testing")]
if let Some(settle) = self.in_process.take() {
return settle.ack();
}
self.settle(
|acker| async move { acker.ack(BasicAckOptions::default()).await },
"basic.ack",
)
.await
}
async fn nack(
#[cfg_attr(not(feature = "testing"), allow(unused_mut))] mut self,
requeue: bool,
) -> Result<(), AckError> {
#[cfg(feature = "testing")]
if let Some(settle) = self.in_process.take() {
return settle.reject(self.into_bus_delivery(), requeue);
}
self.settle(
|acker| async move { acker.reject(BasicRejectOptions { requeue }).await },
"basic.reject",
)
.await
}
fn partition_key(&self) -> Option<&[u8]> {
self.headers.get(PARTITION_KEY_HEADER)
}
fn redelivery_count(&self) -> Option<u64> {
self.returns.map(|returns| returns.saturating_add(1))
}
fn supports_nack_after(&self) -> bool {
#[cfg(feature = "testing")]
if let Some(settle) = &self.in_process {
return settle.delays();
}
self.delay.is_some()
}
async fn nack_after(mut self, delay: Duration) -> Result<(), AckError> {
#[cfg(feature = "testing")]
if let Some(settle) = self.in_process.take() {
return settle.nack_after(self.into_bus_delivery(), delay);
}
let Some(context) = self.delay.take() else {
return Err(AckError::Unsupported);
};
self.refuse_after_shutdown("basic.ack")?;
context
.republish(&self.payload, &self.headers, delay)
.await
.map_err(|err| AckError::Broker(Box::new(err)))?;
self.settle(
|acker| async move { acker.ack(BasicAckOptions::default()).await },
"basic.ack",
)
.await
}
}
fn returns_of(properties: &BasicProperties) -> Option<u64> {
properties
.headers()
.as_ref()?
.inner()
.get(&ShortString::from(DELIVERY_COUNT_HEADER))
.and_then(convert::counter)
}
impl Partitioned for LapinMessage {
fn partition_key(&self) -> Option<&[u8]> {
self.headers.get(PARTITION_KEY_HEADER)
}
}
#[cfg(test)]
mod tests {
use lapin::BasicProperties;
use lapin::types::{AMQPValue, FieldArray, FieldTable, ShortString};
use super::{DELIVERY_COUNT_HEADER, returns_of};
fn delivered(fields: &[(&str, AMQPValue)]) -> BasicProperties {
let mut table = FieldTable::default();
for (name, value) in fields {
table.insert(ShortString::from(*name), value.clone());
}
BasicProperties::default().with_headers(table)
}
#[test]
fn the_queues_counter_is_the_count() {
let properties = delivered(&[(DELIVERY_COUNT_HEADER, AMQPValue::LongLongInt(4))]);
assert_eq!(returns_of(&properties), Some(4));
}
#[test]
fn a_dead_lettered_delivery_reports_no_count_of_its_own() {
let mut entry = FieldTable::default();
entry.insert(
ShortString::from("reason"),
AMQPValue::LongString("rejected".into()),
);
entry.insert(ShortString::from("count"), AMQPValue::LongLongInt(2));
let properties = delivered(&[(
"x-death",
AMQPValue::FieldArray(FieldArray::from(vec![AMQPValue::FieldTable(entry)])),
)]);
assert_eq!(returns_of(&properties), None);
}
#[test]
fn a_delivery_with_no_counter_reports_none() {
assert_eq!(returns_of(&BasicProperties::default()), None);
assert_eq!(returns_of(&delivered(&[])), None);
}
}