use std::{fmt, num::NonZeroU32, time::Duration};
use futures::{Stream, StreamExt, stream};
use tokio::time::{Instant, timeout, timeout_at};
use super::harness::on_foreign_runtime;
use super::helpers::unique_subject;
use crate::{
AckError, Broker, BrokerMoves, Connected, ConnectedBroker, HeaderMap, IncomingMessage,
OutgoingMessage, Publisher, RedeliveryAddressed, RetryDeclaration, Subscriber,
SubscriptionSource, runtime::RETRY_COUNT_HEADER,
};
const ARRIVAL_TIMEOUT: Duration = Duration::from_secs(10);
const MOVE_TIMEOUT: Duration = Duration::from_secs(30);
const QUIET: Duration = Duration::from_millis(500);
const EXTRA_DELIVERIES: usize = 8;
const REQUEUES: u64 = 2;
const COPY_HEADER: &str = "x-conformance-copy";
const COPY_HEADER_VALUE: &[u8] = b"retry";
const FIRST_COPY_COUNT: &str = "1";
const CONTROL: &[u8] = b"control";
const COPY: &[u8] = b"copy";
const POISON: &[u8] = b"poison";
pub async fn redelivery_address<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
) where
B: Broker,
MkBroker: Fn() -> B,
Src: RedeliveryAddressed<Connected<B>> + Clone + Send + Sync,
Src::Subscriber: Send,
MkSrc: Fn(&str) -> Src,
Pub: Publisher + 'static,
MkPub: Fn(&Connected<B>) -> Pub,
{
let subject = unique_subject("conformance.redelivery");
let connected = make_broker()
.connect()
.await
.expect("broker must connect after synchronous construction");
let source = make_source(&subject);
let address = source
.redelivery_address(&connected)
.await
.expect("reporting a redelivery address must not fail against a live connection");
let mut first = source
.clone()
.subscribe(&connected)
.await
.expect("subscription source must open against the connected form");
let mut second = source.subscribe(&connected).await.ok();
let publisher = make_publisher(&connected);
let mut headers = HeaderMap::new();
headers.insert(COPY_HEADER, COPY_HEADER_VALUE);
headers.insert(RETRY_COUNT_HEADER, FIRST_COPY_COUNT);
let destination = address.as_str().to_owned();
let (publisher, sent) = on_foreign_runtime(async move || {
let sent = publisher
.publish(
OutgoingMessage::new(&destination, COPY).with_headers(headers),
None,
)
.await
.map_err(|err| err.to_string());
(publisher, sent)
})
.await;
if let Err(err) = sent {
panic!(
"redelivery_address: publishing the retry copy to the reported address {address} \
failed: {err}"
);
}
publisher
.publish(OutgoingMessage::new(&subject, CONTROL), None)
.await
.expect(
"redelivery_address: publishing the control message to the descriptor's subject after \
the retry copy failed",
);
let second = second.as_mut().map_or_else(
|| stream::pending().right_stream(),
|second| second.stream().map(|item| (1_usize, item)).left_stream(),
);
let mut readers = std::pin::pin!(stream::select(
first.stream().map(|item| (0_usize, item)),
second,
));
let mut seen = Seen::default();
let deadline = Instant::now() + ARRIVAL_TIMEOUT;
while seen.control_total() == 0 || seen.copy_total() == 0 {
let Ok(next) = timeout_at(deadline, readers.next()).await else {
panic!(
"redelivery_address: {} within {ARRIVAL_TIMEOUT:?}; a copy published to the \
reported address {address} must reach the subscription that reported it",
seen.missing(),
);
};
let (reader, msg) = unwrap_delivery(next, "redelivery_address");
seen.take(reader, msg, address.as_str()).await;
}
for _ in 0..EXTRA_DELIVERIES {
match timeout(QUIET, readers.next()).await {
Err(_) => break,
Ok(next) => {
let (reader, msg) = unwrap_delivery(next, "redelivery_address");
seen.take(reader, msg, address.as_str()).await;
}
}
}
seen.assert_once_per_group(address.as_str());
let (_, copy) = seen
.held
.take()
.expect("the loop above stops only once a copy arrived");
count_across_requeues(copy, &mut readers).await;
let _closed = connected
.shutdown()
.await
.expect("broker must shut down cleanly");
}
struct Seen<M> {
control: [u32; 2],
copies: [u32; 2],
held: Option<(usize, M)>,
}
impl<M> Default for Seen<M> {
fn default() -> Self {
Self {
control: [0; 2],
copies: [0; 2],
held: None,
}
}
}
impl<M: IncomingMessage> Seen<M> {
fn control_total(&self) -> u32 {
self.control.iter().sum()
}
fn copy_total(&self) -> u32 {
self.copies.iter().sum()
}
fn missing(&self) -> &'static str {
match (self.control_total(), self.copy_total()) {
(0, 0) => "neither the control message nor the copy arrived",
(0, _) => "the control message published to the descriptor's subject never arrived",
_ => "the copy never arrived",
}
}
async fn take(&mut self, reader: usize, msg: M, address: &str) {
if msg.payload() == CONTROL {
self.control[reader] += 1;
settle_ack(msg, "redelivery_address control").await;
return;
}
assert_eq!(
msg.payload(),
COPY,
"redelivery_address: a subscription received a message the check never published",
);
self.copies[reader] += 1;
assert_copy_headers(&msg, &format!("the copy published to {address}"));
if self.held.is_none() {
self.held = Some((reader, msg));
} else {
settle_ack(msg, "redelivery_address copy").await;
}
}
fn assert_once_per_group(&self, address: &str) {
for reader in 0..2 {
assert!(
self.control[reader] <= 1 && self.copies[reader] <= 1,
"redelivery_address: one subscription received the control message {} and the \
copy {} times; each is published once",
self.control[reader],
self.copies[reader],
);
}
assert_eq!(
self.copy_total(),
self.control_total(),
"redelivery_address: the copy published to {address} reached {} of two subscriptions \
of one descriptor where a publish to its subject reached {}; exactly one consumer of \
a group gets a retry copy, as it gets any delivery",
self.copy_total(),
self.control_total(),
);
}
}
async fn count_across_requeues<S, M, E>(mut msg: M, readers: &mut S)
where
S: Stream<Item = (usize, Result<M, E>)> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
assert_count(&msg, 1, "the copy's first delivery");
for delivery in 2..=REQUEUES + 1 {
match msg.nack(true).await {
Ok(()) => {}
Err(AckError::Unsupported) => return,
Err(other) => panic!(
"redelivery_address: nack(requeue = true) must succeed or be unsupported, got: \
{other:?}"
),
}
let again = next_copy(readers).await;
assert_copy_headers(&again, "the copy redelivered after nack(true)");
assert_count(&again, delivery, "a redelivery after nack(true)");
msg = again;
}
settle_ack(msg, "redelivery_address requeued copy").await;
}
async fn next_copy<S, M, E>(readers: &mut S) -> M
where
S: Stream<Item = (usize, Result<M, E>)> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
let deadline = Instant::now() + ARRIVAL_TIMEOUT;
for _ in 0..EXTRA_DELIVERIES {
let next = timeout_at(deadline, readers.next())
.await
.unwrap_or_else(|_| {
panic!(
"redelivery_address: a copy requeued with nack(true) did not come back within \
{ARRIVAL_TIMEOUT:?}; Ok from a requeue promises a redelivery",
)
});
let (_, again) = unwrap_delivery(next, "redelivery_address requeue");
if again.payload() == CONTROL {
settle_ack(again, "redelivery_address control redelivered").await;
continue;
}
assert_eq!(
again.payload(),
COPY,
"redelivery_address: nack(true) must redeliver the requeued copy",
);
return again;
}
panic!(
"redelivery_address: {EXTRA_DELIVERIES} deliveries of the control message arrived and \
none of the requeued copy"
);
}
fn assert_copy_headers<M: IncomingMessage>(msg: &M, which: &str) {
assert_eq!(
msg.headers().get(COPY_HEADER),
Some(COPY_HEADER_VALUE),
"redelivery_address: {which} lost the header {COPY_HEADER}; a retry copy carries the \
headers the handler saw",
);
assert_eq!(
msg.headers().get_str(RETRY_COUNT_HEADER),
Some(FIRST_COPY_COUNT),
"redelivery_address: {which} lost {RETRY_COUNT_HEADER}; the runtime counts a \
registration's attempts with it",
);
}
fn assert_count<M: IncomingMessage>(msg: &M, expected: u64, which: &str) {
if let Some(count) = msg.redelivery_count() {
assert_eq!(
count, expected,
"redelivery_count on {which} must be {expected}: it counts the deliveries of one \
message, the first one included, and a registration's max_attempts is spent against \
it",
);
}
}
pub async fn broker_moves<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
attempts: NonZeroU32,
) where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>, Copies = BrokerMoves> + Send,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
for (what, half) in half_declarations(attempts) {
let connected = make_broker()
.connect()
.await
.expect("broker must connect after synchronous construction");
let source = make_source(&dashed("conformance-half")).declare_retry(&half);
refuses_at_startup(source, &connected, &half, what).await;
let _closed = connected
.shutdown()
.await
.expect("broker must shut down cleanly");
}
let connected = make_broker()
.connect()
.await
.expect("broker must connect after synchronous construction");
let subject = dashed("conformance-moves");
let dead = dashed("conformance-dead");
let mut dead_letters = make_source(&dead)
.subscribe(&connected)
.await
.expect("the dead-letter destination must open as a subscription of its own");
let declared = RetryDeclaration::new()
.with_max_attempts(attempts)
.with_dead_letter(dead.clone());
let source = make_source(&subject).declare_retry(&declared);
source
.declare_retry_on(&connected, &declared)
.unwrap_or_else(|err| {
panic!(
"broker_moves: a declaration of max_attempts({attempts}) and dead_letter({dead:?}) \
was refused: {err}"
)
});
let mut subscription = source.subscribe(&connected).await.unwrap_or_else(|err| {
panic!(
"broker_moves: the subscription declaring max_attempts({attempts}) and \
dead_letter({dead:?}) did not open: {err:?}"
)
});
make_publisher(&connected)
.publish(OutgoingMessage::new(&subject, POISON), None)
.await
.expect("broker_moves: publish to the subscription's name failed");
let mut deliveries = std::pin::pin!(stream::select(
subscription
.stream()
.map(|item| (Place::Subscription, item)),
dead_letters.stream().map(|item| (Place::DeadLetter, item)),
));
moves_at_the_cap(&mut deliveries, attempts).await;
let _closed = connected
.shutdown()
.await
.expect("broker must shut down cleanly");
}
fn half_declarations(attempts: NonZeroU32) -> [(&'static str, RetryDeclaration); 2] {
[
(
"max_attempts(..) with no dead_letter(..)",
RetryDeclaration::new().with_max_attempts(attempts),
),
(
"dead_letter(..) with no max_attempts(..)",
RetryDeclaration::new().with_dead_letter(dashed("conformance-dead")),
),
]
}
async fn refuses_at_startup<C, Src>(source: Src, connected: &C, half: &RetryDeclaration, what: &str)
where
C: ConnectedBroker,
Src: SubscriptionSource<C> + Send,
{
if source.declare_retry_on(connected, half).is_err() {
return;
}
let opened = source.subscribe(connected).await;
assert!(
opened.is_err(),
"broker_moves: a registration declaring {what} opened its subscription; a broker that \
moves spent deliveries itself maps the cap and the destination together, and refuses a \
declaration it cannot map at startup instead of ignoring it",
);
}
async fn moves_at_the_cap<S, M, E>(deliveries: &mut S, attempts: NonZeroU32)
where
S: Stream<Item = (Place, Result<M, E>)> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
for delivery in 1..=u64::from(attempts.get()) {
let (place, msg) = next_move(deliveries, "a delivery of the message").await;
assert_eq!(
place,
Place::Subscription,
"broker_moves: the message reached the dead-letter destination after {} deliveries; \
the registration declared max_attempts({attempts})",
delivery - 1,
);
assert_eq!(msg.payload(), POISON, "broker_moves: an unexpected message");
assert_count(
&msg,
delivery,
"a delivery the broker counts against the cap",
);
match msg.nack(true).await {
Ok(()) => {}
Err(AckError::Unsupported) => return,
Err(other) => panic!(
"broker_moves: nack(requeue = true) on delivery {delivery} of \
max_attempts({attempts}) must succeed or be unsupported, got: {other:?}"
),
}
}
let (place, moved) = next_move(deliveries, "the dead-lettered message").await;
assert_eq!(
place,
Place::DeadLetter,
"broker_moves: the message was delivered {} times; the registration declared \
max_attempts({attempts}), after which it goes to the dead-letter destination",
u64::from(attempts.get()) + 1,
);
assert_eq!(
moved.payload(),
POISON,
"broker_moves: the dead-letter destination received a message the check never published",
);
settle_ack(moved, "broker_moves dead letter").await;
if let Ok(next) = timeout(QUIET, deliveries.next()).await {
let (place, _) = unwrap_delivery(next, "broker_moves");
panic!(
"broker_moves: after the message reached the dead-letter destination it arrived again \
on {place}; a spent message is moved once and delivered nowhere else"
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Place {
Subscription,
DeadLetter,
}
impl fmt::Display for Place {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Subscription => "the subscription",
Self::DeadLetter => "the dead-letter destination",
})
}
}
async fn next_move<S, M, E>(deliveries: &mut S, what: &str) -> (Place, M)
where
S: Stream<Item = (Place, Result<M, E>)> + Unpin,
E: fmt::Debug,
{
let next = timeout(MOVE_TIMEOUT, deliveries.next())
.await
.unwrap_or_else(|_| panic!("broker_moves: {what} did not arrive within {MOVE_TIMEOUT:?}"));
unwrap_delivery(next, "broker_moves")
}
fn unwrap_delivery<Tag, M, E: fmt::Debug>(
next: Option<(Tag, Result<M, E>)>,
label: &str,
) -> (Tag, M) {
let (tag, item) = next.unwrap_or_else(|| panic!("{label}: a subscription stream ended"));
let msg =
item.unwrap_or_else(|err| panic!("{label}: a subscription yielded an error: {err:?}"));
(tag, msg)
}
fn dashed(prefix: &str) -> String {
unique_subject(prefix).replace('.', "-")
}
async fn settle_ack<M: IncomingMessage>(msg: M, label: &str) {
match msg.ack().await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("{label}: ack must succeed or be unsupported, got: {other:?}"),
}
}
#[cfg(all(test, feature = "memory"))]
mod tests;