#![cfg(all(feature = "macros", feature = "json"))]
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::convert::Infallible;
use std::future::{Future, ready};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use ruststream::codec::{Codec, JsonCodec};
#[cfg(feature = "memory")]
use ruststream::memory::MemoryBroker;
use ruststream::runtime::{Outgoing as OutgoingView, PublishExt, PublishIdentity, PublishPipeline};
use ruststream::{
BytesMut, HeaderMap, Lend, Outgoing, OutgoingMessage, Publisher, Serialized, Take,
};
use serde::Serialize;
struct Counting;
thread_local! {
static ALLOCATIONS: Cell<usize> = const { Cell::new(0) };
}
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.with(|count| count.set(count.get() + 1));
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
fn allocations() -> usize {
ALLOCATIONS.with(Cell::get)
}
#[derive(Outgoing, Serialize)]
#[outgoing(name = "orders.created")]
struct OrderCreated {
id: u64,
}
#[derive(Outgoing, Serialized)]
#[outgoing(name = "orders.audit")]
struct Audit(Vec<u8>);
#[derive(Outgoing, Serialized)]
#[outgoing(name = "ticks")]
#[wire(encode = write_tick)]
struct Tick {
at: AtomicUsize,
}
fn write_tick(tick: &Tick, buf: &mut BytesMut) {
buf.extend_from_slice(&[1, 2, 3, 4]);
tick.at.store(buf.as_ptr() as usize, Ordering::Relaxed);
}
#[derive(Clone, Copy)]
enum Claim {
Read,
Vec,
Bytes,
}
struct Seen {
read_at: usize,
taken_at: usize,
len: usize,
}
struct Probe {
claim: Claim,
seen: Mutex<Option<Seen>>,
}
impl Probe {
fn taking(claim: Claim) -> Self {
Self {
claim,
seen: Mutex::new(None),
}
}
fn seen(&self) -> Seen {
self.seen
.lock()
.expect("probe mutex poisoned")
.take()
.expect("nothing was published")
}
}
impl Publisher for Probe {
type Payload = Take;
type Error = Infallible;
type Options = ();
fn publish(
&self,
msg: OutgoingMessage<'_, BytesMut>,
_options: Option<&()>,
) -> impl Future<Output = Result<(), Infallible>> {
let read_at = msg.payload().as_ptr() as usize;
let len = msg.payload().len();
let payload = msg.into_payload();
let taken_at = match self.claim {
Claim::Read => read_at,
Claim::Vec => {
let taken = Vec::from(payload);
let at = taken.as_ptr() as usize;
drop(taken);
at
}
Claim::Bytes => {
let taken = payload.freeze();
let at = taken.as_ptr() as usize;
drop(taken);
at
}
};
*self.seen.lock().expect("probe mutex poisoned") = Some(Seen {
read_at,
taken_at,
len,
});
ready(Ok(()))
}
}
#[derive(Default)]
struct Reading(Mutex<Option<Seen>>);
impl Reading {
fn seen(&self) -> Seen {
self.0
.lock()
.expect("reading probe mutex poisoned")
.take()
.expect("nothing was published")
}
}
impl Publisher for Reading {
type Payload = Lend;
type Error = Infallible;
type Options = ();
fn publish(
&self,
msg: OutgoingMessage<'_, &[u8]>,
_options: Option<&()>,
) -> impl Future<Output = Result<(), Infallible>> {
let read_at = msg.payload().as_ptr() as usize;
let len = msg.payload().len();
let lent: &[u8] = msg.into_payload();
*self.0.lock().expect("reading probe mutex poisoned") = Some(Seen {
read_at,
taken_at: lent.as_ptr() as usize,
len,
});
ready(Ok(()))
}
}
const STAMP: &str = "x-stamp";
#[derive(Default)]
struct Keeper(Mutex<Option<(String, Vec<u8>, HeaderMap)>>);
impl Keeper {
fn kept(&self) -> (String, Vec<u8>, HeaderMap) {
self.0
.lock()
.expect("keeper mutex poisoned")
.take()
.expect("nothing was published")
}
}
impl Publisher for Keeper {
type Payload = Take;
type Error = Infallible;
type Options = ();
fn publish(
&self,
msg: OutgoingMessage<'_, BytesMut>,
_options: Option<&()>,
) -> impl Future<Output = Result<(), Infallible>> {
let (name, payload, headers) = msg.into_parts();
*self.0.lock().expect("keeper mutex poisoned") =
Some((name.to_owned(), Vec::from(payload), headers));
ready(Ok(()))
}
}
struct SeenHeaders {
len: usize,
value_at: usize,
}
#[derive(Default)]
struct Taker(Mutex<Option<SeenHeaders>>);
impl Taker {
fn seen(&self) -> SeenHeaders {
self.0
.lock()
.expect("taker mutex poisoned")
.take()
.expect("nothing was published")
}
}
impl Publisher for Taker {
type Payload = Lend;
type Error = Infallible;
type Options = ();
fn publish(
&self,
msg: OutgoingMessage<'_, &[u8]>,
_options: Option<&()>,
) -> impl Future<Output = Result<(), Infallible>> {
let (_, _, headers) = msg.into_parts();
let seen = SeenHeaders {
len: headers.len(),
value_at: headers
.get(STAMP)
.map_or(0, |value| value.as_ptr() as usize),
};
*self.0.lock().expect("taker mutex poisoned") = Some(seen);
ready(Ok(()))
}
}
#[tokio::test]
async fn a_value_that_carries_its_own_bytes_is_lent_to_a_reading_broker() {
let audit = Audit(br#"{"seen":true}"#.to_vec());
let publisher = Reading::default();
publisher
.message(&audit)
.publish()
.await
.expect("the probe never refuses");
let seen = publisher.seen();
assert_eq!(
seen.read_at,
audit.0.as_ptr() as usize,
"a transport that reads the payload is handed the value's own buffer, uncopied",
);
}
#[tokio::test]
async fn a_value_that_carries_its_own_bytes_is_copied_for_a_taking_broker() {
let audit = Audit(br#"{"seen":true}"#.to_vec());
let publisher = Probe::taking(Claim::Read);
publisher
.message(&audit)
.publish()
.await
.expect("the probe never refuses");
let seen = publisher.seen();
assert_ne!(
seen.read_at,
audit.0.as_ptr() as usize,
"a transport that keeps the payload cannot keep bytes the value owns, so it is handed \
a buffer of its own",
);
assert_eq!(seen.len, audit.0.len(), "holding the same bytes");
}
#[tokio::test]
async fn a_transport_takes_the_destination_the_payload_and_the_headers_in_one_move() {
let mut headers = HeaderMap::new();
headers.insert(STAMP, b"1".to_vec());
let publisher = Keeper::default();
publisher
.message(&OrderCreated { id: 7 })
.with_headers(headers)
.publish()
.await
.expect("the keeper never refuses");
let (name, payload, headers) = publisher.kept();
assert_eq!(
name, "orders.created",
"the destination the message declared"
);
assert_eq!(
payload,
JsonCodec
.encode(&OrderCreated { id: 7 })
.expect("encodable"),
"the payload reaches the broker byte for byte",
);
assert_eq!(
headers.get(STAMP),
Some(b"1".as_slice()),
"and the map arrives whole, without the transport reading it out entry by entry",
);
}
#[tokio::test]
async fn handing_the_header_map_to_the_broker_allocates_nothing() {
let pipeline = PublishIdentity;
let taker = Taker::default();
let mut out = OutgoingView::new("events", b"{}".as_slice());
out.headers_mut().insert(STAMP, b"1".to_vec());
let stamped_at = out.headers().get(STAMP).expect("just stamped").as_ptr() as usize;
let before = allocations();
pipeline
.run(&mut out, &taker, None)
.await
.expect("the taker never refuses");
let spent = allocations() - before;
let seen = taker.seen();
assert_eq!(
spent, 0,
"the map travels into the broker's message; a copy of it would cost the table and a \
reference block per entry",
);
assert_eq!(
(seen.len, seen.value_at),
(1, stamped_at),
"and it arrives whole, over the buffer the stamp wrote",
);
}
#[tokio::test]
async fn only_a_transport_that_takes_the_buffer_pays_for_it() {
let counted = async |claim| {
let publisher = Probe::taking(claim);
let value = OrderCreated { id: 7 };
publisher
.message(&value)
.publish()
.await
.expect("the probe never refuses");
publisher.seen();
let before = allocations();
publisher
.message(&value)
.publish()
.await
.expect("the probe never refuses");
let spent = allocations() - before;
(spent, publisher.seen())
};
let (handed_over, read) = counted(Claim::Read).await;
let (as_vec, took_vec) = counted(Claim::Vec).await;
let (as_bytes, took_bytes) = counted(Claim::Bytes).await;
assert_eq!(
as_vec, handed_over,
"the codec's buffer is already the vector, so taking it as one allocates nothing",
);
assert_eq!(
took_vec.taken_at, took_vec.read_at,
"the vector is the buffer the codec wrote, not a copy of it",
);
assert_eq!(
as_bytes,
handed_over + 1,
"a `Bytes` needs the shared ownership block, and that block is all it costs",
);
assert_eq!((read.len, took_vec.len, took_bytes.len), (8, 8, 8));
}
#[tokio::test]
async fn a_computed_value_hands_over_the_buffer_it_wrote() {
let tick = Tick {
at: AtomicUsize::new(0),
};
let publisher = Probe::taking(Claim::Read);
publisher
.message(&tick)
.publish()
.await
.expect("the probe never refuses");
let seen = publisher.seen();
assert_eq!(
seen.read_at,
tick.at.load(Ordering::Relaxed),
"the value wrote into the publish path's buffer, and that buffer is what leaves",
);
assert_eq!(seen.len, 4, "holding what the value wrote");
}
#[cfg(feature = "memory")]
#[tokio::test]
async fn a_header_costs_the_in_memory_broker_only_what_writing_it_costs() {
let broker = MemoryBroker::new();
let publisher = broker.publisher();
let counted = async move |carried: usize| {
let before = allocations();
let mut headers = HeaderMap::new();
for _ in 0..carried {
headers.insert(STAMP, b"1".to_vec());
}
let msg = OutgoingMessage::new("orders.created", b"{}").with_headers(headers);
publisher
.publish(msg, None)
.await
.expect("the bus is up for the length of this test");
allocations() - before
};
counted(0).await;
let bare = counted(0).await;
let stamped = counted(1).await;
let writing = {
let before = allocations();
let mut headers = HeaderMap::new();
headers.insert(STAMP, b"1".to_vec());
allocations() - before
};
assert_eq!(
stamped - bare,
writing,
"the map travels into the bus; a copy of it on the way would add the table and a \
reference block per entry",
);
}
#[cfg(all(feature = "memory", feature = "testing"))]
mod pool {
use super::*;
use ruststream::memory::ConnectedMemoryBroker;
use ruststream::prelude::*;
use ruststream::testing::TestApp;
use ruststream::{PairError, PublishPolicy};
use serde::Deserialize;
#[derive(Debug, Serialize, Deserialize, Outgoing)]
struct Order {
id: u64,
}
#[derive(Debug, Serialize, Outgoing)]
#[outgoing(name = "confirmations")]
struct Confirmation {
id: u64,
}
#[subscriber("orders.pooled", workers(2), reply)]
async fn confirm_pooled(order: &Order) -> Confirmation {
Confirmation { id: order.id }
}
#[subscriber("orders.sequential", reply)]
async fn confirm_sequential(order: &Order) -> Confirmation {
Confirmation { id: order.id }
}
#[subscriber("plain.pooled", workers(2))]
async fn acknowledge_pooled(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::ack()
}
#[subscriber("plain.sequential")]
async fn acknowledge_sequential(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::ack()
}
#[derive(Debug, Clone, Copy)]
struct SinkPublish;
impl PublishPolicy<ConnectedMemoryBroker> for SinkPublish {
type Live = Reading;
fn pair(
self,
_connected: &ConnectedMemoryBroker,
) -> impl Future<Output = Result<Reading, PairError>> {
ready(Ok(Reading::default()))
}
}
async fn cost_of(tb: &TestApp<()>, name: &str, count: u64) -> usize {
let before = allocations();
for id in 0..count {
tb.message(&Order { id })
.to(name)
.publish()
.await
.expect("publish");
}
allocations() - before
}
const NAMES: [&str; 4] = [
"orders.pooled",
"orders.sequential",
"plain.pooled",
"plain.sequential",
];
#[tokio::test]
async fn a_pool_reuses_its_encode_buffers_across_deliveries() {
let app =
RustStream::new(AppInfo::new("pool", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(confirm_pooled).out_reply(SinkPublish);
b.include(confirm_sequential).out_reply(SinkPublish);
b.include(acknowledge_pooled);
b.include(acknowledge_sequential);
});
let tb = TestApp::start(app).await.expect("startup failed");
for name in NAMES {
cost_of(&tb, name, 4).await;
}
let mut cost = [0; 4];
for (slot, name) in cost.iter_mut().zip(NAMES) {
*slot = cost_of(&tb, name, 8).await;
}
let [reply_pooled, reply_sequential, ack_pooled, ack_sequential] = cost;
let pool_over_replies = reply_pooled.saturating_sub(reply_sequential);
let pool_over_acks = ack_pooled.saturating_sub(ack_sequential);
assert!(
pool_over_replies <= pool_over_acks,
"over eight deliveries the pool adds {pool_over_replies} allocations to replies \
and {pool_over_acks} to acknowledgements: a worker's buffer is not coming back \
to the pool ({cost:?})",
);
}
}