use std::fmt;
use std::num::NonZeroUsize;
use std::slice;
use std::time::Duration;
use futures::{Stream, StreamExt};
use tokio::time::timeout;
use super::{
DEFAULT_TIMEOUT, REDELIVERY_TIMEOUT, SubscriberMessage, ack_or_unsupported, expect_within,
nack_requeue, within,
};
use crate::conformance::helpers::unique_subject;
use crate::conformance::lifecycle::shutdown_within;
use crate::{
AckError, AddressedCopies, BatchSubscriber, Broker, BrokerMoves, Carries, CarriesBatch,
Connected, IncomingMessage, NamedCopies, OutgoingMessage, Publisher, Subscriber,
SubscriptionSource,
};
const ROUNDS: usize = 5;
const BATCH: NonZeroUsize = NonZeroUsize::new(16).unwrap();
#[doc(hidden)]
pub trait CopyPathCheck {
fn copy_check<Check>(check: Check) -> Option<Check>;
}
impl CopyPathCheck for AddressedCopies {
fn copy_check<Check>(check: Check) -> Option<Check> {
Some(check)
}
}
impl CopyPathCheck for NamedCopies {
fn copy_check<Check>(check: Check) -> Option<Check> {
Some(check)
}
}
impl CopyPathCheck for BrokerMoves {
fn copy_check<Check>(_check: Check) -> Option<Check> {
None
}
}
#[allow(clippy::future_not_send)]
pub async fn carries<
B,
Value,
MkBroker,
Src,
MkSrc,
Pub,
MkPub,
Publish,
PublishError,
Gone,
GoneError,
>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
publish: Publish,
publish_without_value: Gone,
values: &[Value],
) where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Send,
Src::Subscriber: Send,
Src::Copies: CopyPathCheck,
SubscriberMessage<Src::Subscriber>: Carries<Value>,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
Publish: AsyncFn(&Connected<B>, &str, &Value) -> Result<(), PublishError>,
PublishError: fmt::Debug,
Gone: AsyncFn(&Connected<B>, &str) -> Result<(), GoneError>,
GoneError: fmt::Debug,
Value: PartialEq + fmt::Debug,
{
const LABEL: &str = "carries";
assert!(
distinct_pair(values).is_some(),
"{LABEL}: pass at least two distinct values, so a delivery lending one twice shows",
);
let subject = unique_subject("conformance.carries");
let connected = within(make_broker().connect(), "carries: connect")
.await
.expect("broker must connect");
let mut subscriber = within(
make_source(&subject).subscribe(&connected),
"carries: subscribe",
)
.await
.expect("subscription must open after connect");
for value in values {
within(publish(&connected, &subject, value), "carries: a publish")
.await
.expect("publishing a value failed");
}
let mut stream = std::pin::pin!(subscriber.stream());
let mut unseen: Vec<&Value> = values.iter().collect();
for _ in values {
let msg = expect_within(
&mut stream,
DEFAULT_TIMEOUT,
LABEL,
"every published value must arrive",
)
.await;
let lent = msg.carried().unwrap_or_else(|| {
panic!("{LABEL}: a delivery of a published value must lend it, and it lent none")
});
take_published(&mut unseen, lent, LABEL);
ack_or_unsupported(msg, LABEL).await;
}
let copy = a_copy_lends_the_value(
&connected,
&make_publisher,
&publish,
&mut stream,
&subject,
&values[0],
);
if let Some(copy) = <Src::Copies as CopyPathCheck>::copy_check(copy) {
copy.await;
}
within(
publish_without_value(&connected, &subject),
"carries: publishing a delivery without a value",
)
.await
.expect("making a delivery without a value failed");
let msg = expect_within(
&mut stream,
DEFAULT_TIMEOUT,
LABEL,
"the delivery without a value must arrive",
)
.await;
if let Some(lent) = msg.carried() {
panic!(
"{LABEL}: a delivery without a value must lend none, so the runtime settles it by \
the decode-failure policy; it lent {lent:?}, which a handler would take for real",
);
}
drop_unread(msg, LABEL).await;
shutdown_within(connected, LABEL).await;
}
#[allow(clippy::future_not_send)]
pub async fn carries_batch<
B,
Value,
MkBroker,
Src,
MkSrc,
Pub,
MkPub,
Publish,
PublishError,
Gone,
GoneError,
>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
publish: Publish,
publish_without_value: Gone,
values: &[Value],
) where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Send,
Src::Subscriber: BatchSubscriber + Send,
Src::Copies: CopyPathCheck,
<Src::Subscriber as BatchSubscriber>::Batch: CarriesBatch<Value>,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
Publish: AsyncFn(&Connected<B>, &str, &Value) -> Result<(), PublishError>,
PublishError: fmt::Debug,
Gone: AsyncFn(&Connected<B>, &str) -> Result<(), GoneError>,
GoneError: fmt::Debug,
Value: PartialEq + Clone + fmt::Debug,
{
const LABEL: &str = "carries_batch";
let Some(pair) = distinct_pair(values) else {
panic!(
"{LABEL}: pass at least two distinct values, so a batch lending one twice or out of \
order shows",
);
};
let subject = unique_subject("conformance.carries_batch");
let connected = within(make_broker().connect(), "carries_batch: connect")
.await
.expect("broker must connect");
let mut subscriber = within(
make_source(&subject).subscribe(&connected),
"carries_batch: subscribe",
)
.await
.expect("subscription must open after connect");
for value in values {
within(
publish(&connected, &subject, value),
"carries_batch: a publish",
)
.await
.expect("publishing a value failed");
}
let mut stream = std::pin::pin!(subscriber.batches(BATCH));
let mut unseen: Vec<&Value> = values.iter().collect();
while !unseen.is_empty() {
let (lent, deliveries) = next_page(
&mut stream,
DEFAULT_TIMEOUT,
LABEL,
"every published value must arrive",
)
.await;
assert_eq!(
lent.len(),
deliveries.len(),
"{LABEL}: every delivery of a published value must lend it",
);
for value in &lent {
take_published(&mut unseen, value, LABEL);
}
for msg in deliveries {
ack_or_unsupported(msg, LABEL).await;
}
}
pages_keep_their_order(&connected, &publish, &mut stream, &subject, pair).await;
let copy = a_batch_copy_lends_the_value(
&connected,
&make_publisher,
&publish,
&mut stream,
&subject,
&values[0],
);
if let Some(copy) = <Src::Copies as CopyPathCheck>::copy_check(copy) {
copy.await;
}
values_go_before_deliveries_without_one(
&connected,
&publish,
&publish_without_value,
&mut stream,
&subject,
&values[0],
)
.await;
shutdown_within(connected, LABEL).await;
}
#[allow(clippy::future_not_send)]
async fn a_copy_lends_the_value<Conn, Value, Pub, MkPub, Publish, PublishError, S, M, E>(
connected: &Conn,
make_publisher: &MkPub,
publish: &Publish,
stream: &mut S,
subject: &str,
value: &Value,
) where
Pub: Publisher,
MkPub: Fn(&Conn) -> Pub,
Publish: AsyncFn(&Conn, &str, &Value) -> Result<(), PublishError>,
PublishError: fmt::Debug,
S: Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage + Carries<Value>,
E: fmt::Debug,
Value: PartialEq + fmt::Debug,
{
const LABEL: &str = "carries: a copy of a delivery lends its value";
let publisher = make_publisher(connected);
within(publish(connected, subject, value), "carries: a publish")
.await
.expect("publishing a value failed");
let msg = expect_within(
stream,
DEFAULT_TIMEOUT,
LABEL,
"the published value must arrive",
)
.await;
publish_copy(&publisher, subject, &msg, LABEL).await;
ack_or_unsupported(msg, LABEL).await;
let copy = expect_within(stream, DEFAULT_TIMEOUT, LABEL, "the copy must arrive").await;
assert_eq!(
copy.carried(),
Some(value),
"{LABEL}: a copy of a delivery, published from its payload and headers the way the \
runtime publishes a retry copy or a dead letter, must lend the value the delivery lent",
);
ack_or_unsupported(copy, LABEL).await;
}
#[allow(clippy::future_not_send)]
async fn a_batch_copy_lends_the_value<
Conn,
Value,
Pub,
MkPub,
Publish,
PublishError,
S,
Batch,
M,
E,
>(
connected: &Conn,
make_publisher: &MkPub,
publish: &Publish,
stream: &mut S,
subject: &str,
value: &Value,
) where
Pub: Publisher,
MkPub: Fn(&Conn) -> Pub,
Publish: AsyncFn(&Conn, &str, &Value) -> Result<(), PublishError>,
PublishError: fmt::Debug,
S: Stream<Item = Result<Batch, E>> + Unpin,
Batch: CarriesBatch<Value> + IntoIterator<Item = M>,
M: IncomingMessage,
E: fmt::Debug,
Value: PartialEq + Clone + fmt::Debug,
{
const LABEL: &str = "carries_batch: a copy of a delivery lends its value";
let publisher = make_publisher(connected);
within(
publish(connected, subject, value),
"carries_batch: a publish",
)
.await
.expect("publishing a value failed");
let (_, deliveries) = next_page(
&mut *stream,
DEFAULT_TIMEOUT,
LABEL,
"the published value must arrive",
)
.await;
for msg in deliveries {
publish_copy(&publisher, subject, &msg, LABEL).await;
ack_or_unsupported(msg, LABEL).await;
}
let (lent, deliveries) =
next_page(&mut *stream, DEFAULT_TIMEOUT, LABEL, "the copy must arrive").await;
assert_eq!(
lent,
slice::from_ref(value),
"{LABEL}: a copy of a delivery, published from its payload and headers the way the \
runtime publishes a retry copy or a dead letter, must lend the value the delivery lent",
);
for msg in deliveries {
ack_or_unsupported(msg, LABEL).await;
}
}
async fn publish_copy<Pub: Publisher, M: IncomingMessage>(
publisher: &Pub,
subject: &str,
msg: &M,
label: &str,
) {
let copy = OutgoingMessage::new(subject, msg.payload()).with_headers(msg.headers().clone());
within(
publisher.publish(copy, None),
&format!("{label}: publishing a copy"),
)
.await
.expect("publishing a copy failed");
}
#[allow(clippy::future_not_send)]
async fn pages_keep_their_order<Conn, Value, Publish, PublishError, S, Batch, M, E>(
connected: &Conn,
publish: &Publish,
stream: &mut S,
subject: &str,
pair: [&Value; 2],
) where
Publish: AsyncFn(&Conn, &str, &Value) -> Result<(), PublishError>,
PublishError: fmt::Debug,
S: Stream<Item = Result<Batch, E>> + Unpin,
Batch: CarriesBatch<Value> + IntoIterator<Item = M>,
M: IncomingMessage,
E: fmt::Debug,
Value: PartialEq + Clone + fmt::Debug,
{
const LABEL: &str = "carries_batch: a batch lends its values in delivery order";
for _ in 0..ROUNDS {
for value in pair {
within(
publish(connected, subject, value),
"carries_batch: a publish",
)
.await
.expect("publishing a value failed");
}
let mut shared = None;
let mut delivered = 0;
while delivered < 2 {
let (lent, deliveries) = next_page(
&mut *stream,
DEFAULT_TIMEOUT,
LABEL,
"both published values must arrive",
)
.await;
delivered += deliveries.len();
if lent.len() > 1 {
shared = Some((lent, deliveries));
} else {
for msg in deliveries {
ack_or_unsupported(msg, LABEL).await;
}
}
}
let Some((lent, deliveries)) = shared else {
continue;
};
let first = lent[0].clone();
let mut requeued = false;
for (element, msg) in deliveries.into_iter().enumerate() {
if element == 0 {
requeued = nack_requeue(msg, LABEL).await;
} else {
ack_or_unsupported(msg, LABEL).await;
}
}
if !requeued {
return;
}
let (lent, deliveries) = next_page(
&mut *stream,
REDELIVERY_TIMEOUT,
LABEL,
"the delivery nacked with requeue must come back",
)
.await;
assert_eq!(
lent,
[first],
"{LABEL}: the value at index `i` must belong to the `i`-th delivery: the first \
delivery was nacked with requeue, and what came back must lend the value the batch \
lent first",
);
for msg in deliveries {
ack_or_unsupported(msg, LABEL).await;
}
return;
}
}
#[allow(clippy::future_not_send)]
async fn values_go_before_deliveries_without_one<
Conn,
Value,
Publish,
PublishError,
Gone,
GoneError,
S,
Batch,
M,
E,
>(
connected: &Conn,
publish: &Publish,
publish_without_value: &Gone,
stream: &mut S,
subject: &str,
value: &Value,
) where
Publish: AsyncFn(&Conn, &str, &Value) -> Result<(), PublishError>,
PublishError: fmt::Debug,
Gone: AsyncFn(&Conn, &str) -> Result<(), GoneError>,
GoneError: fmt::Debug,
S: Stream<Item = Result<Batch, E>> + Unpin,
Batch: CarriesBatch<Value> + IntoIterator<Item = M>,
M: IncomingMessage,
E: fmt::Debug,
Value: PartialEq + Clone + fmt::Debug,
{
const LABEL: &str = "carries_batch: a delivery without a value goes last";
for _ in 0..ROUNDS {
within(
publish_without_value(connected, subject),
"carries_batch: publishing a delivery without a value",
)
.await
.expect("making a delivery without a value failed");
within(
publish(connected, subject, value),
"carries_batch: a publish",
)
.await
.expect("publishing a value failed");
let mut lent_by_all = Vec::new();
let mut shared = None;
let mut delivered = 0;
while delivered < 2 {
let (lent, deliveries) = next_page(
&mut *stream,
DEFAULT_TIMEOUT,
LABEL,
"both deliveries must arrive",
)
.await;
delivered += deliveries.len();
lent_by_all.extend(lent.iter().cloned());
if deliveries.len() > 1 {
shared = Some((lent, deliveries));
} else {
for (element, msg) in deliveries.into_iter().enumerate() {
if element >= lent.len() {
drop_unread(msg, LABEL).await;
} else {
ack_or_unsupported(msg, LABEL).await;
}
}
}
}
assert!(
lent_by_all == slice::from_ref(value),
"{LABEL}: a delivery without a value must be lent none: it goes past the end of the \
slice, where the runtime settles it by the decode-failure policy; the batches lent \
{lent_by_all:?} for one value and one delivery without a value",
);
let Some((lent, deliveries)) = shared else {
continue;
};
let mut requeued = false;
for (element, msg) in deliveries.into_iter().enumerate() {
if element >= lent.len() {
drop_unread(msg, LABEL).await;
} else if element == 0 {
requeued = nack_requeue(msg, LABEL).await;
} else {
ack_or_unsupported(msg, LABEL).await;
}
}
if !requeued {
return;
}
let (lent, deliveries) = next_page(
&mut *stream,
REDELIVERY_TIMEOUT,
LABEL,
"the delivery nacked with requeue must come back",
)
.await;
assert_eq!(
lent,
slice::from_ref(value),
"{LABEL}: a delivery without a value must come after every delivery with one: the \
batch's first delivery was nacked with requeue, and what came back must lend the \
value the batch lent",
);
for msg in deliveries {
ack_or_unsupported(msg, LABEL).await;
}
return;
}
}
async fn next_page<S, Batch, M, E, Value>(
stream: &mut S,
within: Duration,
label: &str,
expected: &str,
) -> (Vec<Value>, Vec<M>)
where
S: Stream<Item = Result<Batch, E>> + Unpin,
Batch: CarriesBatch<Value> + IntoIterator<Item = M>,
E: fmt::Debug,
Value: Clone + fmt::Debug,
{
let batch = timeout(within, stream.next())
.await
.unwrap_or_else(|_| panic!("{label}: nothing arrived within {within:?}; {expected}"))
.unwrap_or_else(|| panic!("{label}: the batch stream ended; {expected}"))
.unwrap_or_else(|err| panic!("{label}: the batch stream yielded an error: {err:?}"));
let lent = batch.carried().to_vec();
let deliveries: Vec<M> = batch.into_iter().collect();
assert!(
!deliveries.is_empty(),
"{label}: a yielded batch must not be empty; {expected}",
);
assert!(
lent.len() <= deliveries.len(),
"{label}: a batch must never lend more values than it delivers: it lent {lent:?} for {} \
deliveries",
deliveries.len(),
);
(lent, deliveries)
}
fn distinct_pair<Value: PartialEq>(values: &[Value]) -> Option<[&Value; 2]> {
let (first, rest) = values.split_first()?;
let other = rest.iter().find(|value| *value != first)?;
Some([first, other])
}
fn take_published<Value: PartialEq + fmt::Debug>(
unseen: &mut Vec<&Value>,
lent: &Value,
label: &str,
) {
let position = unseen
.iter()
.position(|value| *value == lent)
.unwrap_or_else(|| {
panic!(
"{label}: a delivery lent a value that was not published, or lent one value twice: \
{lent:?}",
)
});
unseen.remove(position);
}
async fn drop_unread<M: IncomingMessage>(msg: M, label: &str) {
match within(msg.nack(false), &format!("{label}: a nack")).await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("{label}: nack must succeed or be unsupported, got: {other:?}"),
}
}