use std::{
collections::HashMap,
sync::{
Arc, Mutex as StdMutex, RwLock,
atomic::{AtomicBool, AtomicU64, Ordering},
},
time::Duration,
};
use lapin::{Channel, Connection};
use queuey_core::{QueueConfig, Result};
use tokio::sync::{Mutex, Notify};
use tracing::{debug, error, info, warn};
use crate::{
error::{RabbitMqError, amqp, short_string},
options::RabbitMqOptions,
reconnect::{Attempt, Rebuilding, ReconnectPolicy},
topology,
};
pub(crate) const REPLY_SUCCESS: u16 = 200;
pub(crate) struct ConnectionHandle {
uri: String,
options: Arc<RabbitMqOptions>,
current: RwLock<Arc<Connection>>,
generation: AtomicU64,
reconnecting: Mutex<()>,
closing: AtomicBool,
closing_notify: Notify,
configs: StdMutex<HashMap<String, QueueConfig>>,
}
impl std::fmt::Debug for ConnectionHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConnectionHandle")
.field("generation", &self.generation.load(Ordering::Relaxed))
.field("closing", &self.is_closing())
.field("connected", &self.is_connected())
.finish_non_exhaustive()
}
}
impl ConnectionHandle {
pub(crate) async fn connect(uri: &str, options: Arc<RabbitMqOptions>) -> Result<Arc<Self>> {
let connection = Connection::connect(uri, options.connection_properties.clone())
.await
.map_err(amqp)?;
Ok(Arc::new(Self {
uri: uri.to_owned(),
options,
current: RwLock::new(Arc::new(connection)),
generation: AtomicU64::new(1),
reconnecting: Mutex::new(()),
closing: AtomicBool::new(false),
closing_notify: Notify::new(),
configs: StdMutex::new(HashMap::new()),
}))
}
fn peek(&self) -> Arc<Connection> {
Arc::clone(
&self
.current
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner),
)
}
pub(crate) fn is_connected(&self) -> bool {
!self.is_closing() && self.peek().status().connected()
}
pub(crate) fn is_closing(&self) -> bool {
self.closing.load(Ordering::Acquire)
}
pub(crate) fn generation(&self) -> u64 {
self.generation.load(Ordering::Acquire)
}
pub(crate) fn reconnects(&self) -> bool {
self.options.reconnect.is_some()
}
pub(crate) fn policy(&self) -> Option<Arc<dyn ReconnectPolicy>> {
self.options.reconnect.clone()
}
pub(crate) fn remember(&self, config: &QueueConfig) {
self.lock_configs()
.insert(config.name.clone(), config.clone());
}
pub(crate) 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 ensure_connected(&self) -> Result<Arc<Connection>> {
if let Some(connection) = self.live()? {
return Ok(connection);
}
let _guard = self.reconnecting.lock().await;
if let Some(connection) = self.live()? {
return Ok(connection);
}
let Some(policy) = self.options.reconnect.clone() else {
return Err(RabbitMqError::ConnectionLost.into_core());
};
let mut failures: u32 = 0;
let mut last: Option<lapin::Error> = None;
loop {
let attempt = match last.as_ref() {
Some(error) => Attempt::after(Rebuilding::Connection, failures, error),
None => Attempt::first(Rebuilding::Connection),
};
let Some(delay) = policy.next_delay(attempt) else {
error!(
attempts = failures,
"giving up on reconnecting to rabbitmq; the policy declined another attempt"
);
return Err(RabbitMqError::ReconnectExhausted {
attempts: failures,
source: last.map(Box::new),
}
.into_core());
};
if !delay.is_zero() {
debug!(
?delay,
failures, "waiting before the next reconnect attempt"
);
self.sleep_unless_closing(delay).await?;
}
self.check_open()?;
match Connection::connect(&self.uri, self.options.connection_properties.clone()).await {
Ok(connection) => {
let connection = Arc::new(connection);
if self.is_closing() {
close_quietly(&connection).await;
return Err(RabbitMqError::Closed.into_core());
}
let generation = self.install(Arc::clone(&connection));
info!(
generation,
attempts = failures + 1,
"reconnected to rabbitmq"
);
self.redeclare(&connection).await;
return Ok(connection);
}
Err(error) => {
failures += 1;
warn!(%error, failures, "reconnecting to rabbitmq failed");
last = Some(error);
}
}
}
}
fn live(&self) -> Result<Option<Arc<Connection>>> {
self.check_open()?;
let connection = self.peek();
Ok(connection.status().connected().then_some(connection))
}
fn check_open(&self) -> Result<()> {
if self.is_closing() {
return Err(RabbitMqError::Closed.into_core());
}
Ok(())
}
fn install(&self, connection: Arc<Connection>) -> u64 {
*self
.current
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = connection;
self.generation.fetch_add(1, Ordering::Release) + 1
}
pub(crate) async fn sleep_unless_closing(&self, delay: Duration) -> Result<()> {
let closing = self.closing_notify.notified();
tokio::pin!(closing);
closing.as_mut().enable();
self.check_open()?;
tokio::select! {
biased;
() = closing => Err(RabbitMqError::Closed.into_core()),
() = tokio::time::sleep(delay) => self.check_open(),
}
}
async fn redeclare(&self, connection: &Connection) {
let configs: Vec<QueueConfig> = self.lock_configs().values().cloned().collect();
if configs.is_empty() {
return;
}
let channel = match connection.create_channel().await {
Ok(channel) => channel,
Err(error) => {
error!(
%error,
queues = configs.len(),
"could not open a channel to re-declare the topology after reconnecting; \
queues lost to a broker restart stay missing until something declares them"
);
return;
}
};
let mut declared = 0usize;
for config in &configs {
if let Err(error) = declare_topology(&channel, config, &self.options).await {
error!(
queue = %config.name,
%error,
declared,
remaining = configs.len() - declared,
"re-declaring a queue after reconnecting failed; operations on it will fail \
until it is declared successfully"
);
break;
}
declared += 1;
}
if declared == configs.len() {
debug!(queues = declared, "topology re-declared after reconnect");
}
if channel.status().connected()
&& let Err(error) = channel.close(REPLY_SUCCESS, "OK".into()).await
{
debug!(%error, "closing the re-declaration channel failed");
}
}
pub(crate) fn mark_closing(&self) {
if !self.closing.swap(true, Ordering::AcqRel) {
self.closing_notify.notify_waiters();
}
}
pub(crate) async fn close(&self) -> Result<()> {
self.mark_closing();
let connection = self.peek();
if !connection.status().connected() {
return Ok(());
}
match connection.close(REPLY_SUCCESS, "OK".into()).await {
Ok(()) => Ok(()),
Err(error) if crate::backend::is_benign_close_error(&error) => {
debug!(%error, "connection already closing; treating close as successful");
Ok(())
}
Err(error) => Err(amqp(error)),
}
}
}
async fn close_quietly(connection: &Connection) {
if !connection.status().connected() {
return;
}
if let Err(error) = connection.close(REPLY_SUCCESS, "OK".into()).await {
debug!(%error, "closing a connection opened by a cancelled reconnect failed");
}
}
pub(crate) async fn declare_topology(
channel: &Channel,
config: &QueueConfig,
options: &RabbitMqOptions,
) -> Result<()> {
channel
.queue_declare(
short_string(&config.name)?,
topology::declare_options(config.durable),
topology::queue_args(config),
)
.await
.map_err(amqp)?;
if options.declare_dead_letter_queues {
let dead = topology::dead_queue_name(&config.name, &options.dead_suffix);
channel
.queue_declare(
short_string(&dead)?,
topology::declare_options(true),
topology::dead_queue_args(config),
)
.await
.map_err(amqp)?;
}
Ok(())
}