use std::{
fmt,
future::Future,
pin::pin,
sync::{
Arc,
mpsc::{self, RecvTimeoutError},
},
thread,
time::Duration,
};
use futures::{Stream, StreamExt};
use tokio::time::{Instant as TokioInstant, timeout, timeout_at};
use super::harness::{expect_next, on_foreign_runtime};
use super::helpers::unique_subject;
use crate::{
AckError, Broker, Connected, ConnectedBroker, IncomingMessage, OutgoingMessage, Publisher,
Subscriber, SubscriptionSource, testing::Backlog,
};
const STEP_TIMEOUT: Duration = Duration::from_secs(2);
pub(crate) const CONSTRUCTION_BUDGET: Duration = Duration::from_secs(1);
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
pub(crate) const REDELIVERY_DELAY: Duration = Duration::from_millis(1500);
const REDELIVERY_TIMEOUT: Duration = Duration::from_secs(10);
const NEGATIVE_WAIT: Duration = Duration::from_millis(100);
const FIRST: &[u8] = b"lifecycle";
const FROM_BROKER_RUNTIME: &[u8] = b"lifecycle.again.broker-runtime";
const FROM_NEW_RUNTIME: &[u8] = b"lifecycle.again.new-runtime";
const HELD: &[u8] = b"lifecycle.held";
const ACKED: &[u8] = b"flush.acked";
const HELD_ACK: &[u8] = b"flush.held";
const FLUSHED: &[u8] = b"flush.published";
type SubscriberMessage<S> = <S as Subscriber>::Message;
pub(crate) async fn ladder<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
) where
B: Broker + Send + 'static,
MkBroker: Fn() -> B + Send + Sync + 'static,
Src: SubscriptionSource<Connected<B>> + Clone + Send + 'static,
Src::Subscriber: Send + 'static,
MkSrc: Fn(&str) -> Src,
SubscriberMessage<Src::Subscriber>: 'static,
Pub: Publisher + 'static,
MkPub: Fn(&Connected<B>) -> Pub,
{
let subject = unique_subject("conformance.lifecycle");
let make_broker = Arc::new(make_broker);
let broker = construct_off_runtime(Arc::clone(&make_broker));
let make_broker = move || make_broker();
let connected = Arc::new(connect_within(broker, "lifecycle").await);
let source = make_source(&subject);
let opening = Arc::clone(&connected);
let opened =
on_foreign_runtime(async move || timeout(STEP_TIMEOUT, source.subscribe(&opening)).await)
.await;
let mut subscriber = opened
.unwrap_or_else(|_| {
panic!(
"lifecycle: subscribing to `{subject}` from a runtime of its own hung for \
{STEP_TIMEOUT:?}"
)
})
.unwrap_or_else(|err| {
panic!(
"lifecycle: the subscription to `{subject}` must open from a runtime of its own, \
got: {err:?}"
)
});
let foreign = make_publisher(&connected);
let native = make_publisher(&connected);
let untouched = make_publisher(&connected);
let (foreign, outcome) = publish_on_foreign(foreign, &subject, FIRST).await;
outcome.unwrap_or_else(|err| panic!("lifecycle: publish after connect failed: {err}"));
let mut stream = pin!(subscriber.stream());
let msg = expect_payload(
&mut stream,
FIRST,
"lifecycle: a subscription opened from a runtime that stopped must keep receiving",
)
.await;
let msg = redeliver_after_delay(&mut stream, msg).await;
requeue_from_foreign(&mut stream, msg).await;
let foreign = foreign_after_stop(foreign, &subject).await;
receive_again(&mut stream).await;
publish_within(&native, &subject, HELD)
.await
.unwrap_or_else(|err| panic!("lifecycle: publish from the broker's runtime failed: {err}"));
let held = expect_payload(&mut stream, HELD, "lifecycle: the held delivery").await;
let connected = Arc::into_inner(connected)
.expect("every runtime the connected form was shared with has stopped");
shutdown_within(connected, "lifecycle").await;
for (publisher, which) in [
(
&untouched,
"a publisher paired before shutdown and never used",
),
(&native, "a publisher used only on the broker's runtime"),
(
&foreign,
"a publisher used from runtimes that have since stopped",
),
] {
let answer = within(
publisher.publish(
OutgoingMessage::new(&subject, b"post-shutdown".as_slice()),
None,
),
&format!(
"lifecycle: publish after shutdown through {which} (a handle aliasing the closed \
connection must error)"
),
)
.await;
assert!(
answer.is_err(),
"lifecycle: publish after shutdown through {which} succeeded; a handle aliasing the \
closed connection must error",
);
}
held_across_shutdown(&mut stream, held, &subject, make_broker, make_source).await;
}
fn construct_off_runtime<B, MkBroker>(make_broker: Arc<MkBroker>) -> B
where
B: Broker + Send + 'static,
MkBroker: Fn() -> B + Send + Sync + 'static,
{
let (built, returned) = mpsc::channel();
let constructor = thread::Builder::new()
.name("conformance-no-runtime".to_owned())
.spawn(move || {
let broker = make_broker();
let _ = built.send(());
broker
})
.expect("the thread with no runtime must start");
assert!(
returned.recv_timeout(CONSTRUCTION_BUDGET) != Err(RecvTimeoutError::Timeout),
"lifecycle: the broker constructor did not return within {CONSTRUCTION_BUDGET:?}; a \
constructor only records configuration and returns at once, while waiting on the \
network belongs in `connect`"
);
constructor.join().unwrap_or_else(|_| {
panic!(
"lifecycle: the broker constructor panicked on a thread with no Tokio runtime (its \
message is above); a constructor only records configuration: no spawn, no \
`Handle::current`, no I/O, all of which belong in `connect`"
)
})
}
async fn redeliver_after_delay<S, M, E>(stream: &mut S, msg: M) -> M
where
S: Stream<Item = Result<M, E>> + Unpin + Send,
M: IncomingMessage + 'static,
E: fmt::Debug,
{
if !msg.supports_nack_after() {
return msg;
}
let settled_at = TokioInstant::now();
let answer = on_foreign_runtime(async move || {
timeout(STEP_TIMEOUT, msg.nack_after(REDELIVERY_DELAY))
.await
.ok()
})
.await;
match answer {
Some(Ok(())) => {}
Some(Err(err)) => panic!(
"lifecycle: the delivery offers nack_after, so a delayed nack must be accepted, got: \
{err:?}"
),
None => panic!("lifecycle: nack_after from a runtime of its own hung for {STEP_TIMEOUT:?}"),
}
let again = deliver_until(
stream,
settled_at + REDELIVERY_DELAY + REDELIVERY_TIMEOUT,
"lifecycle: a delayed nack settled from another runtime never came back; the broker must \
run its internal tasks on the runtime it connected on",
"lifecycle: waiting for the delayed redelivery",
)
.await;
let elapsed = settled_at.elapsed();
assert!(
elapsed >= REDELIVERY_DELAY,
"lifecycle: nack_after({REDELIVERY_DELAY:?}) came back after {elapsed:?}; a broker that \
ignores the delay or rounds it down redelivers before it runs out",
);
assert_eq!(
again.payload(),
FIRST,
"lifecycle: a delayed nack must redeliver the same message",
);
again
}
async fn requeue_from_foreign<S, M, E>(stream: &mut S, msg: M)
where
S: Stream<Item = Result<M, E>> + Unpin + Send,
M: IncomingMessage + 'static,
E: fmt::Debug,
{
if !settle_on_foreign(msg, Settle::Requeue).await {
return;
}
let again = deliver_until(
stream,
TokioInstant::now() + REDELIVERY_TIMEOUT,
&format!(
"lifecycle: nack(requeue = true) from a runtime that stopped answered Ok, and the \
message never came back within {REDELIVERY_TIMEOUT:?}; Ok promises a redelivery, and \
the requeue runs on the runtime the broker connected on"
),
"lifecycle: waiting for the requeued message",
)
.await;
assert_eq!(
again.payload(),
FIRST,
"lifecycle: nack(requeue = true) must redeliver the same message",
);
settle_on_foreign(again, Settle::Ack).await;
}
async fn foreign_after_stop<Pub: Publisher + 'static>(publisher: Pub, subject: &str) -> Pub {
let from_broker = publish_within(&publisher, subject, FROM_BROKER_RUNTIME).await;
let (publisher, from_new) = publish_on_foreign(publisher, subject, FROM_NEW_RUNTIME).await;
for (outcome, runtime) in [
(from_broker, "the broker's runtime"),
(from_new, "a new runtime"),
] {
outcome.unwrap_or_else(|err| {
panic!(
"lifecycle: publish from {runtime}, through a publisher first used on a runtime \
that has since stopped, failed: {err}"
)
});
}
publisher
}
async fn receive_again<S, M, E>(stream: &mut S)
where
S: Stream<Item = Result<M, E>> + Unpin + Send,
M: IncomingMessage + 'static,
E: fmt::Debug,
{
let deadline = TokioInstant::now() + REDELIVERY_TIMEOUT;
let mut pending = vec![FROM_BROKER_RUNTIME, FROM_NEW_RUNTIME];
while !pending.is_empty() {
let missing: Vec<_> = pending
.iter()
.map(|payload| String::from_utf8_lossy(payload))
.collect();
let msg = deliver_until(
stream,
deadline,
&format!(
"lifecycle: {missing:?} never arrived; a publisher whose first publish came from a \
runtime that stopped must keep publishing from any runtime (a lazily attached \
connection must live on the broker's runtime)"
),
"lifecycle: waiting for the repeated publishes",
)
.await;
let Some(at) = pending.iter().position(|payload| *payload == msg.payload()) else {
panic!(
"lifecycle: expected the repeated publishes, got {:?} (a settled delivery came \
back, or one arrived twice)",
String::from_utf8_lossy(msg.payload()),
);
};
let how = if pending.swap_remove(at) == FROM_BROKER_RUNTIME {
Settle::Drop
} else {
Settle::Ack
};
settle_on_foreign(msg, how).await;
}
expect_quiet(
stream,
"lifecycle: a delivery dropped with nack(requeue = false) from a runtime that stopped came \
back",
)
.await;
}
async fn held_across_shutdown<S, M, E, B, MkBroker, Src, MkSrc>(
stream: &mut S,
held: M,
subject: &str,
make_broker: MkBroker,
make_source: MkSrc,
) where
S: Stream<Item = Result<M, E>> + Unpin + Send,
M: IncomingMessage,
E: fmt::Debug,
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>>,
MkSrc: Fn(&str) -> Src,
{
let answer = within(
held.nack(true),
"lifecycle: nack(requeue = true) on a delivery held across shutdown (it must error or take \
effect)",
)
.await;
if answer.is_err() {
return;
}
let deadline = TokioInstant::now() + REDELIVERY_TIMEOUT;
while let Next::Delivered(msg) = next_until(stream, deadline).await {
if msg.payload() == HELD {
return;
}
}
let fresh = connect_within(make_broker(), "lifecycle").await;
let mut subscriber = within(
make_source(subject).subscribe(&fresh),
"lifecycle: subscribing on a new connection",
)
.await
.expect("lifecycle: the subscription source must open on a new connection");
let found = {
let mut stream = pin!(subscriber.stream());
let deadline = TokioInstant::now() + REDELIVERY_TIMEOUT;
loop {
match next_until(&mut stream, deadline).await {
Next::Delivered(msg) => {
let found = msg.payload() == HELD;
let _ = timeout(STEP_TIMEOUT, msg.ack()).await;
if found {
break true;
}
}
Next::Ended | Next::Failed(_) | Next::TimedOut => break false,
}
}
};
assert!(
found,
"lifecycle: a delivery held across shutdown answered Ok to nack(requeue = true) and never \
came back, on its own subscription or on a new connection; a settlement after shutdown \
must error or take effect",
);
drop(subscriber);
shutdown_within(fresh, "lifecycle").await;
}
pub async fn shutdown_flushes<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
backlog: Backlog,
) where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>>,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
let subject = unique_subject("conformance.flush");
let observer = match backlog {
Backlog::Missed => Some(connect_and_subscribe(make_broker(), make_source(&subject)).await),
Backlog::Delivered => None,
};
let (connected, mut subscriber) =
connect_and_subscribe(make_broker(), make_source(&subject)).await;
let publisher = make_publisher(&connected);
let (acked_ok, held) = {
let mut stream = pin!(subscriber.stream());
publish_within(&publisher, &subject, ACKED)
.await
.unwrap_or_else(|err| panic!("shutdown_flushes: publish failed: {err}"));
let acked = expect_payload(&mut stream, ACKED, "shutdown_flushes").await;
let acked_ok = performed(
within(acked.ack(), "shutdown_flushes: ack").await,
"shutdown_flushes: ack",
);
publish_within(&publisher, &subject, HELD_ACK)
.await
.unwrap_or_else(|err| panic!("shutdown_flushes: publish failed: {err}"));
let held = expect_payload(&mut stream, HELD_ACK, "shutdown_flushes").await;
(acked_ok, held)
};
publish_within(&publisher, &subject, FLUSHED)
.await
.unwrap_or_else(|err| panic!("shutdown_flushes: publish failed: {err}"));
shutdown_within(connected, "shutdown_flushes").await;
let held_ok = within(
held.ack(),
"shutdown_flushes: ack of a delivery held across shutdown (it must error or take effect)",
)
.await
.is_ok();
drop(subscriber);
let (reader, mut subscriber) = match observer {
Some(observer) => observer,
None => connect_and_subscribe(make_broker(), make_source(&subject)).await,
};
{
let mut stream = pin!(subscriber.stream());
let reader = match backlog {
Backlog::Delivered => "a new connection (the broker declares a backlog)",
Backlog::Missed => "a subscription open on another connection",
};
let lost = format!(
"shutdown_flushes: a message published right before shutdown never reached {reader}; \
shutdown must flush the publishes handed to it"
);
let deadline = TokioInstant::now() + REDELIVERY_TIMEOUT;
let mut flushed = false;
loop {
let msg = match next_until(&mut stream, deadline).await {
Next::TimedOut if flushed => break,
next => delivered(
next,
&lost,
"shutdown_flushes: reading from another connection",
),
};
let payload = msg.payload().to_vec();
if backlog == Backlog::Delivered {
assert!(
!(payload == ACKED && acked_ok),
"shutdown_flushes: a message acknowledged right before shutdown came back on \
a new connection; shutdown must flush the acknowledgements handed to it",
);
assert!(
!(payload == HELD_ACK && held_ok),
"shutdown_flushes: a delivery acknowledged after shutdown answered Ok and \
came back on a new connection; a settlement after shutdown must error or \
take effect",
);
}
let _ = timeout(STEP_TIMEOUT, msg.ack()).await;
if payload == FLUSHED {
if backlog == Backlog::Missed {
break;
}
flushed = true;
}
}
}
drop(subscriber);
shutdown_within(reader, "shutdown_flushes").await;
}
pub async fn shared_handle_closes<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
) where
B: Broker,
Connected<B>: Clone,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>>,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
let subject = unique_subject("conformance.shared");
let connected = connect_within(make_broker(), "shared_handle_closes").await;
let alias = connected.clone();
let early = make_publisher(&alias);
shutdown_within(connected, "shared_handle_closes").await;
let late = make_publisher(&alias);
for (publisher, which) in [
(&early, "a publisher paired from the clone before shutdown"),
(&late, "a publisher paired from the clone after shutdown"),
] {
match publish_within(publisher, &subject, b"post-shutdown").await {
Err(PublishFailure::Refused(_)) => {}
Ok(()) => panic!(
"shared_handle_closes: publish through {which} succeeded after the original shut \
down; every clone of the connected form must error"
),
Err(PublishFailure::Hung) => panic!(
"shared_handle_closes: publish through {which} hung for {STEP_TIMEOUT:?} after the \
original shut down; every clone of the connected form must error"
),
}
}
let opened = timeout(STEP_TIMEOUT, make_source(&subject).subscribe(&alias))
.await
.unwrap_or_else(|_| {
panic!(
"shared_handle_closes: subscribing through the clone hung for {STEP_TIMEOUT:?} \
after the original shut down; it must error"
)
});
if let Ok(mut subscriber) = opened {
let mut stream = pin!(subscriber.stream());
assert!(
matches!(
next_until(&mut stream, TokioInstant::now() + STEP_TIMEOUT).await,
Next::Ended | Next::Failed(_)
),
"shared_handle_closes: a subscription opened through the clone after the original shut \
down stayed open (silent, or delivering); it must be refused or end",
);
}
}
async fn connect_and_subscribe<B, Src>(broker: B, source: Src) -> (Connected<B>, Src::Subscriber)
where
B: Broker,
Src: SubscriptionSource<Connected<B>>,
{
let connected = connect_within(broker, "shutdown_flushes").await;
let subject = source.name().to_owned();
let subscriber = within(
source.subscribe(&connected),
"shutdown_flushes: subscribing",
)
.await
.unwrap_or_else(|err| {
panic!("shutdown_flushes: the subscription to `{subject}` must open, got: {err:?}")
});
(connected, subscriber)
}
async fn within<Work: Future>(work: Work, what: &str) -> Work::Output {
timeout(STEP_TIMEOUT, work)
.await
.unwrap_or_else(|_| panic!("{what} hung for {STEP_TIMEOUT:?}"))
}
async fn connect_within<B: Broker>(broker: B, scenario: &str) -> Connected<B> {
timeout(CONNECT_TIMEOUT, broker.connect())
.await
.unwrap_or_else(|_| panic!("{scenario}: connect hung for {CONNECT_TIMEOUT:?}"))
.unwrap_or_else(|err| panic!("{scenario}: the broker must connect, got: {err:?}"))
}
pub(crate) async fn shutdown_within<C: ConnectedBroker>(connected: C, scenario: &str) {
let answer = timeout(SHUTDOWN_TIMEOUT, connected.shutdown())
.await
.unwrap_or_else(|_| {
panic!(
"{scenario}: shutdown hung for {SHUTDOWN_TIMEOUT:?}; it must finish its teardown \
and return"
)
});
if let Err(err) = answer {
panic!("{scenario}: the broker must shut down cleanly, got: {err:?}");
}
}
#[derive(Debug)]
enum PublishFailure {
Refused(String),
Hung,
}
impl fmt::Display for PublishFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Refused(err) => write!(f, "{err}"),
Self::Hung => write!(f, "the publish hung for {STEP_TIMEOUT:?}"),
}
}
}
async fn publish_within<Pub: Publisher>(
publisher: &Pub,
subject: &str,
payload: &[u8],
) -> Result<(), PublishFailure> {
match timeout(
STEP_TIMEOUT,
publisher.publish(OutgoingMessage::new(subject, payload), None),
)
.await
{
Ok(Ok(())) => Ok(()),
Ok(Err(err)) => Err(PublishFailure::Refused(format!("{err:?}"))),
Err(_) => Err(PublishFailure::Hung),
}
}
async fn publish_on_foreign<Pub: Publisher + 'static>(
publisher: Pub,
subject: &str,
payload: &'static [u8],
) -> (Pub, Result<(), PublishFailure>) {
let subject = subject.to_owned();
on_foreign_runtime(async move || {
let outcome = publish_within(&publisher, &subject, payload).await;
(publisher, outcome)
})
.await
}
#[derive(Debug, Clone, Copy)]
enum Settle {
Ack,
Requeue,
Drop,
}
async fn settle_on_foreign<M: IncomingMessage + 'static>(msg: M, how: Settle) -> bool {
let what = match how {
Settle::Ack => "lifecycle: ack",
Settle::Requeue => "lifecycle: nack(requeue = true)",
Settle::Drop => "lifecycle: nack(requeue = false)",
};
let answer = on_foreign_runtime(async move || {
match how {
Settle::Ack => timeout(STEP_TIMEOUT, msg.ack()).await,
Settle::Requeue => timeout(STEP_TIMEOUT, msg.nack(true)).await,
Settle::Drop => timeout(STEP_TIMEOUT, msg.nack(false)).await,
}
.ok()
})
.await;
let answer = answer
.unwrap_or_else(|| panic!("{what} from a runtime of its own hung for {STEP_TIMEOUT:?}"));
performed(answer, what)
}
fn performed(answer: Result<(), AckError>, what: &str) -> bool {
match answer {
Ok(()) => true,
Err(AckError::Unsupported) => false,
Err(other) => panic!("{what} must succeed or be unsupported, got: {other:?}"),
}
}
enum Next<M> {
Delivered(M),
Ended,
Failed(String),
TimedOut,
}
fn delivered<M>(next: Next<M>, timed_out: &str, waiting_for: &str) -> M {
match next {
Next::Delivered(msg) => msg,
Next::TimedOut => panic!("{timed_out}"),
Next::Ended => panic!("{waiting_for}: the stream ended"),
Next::Failed(err) => panic!("{waiting_for}: the stream yielded an error: {err}"),
}
}
async fn deliver_until<S, M, E>(
stream: &mut S,
deadline: TokioInstant,
timed_out: &str,
waiting_for: &str,
) -> M
where
S: Stream<Item = Result<M, E>> + Unpin + Send,
E: fmt::Debug,
{
delivered(next_until(stream, deadline).await, timed_out, waiting_for)
}
async fn next_until<S, M, E>(stream: &mut S, deadline: TokioInstant) -> Next<M>
where
S: Stream<Item = Result<M, E>> + Unpin + Send,
E: fmt::Debug,
{
match timeout_at(deadline, stream.next()).await {
Ok(Some(Ok(msg))) => Next::Delivered(msg),
Ok(Some(Err(err))) => Next::Failed(format!("{err:?}")),
Ok(None) => Next::Ended,
Err(_) => Next::TimedOut,
}
}
async fn expect_payload<S, M, E>(stream: &mut S, payload: &[u8], scenario: &str) -> M
where
S: Stream<Item = Result<M, E>> + Unpin + Send,
M: IncomingMessage,
E: fmt::Debug,
{
let msg = expect_next(stream, scenario).await;
assert_eq!(
msg.payload(),
payload,
"{scenario}: expected {:?}, got {:?}",
String::from_utf8_lossy(payload),
String::from_utf8_lossy(msg.payload()),
);
msg
}
async fn expect_quiet<S, M, E>(stream: &mut S, scenario: &str)
where
S: Stream<Item = Result<M, E>> + Unpin + Send,
E: fmt::Debug,
{
if let Next::Delivered(_) = next_until(stream, TokioInstant::now() + NEGATIVE_WAIT).await {
panic!("{scenario}");
}
}
#[cfg(all(test, feature = "memory"))]
mod tests;