use std::{fmt, num::NonZeroUsize};
use futures::{Stream, StreamExt};
use tokio::time::timeout;
use super::{
DEFAULT_TIMEOUT, REDELIVERY_TIMEOUT, SeekPosition, SubscriberMessage, ack_or_unsupported,
collect_batched, collect_batches, expect_no_batch, nack_requeue, payloads, publish_all, within,
};
use crate::conformance::harness::on_foreign_runtime;
use crate::conformance::helpers::unique_subject;
use crate::conformance::lifecycle::shutdown_within;
use crate::{
BatchSubscriber, Broker, Connected, IncomingMessage, OutgoingMessage, Positioned, Publisher,
Seekable, Seeker, SubscriptionSource,
};
pub async fn batches<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
) where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Send,
Src::Subscriber: BatchSubscriber + Send,
SubscriberMessage<Src::Subscriber>: 'static,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
const COUNT: u32 = 10;
const BATCH: NonZeroUsize = NonZeroUsize::new(3).unwrap();
let subject = unique_subject("conformance.batches");
let connected = within(make_broker().connect(), "batches: connect")
.await
.expect("broker must connect");
let mut subscriber = within(
make_source(&subject).subscribe(&connected),
"batches: subscribe",
)
.await
.expect("subscription must open after connect");
let publisher = make_publisher(&connected);
for i in 0..COUNT {
within(
publisher.publish(
OutgoingMessage::new(&subject, i.to_be_bytes().as_slice()),
None,
),
"batches: a publish",
)
.await
.expect("publish failed");
}
let mut received = Vec::new();
let mut stream = std::pin::pin!(subscriber.batches(BATCH));
while received.len() < COUNT as usize {
let batch = timeout(DEFAULT_TIMEOUT, stream.next())
.await
.expect("batches: stream timed out")
.expect("batches: stream ended unexpectedly")
.unwrap_or_else(|err| panic!("batches: stream yielded error: {err:?}"));
let batch: Vec<_> = batch.into_iter().collect();
assert!(!batch.is_empty(), "a yielded batch must not be empty");
assert!(
batch.len() <= BATCH.get(),
"a batch must never carry more than the size it was opened with: got {}, asked {}",
batch.len(),
BATCH,
);
for msg in batch {
received.push(msg.payload().to_vec());
ack_or_unsupported(msg, "batches").await;
}
}
let expected: Vec<Vec<u8>> = (0..COUNT).map(|i| i.to_be_bytes().to_vec()).collect();
assert_eq!(
received, expected,
"batched deliveries must preserve publish order across batches",
);
batch_settles_per_element(&publisher, &mut stream, &subject).await;
batch_settles_on_foreign_runtime(&publisher, &mut stream, &subject).await;
shutdown_within(connected, "batches").await;
}
async fn batch_settles_per_element<Pub, S, Batch, M, E>(
publisher: &Pub,
stream: &mut S,
subject: &str,
) where
Pub: Publisher,
S: Stream<Item = Result<Batch, E>> + Unpin,
Batch: IntoIterator<Item = M>,
M: IncomingMessage,
E: fmt::Debug,
{
const LABEL: &str = "batches: one batch settled element by element";
const ROUNDS: usize = 5;
for round in 0..ROUNDS {
let sent: Vec<Vec<u8>> = (0..3)
.map(|element| format!("element-{round}-{element}").into_bytes())
.collect();
let sent_refs: Vec<&[u8]> = sent.iter().map(Vec::as_slice).collect();
publish_all(publisher, subject, &sent_refs).await;
let batches = collect_batches(
stream,
sent.len(),
DEFAULT_TIMEOUT,
LABEL,
"every published message must arrive",
)
.await;
let delivered: Vec<Vec<u8>> = batches.iter().flat_map(|batch| payloads(batch)).collect();
assert_eq!(
delivered, sent,
"{LABEL}: batched deliveries must preserve publish order",
);
let target = batches
.iter()
.position(|batch| batch.len() > 1)
.map(|index| (index, (batches[index].len() - 1) / 2));
let mut requeued = None;
for (index, batch) in batches.into_iter().enumerate() {
for (element, msg) in batch.into_iter().enumerate() {
if target == Some((index, element)) {
let payload = msg.payload().to_vec();
if nack_requeue(msg, LABEL).await {
requeued = Some(payload);
}
} else {
ack_or_unsupported(msg, LABEL).await;
}
}
}
if target.is_none() {
continue;
}
if let Some(requeued) = requeued {
let again = collect_batched(
stream,
1,
REDELIVERY_TIMEOUT,
LABEL,
"the element nacked with requeue must come back",
)
.await;
assert_eq!(
payloads(&again),
[requeued],
"{LABEL}: only the element nacked with requeue may come back; the elements acked \
next to it must stay settled",
);
for msg in again {
ack_or_unsupported(msg, LABEL).await;
}
}
expect_no_batch(stream, LABEL).await;
return;
}
}
async fn batch_settles_on_foreign_runtime<Pub, S, Batch, M, E>(
publisher: &Pub,
stream: &mut S,
subject: &str,
) where
Pub: Publisher,
S: Stream<Item = Result<Batch, E>> + Unpin,
Batch: IntoIterator<Item = M>,
M: IncomingMessage + 'static,
E: fmt::Debug,
{
const LABEL: &str = "batches: a batch settled from a runtime that has since stopped";
publish_all(publisher, subject, &[b"foreign-requeue", b"foreign-ack"]).await;
let delivered = collect_batched(
stream,
2,
DEFAULT_TIMEOUT,
LABEL,
"every published message must arrive",
)
.await;
assert_eq!(
payloads(&delivered),
[b"foreign-requeue".to_vec(), b"foreign-ack".to_vec()],
"{LABEL}: batched deliveries must preserve publish order",
);
let requeued = on_foreign_runtime(async move || {
let [requeue, acked]: [M; 2] = delivered
.try_into()
.unwrap_or_else(|_| panic!("{LABEL}: exactly the two published messages must arrive"));
let requeued = nack_requeue(requeue, LABEL).await;
ack_or_unsupported(acked, LABEL).await;
requeued
})
.await;
if requeued {
let again = collect_batched(
stream,
1,
REDELIVERY_TIMEOUT,
LABEL,
"an element nacked with requeue from a runtime that has since stopped must still \
come back; the broker must run its internal tasks on the runtime it connected on",
)
.await;
assert_eq!(
payloads(&again),
[b"foreign-requeue".to_vec()],
"{LABEL}: only the element nacked with requeue may come back",
);
for msg in again {
ack_or_unsupported(msg, LABEL).await;
}
}
expect_no_batch(stream, LABEL).await;
}
pub async fn batch_seeking<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
) where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Send,
Src::Subscriber: BatchSubscriber + Seekable + Send,
SubscriberMessage<Src::Subscriber>: Positioned<Position = SeekPosition<Src::Subscriber>>,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
const LABEL: &str = "batch_seeking";
const BATCH: NonZeroUsize = NonZeroUsize::new(2).unwrap();
let subject = unique_subject("conformance.batch_seeking");
let connected = within(make_broker().connect(), "batch_seeking: connect")
.await
.expect("broker must connect");
let mut subscriber = within(
make_source(&subject).subscribe(&connected),
"batch_seeking: subscribe",
)
.await
.expect("subscription must open after connect");
let seeker = subscriber.seeker();
let publisher = make_publisher(&connected);
let sent: [&[u8]; 5] = [&[0], &[1], &[2], &[3], &[4]];
publish_all(&publisher, &subject, &sent).await;
let mut stream = std::pin::pin!(subscriber.batches(BATCH));
let delivered = collect_batched(
&mut stream,
sent.len(),
DEFAULT_TIMEOUT,
LABEL,
"every published message must arrive",
)
.await;
assert_eq!(
payloads(&delivered),
sent.map(<[u8]>::to_vec),
"{LABEL}: batched deliveries must arrive in publish order",
);
let back_to = delivered[1].position();
for msg in delivered {
ack_or_unsupported(msg, LABEL).await;
}
expect_no_batch(&mut stream, LABEL).await;
within(seeker.seek(back_to), "batch_seeking: a seek back")
.await
.unwrap_or_else(|err| panic!("{LABEL}: a seek back on a batch subscription failed: {err}"));
let replayed = collect_batched(
&mut stream,
sent.len() - 1,
DEFAULT_TIMEOUT,
LABEL,
"a seek back must make the next batches replay from the captured position",
)
.await;
assert_eq!(
payloads(&replayed),
sent[1..]
.iter()
.map(|payload| payload.to_vec())
.collect::<Vec<_>>(),
"{LABEL}: after a seek back the batches must start at the captured position and carry \
the ordered suffix after it",
);
for msg in replayed {
ack_or_unsupported(msg, LABEL).await;
}
expect_no_batch(&mut stream, LABEL).await;
shutdown_within(connected, LABEL).await;
}