#![cfg(all(
feature = "macros",
feature = "memory",
feature = "json",
feature = "testing"
))]
mod common;
use std::convert::Infallible;
use std::future::{Future, ready};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use common::Order;
use futures::{Stream, StreamExt};
use ruststream::memory::{
ConnectedMemoryBroker, MemoryBroker, MemoryError, MemoryMessage, MemoryPublish,
MemoryPublisher, MemorySubscriber,
};
use ruststream::runtime::{
AppInfo, ForReply, HandlerOutcome, Names, Outgoing, PublishContext, PublishTransform,
RETRY_COUNT_HEADER, Reads, Router, RustStream, State,
};
use ruststream::testing::TestApp;
use ruststream::{
AckError, AddressedCopies, BrokerMoves, FromRef, HeaderMap, IncomingMessage, NamedCopies,
OutgoingMessage, Publisher, RedeliveryAddress, RedeliveryAddressed, RetryDeclaration,
Subscribe, Subscriber, SubscriptionSource, nonzero, subscriber,
};
use serde::Serialize;
const RETRY_DELAY: Duration = Duration::from_secs(5);
#[derive(Debug, Serialize)]
struct Routed {
#[serde(rename = "x-topic")]
topic: &'static str,
}
#[derive(Debug, Serialize)]
struct Retried {
#[serde(rename = "x-ruststream-retry-count")]
retries: u64,
}
#[derive(Debug, Clone)]
struct Queue {
name: &'static str,
}
impl<C: Subscribe> SubscriptionSource<C> for Queue {
type Subscriber = ShapedSubscriber<C::Subscriber>;
type Copies = AddressedCopies;
fn name(&self) -> &str {
self.name
}
async fn subscribe(self, connected: &C) -> Result<Self::Subscriber, C::Error> {
Ok(ShapedSubscriber(connected.subscribe(self.name).await?))
}
}
impl<C: Subscribe> RedeliveryAddressed<C> for Queue {
fn redelivery_address(
&self,
_connected: &C,
) -> impl Future<Output = Result<RedeliveryAddress, C::Error>> + Send {
ready(Ok(RedeliveryAddress::new(self.name)))
}
}
struct ShapedSubscriber<S>(S);
impl<S: Subscriber> Subscriber for ShapedSubscriber<S> {
type Message = ShapedMessage<S::Message>;
type Error = S::Error;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
self.0.stream().map(|item| item.map(ShapedMessage))
}
}
struct ShapedMessage<M>(M);
impl<M: IncomingMessage> IncomingMessage for ShapedMessage<M> {
fn payload(&self) -> &[u8] {
self.0.payload()
}
fn headers(&self) -> &HeaderMap {
self.0.headers()
}
async fn ack(self) -> Result<(), AckError> {
self.0.ack().await
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
self.0.nack(requeue).await
}
}
const DELIVERY_COUNT: &str = "x-delivery-count";
#[derive(Debug, Serialize)]
struct Delivered {
#[serde(rename = "x-delivery-count")]
count: u64,
}
#[derive(Debug, Clone)]
struct CountedQueue {
name: &'static str,
}
impl<C: Subscribe> SubscriptionSource<C> for CountedQueue {
type Subscriber = CountedSubscriber<C::Subscriber>;
type Copies = AddressedCopies;
fn name(&self) -> &str {
self.name
}
async fn subscribe(self, connected: &C) -> Result<Self::Subscriber, C::Error> {
Ok(CountedSubscriber(connected.subscribe(self.name).await?))
}
}
impl<C: Subscribe> RedeliveryAddressed<C> for CountedQueue {
fn redelivery_address(
&self,
_connected: &C,
) -> impl Future<Output = Result<RedeliveryAddress, C::Error>> + Send {
ready(Ok(RedeliveryAddress::new(self.name)))
}
}
struct CountedSubscriber<S>(S);
impl<S: Subscriber> Subscriber for CountedSubscriber<S> {
type Message = CountedMessage<S::Message>;
type Error = S::Error;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
self.0.stream().map(|item| item.map(CountedMessage))
}
}
struct CountedMessage<M>(M);
impl<M: IncomingMessage> IncomingMessage for CountedMessage<M> {
fn payload(&self) -> &[u8] {
self.0.payload()
}
fn headers(&self) -> &HeaderMap {
self.0.headers()
}
fn redelivery_count(&self) -> Option<u64> {
self.0
.headers()
.get_str(DELIVERY_COUNT)
.and_then(|value| value.parse().ok())
}
async fn ack(self) -> Result<(), AckError> {
self.0.ack().await
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
self.0.nack(requeue).await
}
}
#[subscriber(CountedQueue { name: "shipments" })]
async fn never_ready_counted(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::retry_after(RETRY_DELAY)
}
#[tokio::test(start_paused = true)]
async fn the_brokers_own_delivery_count_drives_the_cap() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_counted)
.max_attempts(nonzero!(3u32))
.dead_letter("shipments.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.broker::<MemoryBroker>()
.publish_with_headers("shipments", &Order { id: 7 }, &Delivered { count: 3 })
.await
.expect("publish");
tb.settle().await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("shipments")
.assert_called_once();
tb.broker::<MemoryBroker>()
.published::<Order>("shipments.dead")
.assert_called_once()
.with(&Order { id: 7 });
}
#[tokio::test(start_paused = true)]
async fn a_first_delivery_the_broker_counts_stays_below_the_cap() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_counted)
.max_attempts(nonzero!(3u32))
.dead_letter("shipments.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.broker::<MemoryBroker>()
.publish_with_headers("shipments", &Order { id: 8 }, &Delivered { count: 1 })
.await
.expect("publish");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("shipments")
.assert_called(2);
tb.broker::<MemoryBroker>()
.published::<Order>("shipments.dead")
.assert_not_called();
}
#[subscriber(Queue { name: "orders" })]
async fn never_ready(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::retry_after(RETRY_DELAY)
}
#[subscriber(Queue { name: "jobs" })]
async fn never_ready_now(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::retry()
}
#[subscriber(Queue { name: "invoices" })]
async fn settle_on_the_copy(order: &Order, ctx: &mut Context) -> HandlerOutcome {
let _ = order.id;
if ctx.headers().get_str(RETRY_COUNT_HEADER).is_none() {
HandlerOutcome::retry_after(RETRY_DELAY)
} else {
HandlerOutcome::ack()
}
}
#[derive(Debug, Clone, Copy)]
struct StampSource;
impl<C, Options> PublishTransform<ForReply<C>, Options> for StampSource {
type Destination = Reads;
fn apply(
&self,
out: &mut Outgoing<'_>,
_options: &mut Option<Options>,
cx: &PublishContext<'_, C>,
) {
out.headers_mut()
.insert("x-retried-from", cx.name().to_owned());
}
}
#[tokio::test(start_paused = true)]
async fn the_cap_sends_the_last_delivery_to_the_dead_letter_destination() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready)
.max_attempts(nonzero!(3u32))
.dead_letter("orders.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 1 })
.to("orders")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called_once();
tb.advance(RETRY_DELAY).await.expect("settle");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called(3);
tb.broker::<MemoryBroker>()
.published::<Order>("orders.dead")
.assert_called_once()
.with(&Order { id: 1 });
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called(3);
}
#[tokio::test(start_paused = true)]
async fn a_cap_without_a_destination_rejects_the_last_delivery() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready).max_attempts(nonzero!(2u32));
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 2 })
.to("orders")
.publish()
.await
.expect("publish");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called(2);
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called(2);
tb.broker::<MemoryBroker>()
.published::<Order>("orders.dead")
.assert_not_called();
}
#[tokio::test(start_paused = true)]
async fn a_destination_without_a_cap_redirects_every_copy() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready).dead_letter("orders.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 3 })
.to("orders")
.publish()
.await
.expect("publish");
tb.settle().await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called_once();
tb.broker::<MemoryBroker>()
.published::<Order>("orders.dead")
.assert_called_once()
.with(&Order { id: 3 });
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_immediate_retry_counts_through_the_header() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_now)
.max_attempts(nonzero!(3u32))
.dead_letter("jobs.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 4 })
.to("jobs")
.publish()
.await
.expect("publish");
tb.settle().await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("jobs")
.assert_called(3);
tb.broker::<MemoryBroker>()
.published::<Order>("jobs.dead")
.assert_called_once()
.with(&Order { id: 4 });
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_immediate_retry_at_the_cap_without_a_destination_is_rejected() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_now).max_attempts(nonzero!(2u32));
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 16 })
.to("jobs")
.publish()
.await
.expect("publish");
tb.settle().await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("jobs")
.assert_called(2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_destination_without_a_cap_takes_an_immediate_retry_away() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_now).dead_letter("jobs.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 17 })
.to("jobs")
.publish()
.await
.expect("publish");
tb.settle().await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("jobs")
.assert_called_once();
tb.broker::<MemoryBroker>()
.published::<Order>("jobs.dead")
.assert_called_once()
.with(&Order { id: 17 });
}
#[tokio::test(start_paused = true)]
async fn the_declaration_composes_with_the_named_publisher() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready)
.max_attempts(nonzero!(2u32))
.dead_letter("orders.dead")
.out_retry(MemoryPublish)
.transform(StampSource);
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 6 })
.to("orders")
.publish()
.await
.expect("publish");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called(2);
tb.broker::<MemoryBroker>()
.published::<Order>("orders.dead")
.assert_called_once()
.with_header("x-retried-from", "orders");
}
#[derive(Debug, Clone)]
struct ManagedQueue {
name: &'static str,
}
impl<C: Subscribe> SubscriptionSource<C> for ManagedQueue {
type Subscriber = C::Subscriber;
type Copies = BrokerMoves;
fn name(&self) -> &str {
self.name
}
fn declare_retry(mut self, declaration: &RetryDeclaration) -> Self {
if declaration.max_attempts().is_some() && declaration.dead_letter().is_some() {
self.name = "managed.capped";
}
self
}
async fn subscribe(self, connected: &C) -> Result<Self::Subscriber, C::Error> {
connected.subscribe(self.name).await
}
}
#[derive(Clone, FromRef)]
struct Deliveries {
seen: Arc<AtomicU32>,
}
#[subscriber(ManagedQueue { name: "managed" })]
async fn moved_retry(order: &Order, State(seen): State<Arc<AtomicU32>>) -> HandlerOutcome {
let _ = order.id;
if seen.fetch_add(1, Ordering::SeqCst) == 0 {
HandlerOutcome::retry()
} else {
HandlerOutcome::ack()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_broker_moved_immediate_retry_is_never_capped() {
let seen = Arc::new(AtomicU32::new(0));
let deliveries = Deliveries {
seen: Arc::clone(&seen),
};
let app = RustStream::new(AppInfo::new("declared", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(deliveries))
.with_broker(MemoryBroker::new(), |b| {
b.include(moved_retry)
.max_attempts(nonzero!(1u32))
.dead_letter("managed.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 14 })
.to("managed.capped")
.publish()
.await
.expect("publish");
tb.settle().await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("managed")
.assert_called(2);
tb.broker::<MemoryBroker>()
.published::<Order>("managed.dead")
.assert_not_called();
}
fn retry_count(headers: &HeaderMap) -> u64 {
headers
.get_str(RETRY_COUNT_HEADER)
.and_then(|value| value.parse().ok())
.unwrap_or(0)
}
#[derive(Debug, Clone)]
struct WaitQueue {
name: &'static str,
}
impl SubscriptionSource<ConnectedMemoryBroker> for WaitQueue {
type Subscriber = WaitQueueSubscriber;
type Copies = AddressedCopies;
fn name(&self) -> &str {
self.name
}
async fn subscribe(
self,
connected: &ConnectedMemoryBroker,
) -> Result<Self::Subscriber, MemoryError> {
Ok(WaitQueueSubscriber {
inner: connected.subscribe(self.name).await?,
publisher: connected.publisher(),
name: self.name,
})
}
}
impl RedeliveryAddressed<ConnectedMemoryBroker> for WaitQueue {
fn redelivery_address(
&self,
_connected: &ConnectedMemoryBroker,
) -> impl Future<Output = Result<RedeliveryAddress, MemoryError>> + Send {
ready(Ok(RedeliveryAddress::new(self.name)))
}
}
struct WaitQueueSubscriber {
inner: MemorySubscriber,
publisher: MemoryPublisher,
name: &'static str,
}
impl Subscriber for WaitQueueSubscriber {
type Message = WaitQueueMessage;
type Error = <MemorySubscriber as Subscriber>::Error;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
let publisher = self.publisher.clone();
let name = self.name;
self.inner.stream().map(move |item| {
item.map(|inner| WaitQueueMessage {
inner,
publisher: publisher.clone(),
name,
})
})
}
}
struct WaitQueueMessage {
inner: MemoryMessage,
publisher: MemoryPublisher,
name: &'static str,
}
impl IncomingMessage for WaitQueueMessage {
fn payload(&self) -> &[u8] {
self.inner.payload()
}
fn headers(&self) -> &HeaderMap {
self.inner.headers()
}
fn redelivery_count(&self) -> Option<u64> {
None
}
fn supports_nack_after(&self) -> bool {
true
}
async fn nack_after(self, _delay: Duration) -> Result<(), AckError> {
let mut headers = self.inner.headers().clone();
headers.insert(RETRY_COUNT_HEADER, (retry_count(&headers) + 1).to_string());
let copy = OutgoingMessage::new(self.name, self.inner.payload()).with_headers(headers);
self.publisher
.publish(copy, None)
.await
.expect("the wait queue lost the copy");
self.inner.nack(false).await
}
async fn ack(self) -> Result<(), AckError> {
self.inner.ack().await
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
self.inner.nack(requeue).await
}
}
#[subscriber(WaitQueue { name: "returns" })]
async fn never_ready_in_the_wait_queue(order: &Order, ctx: &mut Context) -> HandlerOutcome {
let _ = order.id;
if retry_count(ctx.headers()) < 6 {
HandlerOutcome::retry_after(RETRY_DELAY)
} else {
HandlerOutcome::ack()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_cap_applies_where_a_native_delay_counts_through_the_header() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_in_the_wait_queue)
.max_attempts(nonzero!(3u32))
.dead_letter("returns.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 14 })
.to("returns")
.publish()
.await
.expect("publish");
tb.settle().await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("returns")
.assert_called(3);
tb.broker::<MemoryBroker>()
.published::<Order>("returns.dead")
.assert_called_once()
.with(&Order { id: 14 });
}
#[derive(Debug, Clone)]
struct Filter {
name: &'static str,
}
impl<C: Subscribe> SubscriptionSource<C> for Filter {
type Subscriber = ShapedSubscriber<C::Subscriber>;
type Copies = NamedCopies;
fn name(&self) -> &str {
self.name
}
async fn subscribe(self, connected: &C) -> Result<Self::Subscriber, C::Error> {
Ok(ShapedSubscriber(connected.subscribe(self.name).await?))
}
}
#[subscriber(Filter { name: "sensors" })]
async fn filtered(order: &Order, ctx: &mut Context) -> HandlerOutcome {
let _ = order.id;
if ctx.headers().get_str(RETRY_COUNT_HEADER).is_none() {
HandlerOutcome::retry_after(RETRY_DELAY)
} else {
HandlerOutcome::ack()
}
}
#[tokio::test(start_paused = true)]
async fn a_named_destination_overrides_an_addressed_descriptor() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(settle_on_the_copy)
.out_retry(MemoryPublish)
.to("invoices.retry");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 22 })
.to("invoices")
.publish()
.await
.expect("publish");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("invoices")
.assert_called_once();
tb.broker::<MemoryBroker>()
.published::<Order>("invoices.retry")
.assert_called_once()
.with(&Order { id: 22 });
}
#[derive(Debug, Clone, Copy)]
struct ToOriginalTopic;
impl<C, Options> PublishTransform<ForReply<C>, Options> for ToOriginalTopic {
type Destination = Names;
fn apply(
&self,
out: &mut Outgoing<'_>,
_options: &mut Option<Options>,
cx: &PublishContext<'_, C>,
) {
let topic = cx
.headers()
.get_str("x-topic")
.unwrap_or_else(|| cx.name())
.to_owned();
out.set_name(topic);
}
}
#[tokio::test(start_paused = true)]
async fn a_naming_transform_sends_each_copy_to_the_delivery_s_own_topic() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include_router(
Router::<MemoryBroker>::new()
.include(filtered)
.out_retry(MemoryPublish)
.transform(ToOriginalTopic)
.build(),
);
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.broker::<MemoryBroker>()
.publish_with_headers(
"sensors",
&Order { id: 23 },
&Routed {
topic: "sensors.north",
},
)
.await
.expect("publish");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("sensors")
.assert_called_once();
tb.broker::<MemoryBroker>()
.published::<Order>("sensors.north")
.assert_called_once()
.with(&Order { id: 23 });
}
#[tokio::test]
async fn an_unnamed_destination_on_an_unaddressed_descriptor_refuses_to_start() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(filtered);
});
let failed = TestApp::start(app)
.await
.expect_err("a registration whose copies go nowhere must not start");
let message = failed.to_string();
assert!(message.contains("sensors"), "{message}");
assert!(message.contains("NamedCopies"), "{message}");
assert!(message.contains(".to("), "{message}");
}
#[subscriber("crates")]
async fn never_ready_in_memory(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::retry_after(RETRY_DELAY)
}
#[tokio::test(start_paused = true)]
async fn the_cap_is_spent_on_the_reference_brokers_native_redelivery() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_in_memory)
.max_attempts(nonzero!(3u32))
.dead_letter("crates.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 31 })
.to("crates")
.publish()
.await
.expect("publish");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("crates")
.assert_called(3);
tb.broker::<MemoryBroker>()
.published::<Order>("crates.dead")
.assert_called_once()
.with(&Order { id: 31 });
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("crates")
.assert_called(3);
}
#[tokio::test(start_paused = true)]
async fn a_cap_without_a_destination_terminates_on_the_reference_broker() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_in_memory)
.max_attempts(nonzero!(2u32));
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 32 })
.to("crates")
.publish()
.await
.expect("publish");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("crates")
.assert_called(2);
tb.broker::<MemoryBroker>()
.published::<Order>("crates.dead")
.assert_not_called();
}
#[tokio::test(start_paused = true)]
async fn a_native_count_is_the_only_count() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_in_memory)
.max_attempts(nonzero!(3u32))
.dead_letter("crates.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.broker::<MemoryBroker>()
.publish_with_headers("crates", &Order { id: 34 }, &Retried { retries: 3 })
.await
.expect("publish");
tb.advance(RETRY_DELAY).await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("crates")
.assert_called(2);
tb.broker::<MemoryBroker>()
.published::<Order>("crates.dead")
.assert_not_called();
}
#[subscriber("tarballs")]
async fn never_ready_now_in_memory(order: &Order, ctx: &mut Context) -> HandlerOutcome {
let _ = order.id;
assert!(
ctx.headers().get_str(RETRY_COUNT_HEADER).is_none(),
"the broker counts its own redeliveries, so nothing republishes the delivery",
);
HandlerOutcome::retry()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_immediate_retry_is_requeued_natively_and_still_capped() {
let app =
RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(never_ready_now_in_memory)
.max_attempts(nonzero!(3u32))
.dead_letter("tarballs.dead");
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 33 })
.to("tarballs")
.publish()
.await
.expect("publish");
tb.settle().await.expect("settle");
tb.broker::<MemoryBroker>()
.subscriber("tarballs")
.assert_called(3);
tb.broker::<MemoryBroker>()
.published::<Order>("tarballs.dead")
.assert_called_once()
.with(&Order { id: 33 });
}