#![cfg(all(
feature = "memory",
feature = "macros",
feature = "json",
feature = "testing"
))]
mod common;
use common::{Event, Wire};
use ruststream::memory::prelude::*;
use ruststream::memory::{ConnectedMemoryBroker, MemoryPublisher};
use ruststream::testing::TestApp;
use ruststream::{OutgoingMessage, PairError};
#[derive(Deserialized)]
struct Frame<'a>(&'a [u8]);
#[derive(OutSlot)]
#[publishes(Wire)]
struct Encoded;
#[derive(OutSlot)]
#[publishes(Wire)]
struct Audit;
#[subscriber("slots.in")]
async fn transcode(
chunk: &Frame<'_>,
Out(encoded): Out<impl Publisher, Encoded>,
Out(audit): Out<impl Publisher, Audit>,
) -> HandlerOutcome {
let mut headers = HeaderMap::new();
headers.insert("source", "slots.in");
if encoded
.message(&Wire::of(chunk.0))
.with_headers(headers)
.to("slots.encoded")
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
let receipt = chunk.0.len().to_be_bytes();
if audit
.message(&Wire::of(receipt))
.to("slots.audit")
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn slots_bind_by_marker_and_capture_per_slot() {
let app =
RustStream::new(AppInfo::new("slots", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(transcode)
.out(Audit, Publish)
.out(Encoded, Publish)
.build();
});
let tb = TestApp::start(app).await.expect("harness start");
tb.broker::<MemoryBroker>()
.message(&Wire::of(b"frame"))
.to("slots.in")
.publish()
.await
.expect("publish");
let encoded = tb.out::<Encoded>().assert_called_once().with_raw(b"frame");
let recorded = &encoded.messages()[0];
assert_eq!(recorded.name(), "slots.encoded");
assert_eq!(
recorded.headers().get("source"),
Some(b"slots.in".as_slice()),
);
tb.out::<Audit>()
.assert_called_once()
.with_raw(5u64.to_be_bytes().as_slice());
tb.broker::<MemoryBroker>()
.published::<Event>("slots.encoded")
.assert_called_once();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_slot_binds_a_foreign_broker_through_a_token() {
let ingress = MemoryBroker::new();
let other = MemoryBroker::new().bindable();
let to_other = other.bind(Publish);
let app = RustStream::new(AppInfo::new("slots-cross", "0.1.0"))
.with_broker_labeled("other", other, |_b| {})
.with_broker_labeled("ingress", ingress, |b| {
b.include(transcode)
.out(Audit, to_other)
.out(Encoded, Publish)
.build();
});
let tb = TestApp::start(app).await.expect("harness start");
tb.broker_named("ingress")
.message(&Wire::of(b"xy"))
.to("slots.in")
.publish()
.await
.expect("publish");
let ingress = tb.broker_named("ingress");
let other = tb.broker_named("other");
ingress
.published::<()>("slots.encoded")
.assert_called_once()
.with_raw(b"xy");
other.published::<()>("slots.encoded").assert_not_called();
other
.published::<()>("slots.audit")
.assert_called_once()
.with_raw(2u64.to_be_bytes().as_slice());
ingress.published::<()>("slots.audit").assert_not_called();
}
#[subscriber("slots.ledger")]
async fn settle(event: &Event, Out(tx): Out<impl OwnedTransactions, Encoded>) -> HandlerOutcome {
let Ok(mut txn) = tx.transaction().await else {
return HandlerOutcome::retry();
};
let payload = serde_json::to_vec(event).expect("serializable");
if txn
.message(&Wire::of(payload))
.to("slots.settled")
.publish()
.await
.is_err()
|| txn.commit().await.is_err()
{
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_capability_refined_slot_pairs_a_transactional_publisher() {
let app =
RustStream::new(AppInfo::new("slots-txn", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(settle).out(Encoded, TransactionalPublish).build();
});
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Event { id: 4 })
.to("slots.ledger")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Event>("slots.settled")
.assert_called_once()
.with(&Event { id: 4 });
tb.out::<Encoded>().assert_not_called();
}
#[subscriber("slots.by-hand")]
async fn settle_by_hand(
event: &Event,
Out(journal): Out<impl TransactionalPublisher, Encoded>,
) -> HandlerOutcome {
if TransactionalPublisher::begin_transaction(journal)
.await
.is_err()
{
return HandlerOutcome::retry();
}
let payload = serde_json::to_vec(event).expect("serializable");
if journal
.message(&Wire::of(payload))
.to("slots.by-hand.settled")
.publish()
.await
.is_err()
{
let _ = TransactionalPublisher::abort(journal).await;
return HandlerOutcome::retry();
}
if TransactionalPublisher::commit(journal).await.is_err() {
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_raw_capability_calls_keep_the_slot_attribution() {
let app = RustStream::new(AppInfo::new("slots-by-hand", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(settle_by_hand)
.out(Encoded, TransactionalPublish)
.build();
},
);
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Event { id: 6 })
.to("slots.by-hand")
.publish()
.await
.expect("publish");
tb.out::<Encoded>()
.assert_called_once()
.decoded_as::<Event>()
.with(&Event { id: 6 });
tb.broker::<MemoryBroker>()
.published::<Event>("slots.by-hand.settled")
.assert_called_once()
.with(&Event { id: 6 });
}
#[subscriber("slots.refused")]
async fn commit_without_begin(
_event: &Event,
Out(journal): Out<impl TransactionalPublisher, Encoded>,
) -> HandlerOutcome {
if TransactionalPublisher::commit(journal).await.is_err() {
return HandlerOutcome::drop();
}
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_refused_capability_call_comes_back_as_the_entrys_error() {
let app = RustStream::new(AppInfo::new("slots-refused", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(commit_without_begin)
.out(Encoded, TransactionalPublish)
.build();
},
);
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Event { id: 8 })
.to("slots.refused")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.subscriber("slots.refused")
.assert_called_once()
.settled(HandlerOutcome::drop());
tb.out::<Encoded>().assert_not_called();
}
#[subscriber("slots.raw-owned")]
async fn settle_raw(
event: &Event,
Out(ledger): Out<impl OwnedTransactions, Encoded>,
) -> HandlerOutcome {
let Ok(mut txn) = OwnedTransactions::transaction(ledger).await else {
return HandlerOutcome::retry();
};
let payload = serde_json::to_vec(event).expect("serializable");
if txn
.publish(
OutgoingMessage::new("slots.raw-owned.settled", &payload),
None,
)
.await
.is_err()
{
let _ = txn.abort().await;
return HandlerOutcome::retry();
}
if txn.commit().await.is_err() {
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_raw_owned_transaction_settles_outside_the_slot() {
let app = RustStream::new(AppInfo::new("slots-raw-owned", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(settle_raw)
.out(Encoded, TransactionalPublish)
.build();
},
);
let tb = TestApp::start(app).await.expect("harness start");
tb.message(&Event { id: 5 })
.to("slots.raw-owned")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Event>("slots.raw-owned.settled")
.assert_called_once()
.with(&Event { id: 5 });
tb.out::<Encoded>().assert_not_called();
}
#[derive(Clone)]
struct LaneRouter {
publisher: MemoryPublisher,
}
trait ShardLanes {
fn lane(&self, shard: u64) -> (&MemoryPublisher, &'static str);
}
impl ShardLanes for LaneRouter {
fn lane(&self, shard: u64) -> (&MemoryPublisher, &'static str) {
let dest = if shard.is_multiple_of(2) {
"slots.lane.even"
} else {
"slots.lane.odd"
};
(&self.publisher, dest)
}
}
impl<M, W: ShardLanes, E, Pipe, Body> ShardLanes for Slot<M, W, E, Pipe, Body> {
fn lane(&self, shard: u64) -> (&MemoryPublisher, &'static str) {
(**self).lane(shard)
}
}
async fn sent<L: ShardLanes + Sync>(lanes: &L, event: &Event) -> bool {
let (publisher, dest) = lanes.lane(event.id);
publisher.message(event).to(dest).publish().await.is_ok()
}
struct LanePolicy;
impl PublishPolicy<ConnectedMemoryBroker> for LanePolicy {
type Live = LaneRouter;
async fn pair(self, connected: &ConnectedMemoryBroker) -> Result<Self::Live, PairError> {
Ok(LaneRouter {
publisher: Publish.pair(connected).await?,
})
}
}
#[subscriber("slots.sharded")]
async fn route_shard(event: &Event, Out(lanes): Out<impl ShardLanes>) -> HandlerOutcome {
if sent(lanes, event).await {
HandlerOutcome::ack()
} else {
HandlerOutcome::retry()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_broker_defined_capability_extends_the_slot_vocabulary() {
let app = RustStream::new(AppInfo::new("slots-lanes", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(route_shard).out(DefaultSlot, LanePolicy).build();
},
);
let tb = TestApp::start(app).await.expect("harness start");
for id in [2u64, 3u64] {
tb.message(&Event { id })
.to("slots.sharded")
.publish()
.await
.expect("publish");
}
tb.broker::<MemoryBroker>()
.published::<Event>("slots.lane.even")
.assert_called_once()
.with(&Event { id: 2 });
tb.broker::<MemoryBroker>()
.published::<Event>("slots.lane.odd")
.assert_called_once()
.with(&Event { id: 3 });
tb.out::<DefaultSlot>().assert_not_called();
}