use lapin::types::ShortString;
use queuey_core::Error;
use thiserror::Error as ThisError;
pub(crate) fn amqp(error: lapin::Error) -> Error {
Error::backend(error)
}
#[derive(Debug, ThisError)]
pub(crate) enum RabbitMqError {
#[error("broker nacked the message published to `{queue}`")]
Nacked {
queue: String,
},
#[error(
"broker returned the message published to `{routing_key}` as unroutable: {reply_code} {reply_text}"
)]
Returned {
reply_code: u16,
reply_text: String,
routing_key: String,
},
#[error("publisher confirms are not enabled; cannot confirm publish to `{queue}`")]
ConfirmsNotEnabled {
queue: String,
},
#[error(
"cannot {operation} delivery of job `{job_id}` on `{queue}`: it was already settled or its channel is gone"
)]
AlreadySettled {
operation: &'static str,
job_id: String,
queue: String,
},
#[error("invalid AMQP name `{name}`: {source}")]
InvalidName {
name: String,
#[source]
source: lapin::types::ShortStringError,
},
#[error(
"delay of {requested:?} is longer than a hold queue can wait ({max:?}); it was refused rather than released early"
)]
DelayTooLong {
requested: std::time::Duration,
max: std::time::Duration,
},
#[error(
"queue `{queue}` leaves no room for its hold queues: `{hold}` is {length} bytes, over the {limit}-byte AMQP limit"
)]
DeferredNameTooLong {
queue: String,
hold: String,
length: usize,
limit: usize,
},
}
impl RabbitMqError {
pub(crate) fn into_core(self) -> Error {
Error::backend(self)
}
}
pub(crate) fn short_string(value: &str) -> Result<ShortString, Error> {
ShortString::try_new(value).map_err(|source| {
RabbitMqError::InvalidName {
name: value.to_owned(),
source,
}
.into_core()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_string_accepts_normal_queue_names() {
assert_eq!(
short_string("myapp.emails").unwrap().as_str(),
"myapp.emails"
);
}
#[test]
fn short_string_accepts_exactly_255_bytes() {
let name = "a".repeat(255);
assert_eq!(short_string(&name).unwrap().as_str(), name);
}
#[test]
fn short_string_rejects_over_long_names_without_panicking() {
let name = "a".repeat(256);
let err = short_string(&name).expect_err("expected rejection");
assert!(matches!(err, Error::Backend(_)), "got {err:?}");
assert!(err.to_string().contains("invalid AMQP name"));
}
#[test]
fn nack_error_names_the_queue() {
let err = RabbitMqError::Nacked {
queue: "emails.deferred.1000".to_owned(),
};
assert_eq!(
err.to_string(),
"broker nacked the message published to `emails.deferred.1000`"
);
}
#[test]
fn returned_error_carries_the_brokers_reply() {
let err = RabbitMqError::Returned {
reply_code: 312,
reply_text: "NO_ROUTE".to_owned(),
routing_key: "emails.dead".to_owned(),
};
let text = err.to_string();
assert!(text.contains("emails.dead"), "{text}");
assert!(text.contains("312"), "{text}");
assert!(text.contains("NO_ROUTE"), "{text}");
}
#[test]
fn already_settled_error_names_the_operation_and_job() {
let err = RabbitMqError::AlreadySettled {
operation: "ack",
job_id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_owned(),
queue: "emails".to_owned(),
};
let text = err.to_string();
assert!(text.contains("cannot ack delivery"), "{text}");
assert!(
text.contains("67e55044-10b1-426f-9247-bb680e5fe0c8"),
"{text}"
);
assert!(text.contains("emails"), "{text}");
}
#[test]
fn delay_too_long_says_it_was_refused_not_shortened() {
let err = RabbitMqError::DelayTooLong {
requested: std::time::Duration::from_secs(30 * 86_400),
max: std::time::Duration::from_millis(u64::from(crate::topology::MAX_DEFERRAL_MS)),
};
let text = err.to_string();
assert!(text.contains("longer than a hold queue can wait"), "{text}");
assert!(
text.contains("refused rather than released early"),
"{text}"
);
assert!(matches!(err.into_core(), Error::Backend(_)));
}
#[test]
fn deferred_name_too_long_names_both_queues() {
let err = RabbitMqError::DeferredNameTooLong {
queue: "q".repeat(250),
hold: format!("{}.deferred.2147483647", "q".repeat(250)),
length: 270,
limit: 255,
};
let text = err.to_string();
assert!(
text.contains("leaves no room for its hold queues"),
"{text}"
);
assert!(text.contains("270 bytes"), "{text}");
assert!(text.contains("255-byte"), "{text}");
}
#[test]
fn confirms_not_enabled_error_names_the_queue() {
let err = RabbitMqError::ConfirmsNotEnabled {
queue: "emails".to_owned(),
};
assert!(
err.to_string()
.contains("publisher confirms are not enabled")
);
}
}