use std::{fmt, future::Future, pin::pin, time::Duration};
use futures::{Stream, StreamExt};
use tokio::time::{Instant, timeout, timeout_at};
use super::harness::{InProcessBroker, on_foreign_runtime};
use super::helpers::unique_subject;
use crate::{
AckError, Broker, Connected, ConnectedBroker, IncomingMessage, OutgoingMessage, Publisher,
Subscriber, SubscriptionSource, testing::InProcess,
};
type SubscriberMessage<S> = <S as Subscriber>::Message;
const QUIET_MARGIN: Duration = Duration::from_secs(1);
const ARRIVAL: Duration = Duration::from_secs(10);
const STEP: Duration = Duration::from_secs(10);
const SENTINEL: &[u8] = b"conformance-sentinel";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Answer {
Settled,
Unsupported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Answers {
pub ack: Answer,
pub reject: Answer,
pub requeue: Answer,
pub delay: bool,
}
#[allow(clippy::future_not_send)]
pub async fn suite<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
redelivery_timeout: Duration,
) -> Answers
where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Clone + Send,
Src::Subscriber: Send,
MkSrc: Fn(&str) -> Src,
SubscriberMessage<Src::Subscriber>: 'static,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
Probes {
make_broker: &make_broker,
make_source: &make_source,
make_publisher: &make_publisher,
redelivery_timeout,
run: "",
}
.answers()
.await
}
#[allow(clippy::future_not_send)]
pub async fn matches_in_process<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
redelivery_timeout: Duration,
) where
B: InProcess,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Clone + Send,
Src::Subscriber: Send,
MkSrc: Fn(&str) -> Src,
SubscriberMessage<Src::Subscriber>: 'static,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
let live = Probes {
make_broker: &make_broker,
make_source: &make_source,
make_publisher: &make_publisher,
redelivery_timeout,
run: "live ",
}
.answers()
.await;
let in_process = Probes {
make_broker: &|| InProcessBroker::new(make_broker()),
make_source: &make_source,
make_publisher: &make_publisher,
redelivery_timeout,
run: "in process ",
}
.answers()
.await;
assert_eq!(
in_process, live,
"settlement: the in-process transport answers settlements differently from the server \
(left: in process, right: live); a test on it passes where production fails",
);
}
#[derive(Debug, Clone, Copy)]
enum Consume {
Ack,
Reject,
}
impl Consume {
const fn label(self) -> &'static str {
match self {
Self::Ack => "settlement ack consumes",
Self::Reject => "settlement nack(false) drops",
}
}
}
struct Probes<'a, MkBroker, MkSrc, MkPub> {
make_broker: &'a MkBroker,
make_source: &'a MkSrc,
make_publisher: &'a MkPub,
redelivery_timeout: Duration,
run: &'static str,
}
#[allow(clippy::future_not_send)]
impl<B, MkBroker, Src, MkSrc, Pub, MkPub> Probes<'_, MkBroker, MkSrc, MkPub>
where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Clone + Send,
Src::Subscriber: Send,
MkSrc: Fn(&str) -> Src,
SubscriberMessage<Src::Subscriber>: 'static,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
async fn answers(&self) -> Answers {
let (ack, delay) = self.consumed_by(Consume::Ack).await;
let (reject, _) = self.consumed_by(Consume::Reject).await;
let requeue = self.requeue_returns().await;
if requeue == Answer::Settled {
self.out_of_order_is_safe().await;
self.unsettled_drop_returns().await;
}
Answers {
ack,
reject,
requeue,
delay,
}
}
fn label(&self, check: &str) -> String {
format!("{}{check}", self.run)
}
async fn open(&self, source: &Src, label: &str) -> (Connected<B>, Src::Subscriber) {
let connected = bounded((self.make_broker)().connect(), label, "connect")
.await
.unwrap_or_else(|err| panic!("{label}: the broker must connect, got: {err:?}"));
let subscriber = subscribe(source, &connected, label).await;
(connected, subscriber)
}
async fn close(connected: Connected<B>, subscriber: Src::Subscriber, label: &str) {
drop(subscriber);
shutdown(connected, label).await;
}
async fn consumed_by(&self, how: Consume) -> (Answer, bool) {
let label = &self.label(how.label());
let subject = unique_subject("conformance.settlement.consume");
let source = (self.make_source)(&subject);
let (connected, mut subscriber) = self.open(&source, label).await;
publish(
&(self.make_publisher)(&connected),
&subject,
b"settled",
label,
)
.await;
let (answer, delay) = {
let mut stream = pin!(subscriber.stream());
let msg = arrival(&mut stream, ARRIVAL, label).await;
let delay = msg.supports_nack_after();
let answer = match how {
Consume::Ack => settle(msg.ack(), label, "ack").await,
Consume::Reject => settle(msg.nack(false), label, "nack(false)").await,
};
if answer == Answer::Settled {
quiet(
&mut stream,
self.redelivery_timeout + QUIET_MARGIN,
label,
"the settled delivery came back on its subscription",
)
.await;
}
(answer, delay)
};
Self::close(connected, subscriber, label).await;
if answer == Answer::Unsupported {
return (answer, delay);
}
let (connected, mut subscriber) = self.open(&source, label).await;
publish(
&(self.make_publisher)(&connected),
&subject,
SENTINEL,
label,
)
.await;
{
let mut stream = pin!(subscriber.stream());
until_sentinel(&mut stream, self.redelivery_timeout, label).await;
}
Self::close(connected, subscriber, label).await;
(answer, delay)
}
async fn requeue_returns(&self) -> Answer {
let label = &self.label("settlement nack(true) returns");
let subject = unique_subject("conformance.settlement.requeue");
let source = (self.make_source)(&subject);
let (connected, mut subscriber) = self.open(&source, label).await;
publish(
&(self.make_publisher)(&connected),
&subject,
b"requeued",
label,
)
.await;
let answer = {
let mut stream = pin!(subscriber.stream());
let msg = arrival(&mut stream, ARRIVAL, label).await;
let answer = settle(msg.nack(true), label, "nack(true)").await;
if answer == Answer::Settled {
let again = arrival(&mut stream, self.redelivery_timeout + ARRIVAL, label).await;
assert_eq!(
again.payload(),
b"requeued",
"{label}: nack(true) returned Ok, so the same delivery must come back",
);
settle(again.ack(), label, "ack").await;
}
answer
};
Self::close(connected, subscriber, label).await;
answer
}
async fn out_of_order_is_safe(&self) {
let label = &self.label("settlement out of order");
let subject = unique_subject("conformance.settlement.order");
let source = (self.make_source)(&subject);
let (connected, mut subscriber) = self.open(&source, label).await;
let publisher = (self.make_publisher)(&connected);
for payload in [b"first".as_slice(), b"second", b"third"] {
publish(&publisher, &subject, payload, label).await;
}
drop(publisher);
let (mut missing, acked) = {
let mut stream = pin!(subscriber.stream());
let mut held = Vec::with_capacity(2);
for _ in 0..2 {
held.push(arrival(&mut stream, ARRIVAL, label).await);
}
let last = arrival(&mut stream, ARRIVAL, label).await;
let acked = last.payload().to_vec();
settle(last.ack(), label, "ack").await;
let missing: Vec<Vec<u8>> = held.iter().map(|msg| msg.payload().to_vec()).collect();
drop(held);
(missing, acked)
};
self.expect_back(
connected,
subscriber,
&source,
&mut missing,
Some(&acked),
label,
)
.await;
}
async fn unsettled_drop_returns(&self) {
let label = &self.label("settlement unsettled drop");
let subject = unique_subject("conformance.settlement.unsettled");
let source = (self.make_source)(&subject);
let (connected, mut subscriber) = self.open(&source, label).await;
publish(
&(self.make_publisher)(&connected),
&subject,
b"unsettled",
label,
)
.await;
{
let mut stream = pin!(subscriber.stream());
let msg = arrival(&mut stream, ARRIVAL, label).await;
on_foreign_runtime(async move || drop(msg)).await;
}
let mut missing = vec![b"unsettled".to_vec()];
self.expect_back(connected, subscriber, &source, &mut missing, None, label)
.await;
}
async fn expect_back(
&self,
connected: Connected<B>,
mut subscriber: Src::Subscriber,
source: &Src,
missing: &mut Vec<Vec<u8>>,
tolerated: Option<&[u8]>,
label: &str,
) {
{
let mut stream = pin!(subscriber.stream());
collect_back(
&mut stream,
missing,
tolerated,
self.redelivery_timeout + QUIET_MARGIN,
label,
)
.await;
}
drop(subscriber);
if !missing.is_empty() {
let mut subscriber = subscribe(source, &connected, label).await;
{
let mut stream = pin!(subscriber.stream());
collect_back(
&mut stream,
missing,
tolerated,
self.redelivery_timeout + ARRIVAL,
label,
)
.await;
}
drop(subscriber);
}
shutdown(connected, label).await;
if missing.is_empty() {
return;
}
let (connected, mut subscriber) = self.open(source, label).await;
{
let mut stream = pin!(subscriber.stream());
collect_back(
&mut stream,
missing,
tolerated,
self.redelivery_timeout + ARRIVAL,
label,
)
.await;
}
Self::close(connected, subscriber, label).await;
assert!(
missing.is_empty(),
"{label}: deliveries released without a settlement never came back, neither on their \
subscription, nor on it opened again, nor on a new connection to it; the broker \
consumed them without an ack. Lost: {:?}",
missing
.iter()
.map(|payload| String::from_utf8_lossy(payload).into_owned())
.collect::<Vec<_>>(),
);
}
}
async fn bounded<Work: Future>(work: Work, label: &str, what: &str) -> Work::Output {
timeout(STEP, work)
.await
.unwrap_or_else(|_| panic!("{label}: {what} hung for {STEP:?}"))
}
#[allow(clippy::future_not_send)]
async fn subscribe<C, Src>(source: &Src, connected: &C, label: &str) -> Src::Subscriber
where
C: ConnectedBroker,
Src: SubscriptionSource<C> + Clone,
{
bounded(source.clone().subscribe(connected), label, "subscribing")
.await
.unwrap_or_else(|err| panic!("{label}: the subscription must open, got: {err:?}"))
}
async fn shutdown<C: ConnectedBroker>(connected: C, label: &str) {
if let Err(err) = bounded(connected.shutdown(), label, "shutdown").await {
panic!("{label}: the broker must shut down cleanly, got: {err:?}");
}
}
async fn publish<Pub: Publisher>(publisher: &Pub, subject: &str, payload: &[u8], label: &str) {
let publishing = publisher.publish(OutgoingMessage::new(subject, payload), None);
if let Err(err) = bounded(publishing, label, "publish").await {
panic!("{label}: the publish must succeed, got: {err:?}");
}
}
async fn settle(
settling: impl Future<Output = Result<(), AckError>>,
label: &str,
what: &str,
) -> Answer {
match bounded(settling, label, what).await {
Ok(()) => Answer::Settled,
Err(AckError::Unsupported) => Answer::Unsupported,
Err(other) => panic!("{label}: {what} must succeed or be unsupported, got: {other:?}"),
}
}
fn delivered<M, E: fmt::Debug>(item: Option<Result<M, E>>, label: &str) -> M {
item.unwrap_or_else(|| panic!("{label}: the subscription stream ended"))
.unwrap_or_else(|err| panic!("{label}: the subscription stream failed: {err:?}"))
}
async fn arrival<S, M, E>(stream: &mut S, within: Duration, label: &str) -> M
where
S: Stream<Item = Result<M, E>> + Unpin,
E: fmt::Debug,
{
let item = timeout(within, stream.next())
.await
.unwrap_or_else(|_| panic!("{label}: no delivery arrived within {within:?}"));
delivered(item, label)
}
async fn quiet<S, M, E>(stream: &mut S, within: Duration, label: &str, what: &str)
where
S: Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
if let Ok(item) = timeout(within, stream.next()).await {
let msg = delivered(item, label);
panic!(
"{label}: {what} within {within:?}: {:?}",
String::from_utf8_lossy(msg.payload()),
);
}
}
async fn until_sentinel<S, M, E>(stream: &mut S, redelivery_timeout: Duration, label: &str)
where
S: Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
const CAME_BACK: &str =
"the settled delivery came back on a new connection to its subscription";
let msg = arrival(stream, redelivery_timeout + ARRIVAL, label).await;
assert!(
msg.payload() == SENTINEL,
"{label}: {CAME_BACK}: {:?}",
String::from_utf8_lossy(msg.payload()),
);
settle(msg.ack(), label, "ack").await;
quiet(stream, redelivery_timeout + QUIET_MARGIN, label, CAME_BACK).await;
}
async fn collect_back<S, M, E>(
stream: &mut S,
missing: &mut Vec<Vec<u8>>,
tolerated: Option<&[u8]>,
within: Duration,
label: &str,
) where
S: Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
let deadline = Instant::now() + within;
while !missing.is_empty() {
let Ok(item) = timeout_at(deadline, stream.next()).await else {
return;
};
let msg = delivered(item, label);
let payload = msg.payload();
if let Some(found) = missing.iter().position(|expected| expected == payload) {
missing.swap_remove(found);
} else {
assert!(
tolerated == Some(payload),
"{label}: an unexpected delivery arrived while waiting for the unsettled ones: \
{:?}",
String::from_utf8_lossy(payload),
);
}
settle(msg.ack(), label, "ack").await;
}
}
#[cfg(all(test, feature = "memory"))]
mod tests;