use futures::StreamExt;
use lapin::{options::*, types::FieldTable, BasicProperties, Channel, Connection, ConnectionProperties};
use serde::{de::DeserializeOwned, Serialize};
#[derive(Debug, Clone)]
pub struct PublishOptions {
pub persistent: bool,
pub prefetch_count: u16,
pub use_retry_queue: bool,
pub use_dead_letter: bool,
pub max_retries: usize,
pub retry_interval_ms: u64,
pub processing_timeout_ms: Option<u64>,
}
impl Default for PublishOptions {
fn default() -> Self {
Self {
persistent: true,
prefetch_count: 16,
use_retry_queue: false,
use_dead_letter: false,
max_retries: 5,
retry_interval_ms: 30_000,
processing_timeout_ms: None,
}
}
}
pub struct QueueConsumer<T> {
channel: Channel,
queue_name: String,
options: PublishOptions,
_marker: std::marker::PhantomData<T>,
}
impl<T> QueueConsumer<T>
where
T: DeserializeOwned + Send + Sync + 'static,
{
pub async fn connect(addr: &str, queue_name: &str, options: PublishOptions) -> anyhow::Result<Self> {
let conn = Connection::connect(addr, ConnectionProperties::default()).await?;
let channel = conn.create_channel().await?;
channel.basic_qos(options.prefetch_count, BasicQosOptions::default()).await?;
Ok(Self { channel, queue_name: queue_name.to_string(), options, _marker: std::marker::PhantomData })
}
pub async fn declare(&self) -> anyhow::Result<()> {
let args = FieldTable::default();
if self.options.use_retry_queue {
}
self.channel
.queue_declare(self.queue_name.clone().into(), QueueDeclareOptions { durable: true, ..Default::default() }, args)
.await?;
if self.options.use_retry_queue {
let retry = format!("{}.retry", self.queue_name);
let mut retry_args = FieldTable::default();
retry_args.insert("x-dead-letter-exchange".into(), lapin::types::AMQPValue::LongString("".into()));
retry_args.insert("x-dead-letter-routing-key".into(), lapin::types::AMQPValue::LongString(self.queue_name.clone().into()));
retry_args.insert("x-message-ttl".into(), lapin::types::AMQPValue::LongLongInt(self.options.retry_interval_ms as i64));
self.channel.queue_declare(retry.into(), QueueDeclareOptions { durable: true, ..Default::default() }, retry_args).await?;
}
if self.options.use_dead_letter {
let dlq = format!("{}.dlq", self.queue_name);
self.channel.queue_declare(dlq.into(), QueueDeclareOptions { durable: true, ..Default::default() }, FieldTable::default()).await?;
}
Ok(())
}
pub async fn consume<F, Fut>(&self, mut handler: F) -> anyhow::Result<()>
where
F: FnMut(T, bool) -> Fut + Send + 'static,
Fut: Future<Output = anyhow::Result<()>> + Send,
{
let mut consumer = self.channel.basic_consume(self.queue_name.clone().into(), format!("consumer-{}", uuid::Uuid::new_v4()).into(), BasicConsumeOptions::default(), FieldTable::default()).await?;
while let Some(delivery) = consumer.next().await {
if let Ok(delivery) = delivery {
let data: Result<T, _> = serde_json::from_slice(&delivery.data);
match data {
Ok(msg) => {
let redelivered = delivery.redelivered;
let res = if let Some(timeout) = self.options.processing_timeout_ms {
tokio::time::timeout(std::time::Duration::from_millis(timeout), handler(msg, redelivered)).await
} else {
Ok(handler(msg, redelivered).await)
};
match res {
Ok(Ok(_)) => { let _ = delivery.ack(BasicAckOptions::default()).await; }
_ => { let _ = self.handle_nack(&delivery).await; }
}
}
Err(_) => { let _ = self.handle_nack(&delivery).await; }
}
}
}
Ok(())
}
async fn handle_nack(&self, delivery: &lapin::message::Delivery) -> anyhow::Result<()> {
let retry_count: i64 = delivery.properties.headers().as_ref()
.and_then(|h| h.inner().get("x-retry-count"))
.and_then(|v| match v { lapin::types::AMQPValue::LongLongInt(i) => Some(*i), _ => None })
.unwrap_or(0);
if self.options.use_retry_queue && (retry_count as usize) < self.options.max_retries {
let mut headers = FieldTable::default();
headers.insert("x-retry-count".into(), lapin::types::AMQPValue::LongLongInt(retry_count + 1));
let props = BasicProperties::default().with_headers(headers).with_delivery_mode(2);
self.channel.basic_publish("".into(), format!("{}.retry", self.queue_name).into(), BasicPublishOptions::default(), &delivery.data, props).await?;
delivery.ack(BasicAckOptions::default()).await?;
} else if self.options.use_dead_letter {
let props = BasicProperties::default().with_delivery_mode(2);
self.channel.basic_publish("".into(), format!("{}.dlq", self.queue_name).into(), BasicPublishOptions::default(), &delivery.data, props).await?;
delivery.ack(BasicAckOptions::default()).await?;
} else {
delivery.nack(BasicNackOptions { requeue: false, multiple: false }).await?;
}
Ok(())
}
}
pub struct MessageProducer {
channel: Channel,
}
impl MessageProducer {
pub async fn new(addr: &str) -> anyhow::Result<Self> {
let conn = Connection::connect(addr, ConnectionProperties::default()).await?;
Ok(Self { channel: conn.create_channel().await? })
}
pub async fn publish<T: Serialize>(&self, routing_key: &str, payload: &T) -> anyhow::Result<()> {
let body = serde_json::to_vec(payload)?;
self.channel.basic_publish("".into(), routing_key.to_string().into(), BasicPublishOptions::default(), &body, BasicProperties::default().with_delivery_mode(2)).await?;
Ok(())
}
}