use std::{
collections::HashMap,
sync::{Arc, Mutex as StdMutex},
time::Duration,
};
use lapin::{
BasicProperties, Channel, Confirmation, Connection,
options::{BasicPublishOptions, ConfirmSelectOptions},
};
use queuey_core::{Envelope, Error, QueueConfig, Result};
use tokio::sync::{Mutex, MutexGuard};
use tracing::{debug, warn};
use crate::{
codec,
error::{RabbitMqError, amqp, short_string},
options::RabbitMqOptions,
topology,
};
#[derive(Clone)]
pub(crate) struct Publisher {
connection: Arc<Connection>,
channel: Arc<Mutex<Channel>>,
declare_channel: Arc<Mutex<Channel>>,
options: Arc<RabbitMqOptions>,
configs: Arc<StdMutex<HashMap<String, QueueConfig>>>,
}
impl std::fmt::Debug for Publisher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Publisher").finish_non_exhaustive()
}
}
impl Publisher {
pub(crate) fn new(
connection: Arc<Connection>,
channel: Channel,
declare_channel: Channel,
options: Arc<RabbitMqOptions>,
) -> Self {
Self {
connection,
channel: Arc::new(Mutex::new(channel)),
declare_channel: Arc::new(Mutex::new(declare_channel)),
options,
configs: Arc::new(StdMutex::new(HashMap::new())),
}
}
pub(crate) fn remember(&self, config: &QueueConfig) {
self.lock_configs()
.insert(config.name.clone(), config.clone());
}
fn config_for(&self, queue: &str) -> Option<QueueConfig> {
self.lock_configs().get(queue).cloned()
}
fn lock_configs(&self) -> std::sync::MutexGuard<'_, HashMap<String, QueueConfig>> {
self.configs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) async fn open_confirm_channel(connection: &Connection) -> Result<Channel> {
let channel = connection.create_channel().await.map_err(amqp)?;
channel
.confirm_select(ConfirmSelectOptions::default())
.await
.map_err(amqp)?;
Ok(channel)
}
pub(crate) fn options(&self) -> &RabbitMqOptions {
&self.options
}
pub(crate) fn dead_queue(&self, queue: &str) -> String {
topology::dead_queue_name(queue, &self.options.dead_suffix)
}
pub(crate) fn deferred_queue(&self, queue: &str, ttl_ms: u32) -> String {
topology::deferred_queue_name(queue, &self.options.deferred_suffix, ttl_ms)
}
async fn with_channel(&self, context: &str) -> Result<MutexGuard<'_, Channel>> {
let mut channel = self.channel.lock().await;
if !channel.status().connected() {
warn!(
queue = context,
channel = channel.id(),
"publishing channel is closed; opening a replacement in confirm mode"
);
*channel = Self::open_confirm_channel(&self.connection).await?;
}
Ok(channel)
}
async fn with_declare_channel(&self, context: &str) -> Result<MutexGuard<'_, Channel>> {
let mut channel = self.declare_channel.lock().await;
if !channel.status().connected() {
warn!(
queue = context,
channel = channel.id(),
"declaration channel is closed; opening a replacement"
);
*channel = self.connection.create_channel().await.map_err(amqp)?;
}
Ok(channel)
}
pub(crate) async fn publish_confirmed(
&self,
queue: &str,
payload: &[u8],
properties: BasicProperties,
) -> Result<()> {
let routing_key = short_string(queue)?;
let confirm = {
let channel = self.with_channel(queue).await?;
channel
.basic_publish(
"".into(),
routing_key,
BasicPublishOptions {
mandatory: true,
immediate: false,
},
payload,
properties,
)
.await
.map_err(amqp)?
};
confirmation_to_result(confirm.await.map_err(amqp)?, queue)?;
debug!(queue, bytes = payload.len(), "publish confirmed");
Ok(())
}
pub(crate) async fn publish_envelope(&self, envelope: &Envelope) -> Result<()> {
let payload = envelope.to_bytes()?;
let properties = codec::props_for(envelope);
self.publish_confirmed(&envelope.queue, &payload, properties)
.await
}
pub(crate) async fn publish_held(
&self,
envelope: &Envelope,
delay: Duration,
hold: Hold,
) -> Result<()> {
let Some(config) = self.config_for(&envelope.queue) else {
return Err(Error::UnknownQueue(envelope.queue.clone()));
};
let granularity = hold.granularity(&self.options);
let Some(ttl_ms) = topology::deferred_ttl_ms(delay, granularity) else {
return Err(RabbitMqError::DelayTooLong {
requested: delay,
max: Duration::from_millis(u64::from(topology::MAX_DEFERRAL_MS)),
}
.into_core());
};
let hold_queue = self.deferred_queue(&envelope.queue, ttl_ms);
let hold_name = short_string(&hold_queue)?;
{
let channel = self.with_declare_channel(&hold_queue).await?;
channel
.queue_declare(
hold_name,
topology::declare_options(config.durable),
topology::deferred_queue_args(&config, ttl_ms),
)
.await
.map_err(amqp)?;
}
debug!(
queue = %envelope.queue,
hold = %hold_queue,
ttl_ms,
attempt = envelope.attempt,
deferrals = envelope.deferrals,
priority = envelope.priority,
"{}",
hold.log_message()
);
let payload = envelope.to_bytes()?;
self.publish_confirmed(&hold_queue, &payload, codec::props_for(envelope))
.await
}
pub(crate) async fn publish_dead_letter(
&self,
envelope: &Envelope,
reason: &str,
) -> Result<()> {
let payload = envelope.to_bytes()?;
let properties = codec::dead_letter_props(envelope, reason);
let target = self.dead_queue(&envelope.queue);
self.publish_confirmed(&target, &payload, properties).await
}
pub(crate) async fn publish_malformed(
&self,
queue: &str,
payload: &[u8],
reason: &str,
) -> Result<()> {
let properties = codec::malformed_props(queue, reason);
let target = self.dead_queue(queue);
self.publish_confirmed(&target, payload, properties).await
}
pub(crate) async fn close(&self) -> Result<()> {
let publishing = {
let channel = self.channel.lock().await;
close_channel(&channel).await
};
let declaring = {
let channel = self.declare_channel.lock().await;
close_channel(&channel).await
};
publishing.and(declaring)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Hold {
Retry,
Deferral,
}
impl Hold {
pub(crate) fn granularity(self, options: &RabbitMqOptions) -> Duration {
match self {
Self::Retry => options.retry_granularity,
Self::Deferral => options.deferred_granularity,
}
}
fn log_message(self) -> &'static str {
match self {
Self::Retry => "holding job for a delayed redelivery",
Self::Deferral => "deferring job",
}
}
}
async fn close_channel(channel: &Channel) -> Result<()> {
if !channel.status().connected() {
return Ok(());
}
match channel.close(200, "OK".into()).await {
Ok(()) => Ok(()),
Err(error) if crate::backend::is_benign_close_error(&error) => {
debug!(%error, channel = channel.id(), "channel already closing; treating close as successful");
Ok(())
}
Err(error) => Err(amqp(error)),
}
}
fn confirmation_to_result(confirmation: Confirmation, routing_key: &str) -> Result<()> {
match confirmation {
Confirmation::Ack(None) => Ok(()),
Confirmation::Ack(Some(returned)) | Confirmation::Nack(Some(returned)) => {
Err(RabbitMqError::Returned {
reply_code: returned.reply_code,
reply_text: returned.reply_text.to_string(),
routing_key: routing_key.to_owned(),
}
.into_core())
}
Confirmation::Nack(None) => Err(RabbitMqError::Nacked {
queue: routing_key.to_owned(),
}
.into_core()),
Confirmation::NotRequested => Err(RabbitMqError::ConfirmsNotEnabled {
queue: routing_key.to_owned(),
}
.into_core()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_hold_kind_reads_its_own_granularity() {
let options = RabbitMqOptions::default()
.retry_granularity(Duration::from_secs(10))
.deferred_granularity(Duration::from_millis(250));
assert_eq!(Hold::Retry.granularity(&options), Duration::from_secs(10));
assert_eq!(
Hold::Deferral.granularity(&options),
Duration::from_millis(250)
);
}
#[test]
fn a_plain_ack_is_success() {
assert!(confirmation_to_result(Confirmation::Ack(None), "emails").is_ok());
}
#[test]
fn a_nack_is_an_error_naming_the_queue() {
let err = confirmation_to_result(Confirmation::Nack(None), "emails.deferred.1000")
.expect_err("nack must not be success");
let text = err.to_string();
assert!(text.contains("nacked"), "{text}");
assert!(text.contains("emails.deferred.1000"), "{text}");
}
#[test]
fn not_requested_is_an_error_rather_than_a_silent_success() {
let err = confirmation_to_result(Confirmation::NotRequested, "emails.dead")
.expect_err("an unconfirmed publish must not be success");
let text = err.to_string();
assert!(
text.contains("publisher confirms are not enabled"),
"{text}"
);
assert!(text.contains("emails.dead"), "{text}");
}
#[test]
fn every_confirmation_variant_is_classified() {
for confirmation in [
Confirmation::Ack(None),
Confirmation::Nack(None),
Confirmation::NotRequested,
] {
let expected_ok = confirmation == Confirmation::Ack(None);
assert_eq!(
confirmation_to_result(confirmation, "q").is_ok(),
expected_ok
);
}
}
}