use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use super::{
PayloadGrant, PublishFact, Recorded, Tap, TapBatch, TapError, TapFilter, TapIdent, TapOutcome,
TapSource,
};
fn filter(subject: &str) -> TapFilter {
TapFilter {
subject: subject.to_owned(),
}
}
fn fact<'a>(subject: &'a str, payload: &'a [u8]) -> PublishFact<'a> {
PublishFact {
subject,
payload,
outcome: TapOutcome::Delivered,
message_id: None,
source: TapSource::Connection(7),
reply_to: None,
recorded: None,
}
}
fn publish(tap: &Tap, times: usize) -> Result<(), TapError> {
for _ in 0..times {
tap.observe(&fact("orders", b"{}"))?;
}
Ok(())
}
fn shape(batches: &[TapBatch]) -> Vec<(u64, u32, u64, usize)> {
batches
.iter()
.map(|batch| {
(
batch.seq,
batch.sampled.get(),
batch.dropped,
batch.messages.len(),
)
})
.collect()
}
#[test]
fn with_no_tap_the_hook_counts_nothing() -> Result<(), TapError> {
let tap = Tap::new("local", 8);
publish(&tap, 1_000)?;
assert_eq!(tap.observations(), 0);
let subscription = tap.subscribe(filter("orders"))?;
publish(&tap, 3)?;
assert_eq!(tap.observations(), 3, "positive control: a live tap counts");
drop(subscription);
publish(&tap, 5)?;
assert_eq!(tap.observations(), 3);
assert_eq!(tap.subscriber_count(), 0);
Ok(())
}
#[test]
fn a_slow_subscriber_never_holds_more_than_its_bound() -> Result<(), TapError> {
let tap = Tap::new("local", 8);
let slow = tap.subscribe(filter("orders"))?;
for _ in 0..1_000 {
tap.observe(&fact("orders", b"{}"))?;
assert!(slow.held()? <= 8);
}
let batches = slow.take_batches()?;
let kept: usize = batches.iter().map(|batch| batch.messages.len()).sum();
let dropped: u64 = batches.iter().map(|batch| batch.dropped).sum();
assert_eq!(kept, 8);
assert_eq!(dropped, tap.dropped_total());
assert!(dropped > 0, "the ring filled, so something was evicted");
Ok(())
}
#[test]
fn dropped_is_a_delta() -> Result<(), TapError> {
let tap = Tap::new("local", 2);
let subscription = tap.subscribe(filter("orders"))?;
publish(&tap, 3)?;
let mut published = subscription.take_batches()?;
publish(&tap, 2)?;
published.extend(subscription.take_batches()?);
publish(&tap, 1)?;
published.extend(subscription.take_batches()?);
assert_eq!(
shape(&published),
vec![(1, 1, 1, 1), (2, 2, 0, 1), (3, 2, 0, 1), (4, 1, 0, 1)]
);
let sum: u64 = published.iter().map(|batch| batch.dropped).sum();
assert_eq!(sum, 1);
assert_eq!(tap.dropped_total(), 1);
assert!(subscription.take_batches()?.is_empty());
Ok(())
}
#[test]
fn each_batch_has_one_divisor() -> Result<(), TapError> {
let tap = Tap::new("local", 8);
let subscription = tap.subscribe(filter("orders"))?;
publish(&tap, 11)?;
let batches = subscription.take_batches()?;
assert_eq!(
shape(&batches),
vec![(1, 1, 0, 5), (2, 2, 0, 1), (3, 4, 0, 1)]
);
let weighted: u64 = batches
.iter()
.map(|batch| {
u64::from(batch.sampled.get()) * u64::try_from(batch.messages.len()).unwrap_or(u64::MAX)
})
.sum();
assert_eq!(weighted, 11);
assert_eq!(subscription.sample_one_in()?.get(), 4, "a take halves N");
Ok(())
}
#[test]
fn one_wake_per_publish() -> Result<(), TapError> {
let wakes = Arc::new(AtomicU64::new(0));
let counter = Arc::clone(&wakes);
let tap = Tap::with_waker(
"local",
64,
Arc::new(move || {
counter.fetch_add(1, Ordering::Relaxed);
}),
);
let first = tap.subscribe(filter("orders"))?;
let second = tap.subscribe(filter("ord*"))?;
let third = tap.subscribe(filter(""))?;
assert!(tap.observe(&fact("orders", b"{}"))?);
assert_eq!(wakes.load(Ordering::Relaxed), 1);
assert_eq!(
(first.held()?, second.held()?, third.held()?),
(1, 1, 1),
"all three kept it"
);
let elsewhere = tap.observe(&fact("audit", b"{}"))?;
assert!(elsewhere, "the catch-all tap keeps it");
assert_eq!(wakes.load(Ordering::Relaxed), 2);
drop(third);
assert!(!tap.observe(&fact("audit", b"{}"))?);
assert_eq!(wakes.load(Ordering::Relaxed), 2, "no tap kept it: no wake");
Ok(())
}
#[test]
fn reply_to_rides_the_kept_message() -> Result<(), TapError> {
let tap = Tap::new("local", 64);
let subscription = tap.subscribe(filter("orders"))?;
tap.observe(&PublishFact {
reply_to: Some("q-1"),
message_id: Some(41),
..fact("orders", b"{}")
})?;
tap.observe(&PublishFact {
outcome: TapOutcome::Refused,
..fact("orders", b"{}")
})?;
let batches = subscription.take_batches()?;
let messages: Vec<_> = batches.iter().flat_map(|batch| &batch.messages).collect();
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].reply_to.as_deref(), Some("q-1"));
assert_eq!(messages[0].ident, TapIdent::Message(41));
assert_eq!(messages[1].reply_to, None);
assert_eq!(
messages[1].ident,
TapIdent::Observation(1),
"a publish with no message id is named by its observation"
);
assert_eq!(messages[1].outcome, TapOutcome::Refused);
Ok(())
}
#[test]
fn filters_and_grants() -> Result<(), TapError> {
let tap = Tap::new("local", 64);
let exact = tap.subscribe(filter("orders"))?;
let prefix = tap.subscribe(filter("ord*"))?;
let granted = tap.subscribe_with(filter("*"), Some(PayloadGrant::server_issued()))?;
tap.observe(&fact("orders", b"abc"))?;
tap.observe(&fact("other", b"z"))?;
let exact_messages: Vec<_> = exact
.take_batches()?
.into_iter()
.flat_map(|batch| batch.messages)
.collect();
assert_eq!(exact_messages.len(), 1);
assert_eq!(exact_messages[0].subject, "orders");
assert_eq!(exact_messages[0].size, 3);
assert_eq!(exact_messages[0].payload, None, "no grant, no payload");
assert!(
exact_messages[0].hops.is_empty(),
"nothing measured it, so it has no hop"
);
let prefix_count: usize = prefix
.take_batches()?
.iter()
.map(|batch| batch.messages.len())
.sum();
assert_eq!(prefix_count, 1);
let granted_messages: Vec<_> = granted
.take_batches()?
.into_iter()
.flat_map(|batch| batch.messages)
.collect();
assert_eq!(granted_messages.len(), 2);
assert_eq!(granted_messages[0].payload.as_deref(), Some(&b"abc"[..]));
assert_eq!(tap.subscriber_count(), 3);
drop(exact);
drop(prefix);
drop(granted);
assert_eq!(tap.subscriber_count(), 0);
assert_eq!(tap.unsubscribe_failures(), 0);
Ok(())
}
#[test]
fn a_recorded_fan_out_that_reached_no_worker_is_an_empty_list() -> Result<(), TapError> {
let tap = Tap::new("local", 64);
let subscription = tap.subscribe(filter("orders"))?;
tap.name_station(3, "orders.ledger")?;
tap.name_receiver(30, 3)?;
let at = std::time::Instant::now();
for recipients in [
Some(&[][..]),
Some(&[(31, at)][..]),
Some(&[(30, at), (31, at)][..]),
None,
] {
tap.observe(&PublishFact {
recorded: recipients.map(|recipients| Recorded {
recipients,
received_at: at,
appended_at: None,
}),
..fact("orders", b"{}")
})?;
}
let batches = subscription.take_batches()?;
let stations: Vec<Option<Vec<String>>> = batches
.iter()
.flat_map(|batch| &batch.messages)
.map(|message| {
message.to_stations.as_deref().map(|list| {
list.iter()
.map(|reached| reached.station.to_string())
.collect()
})
})
.collect();
assert_eq!(
stations,
vec![
Some(vec![]),
Some(vec![]),
Some(vec!["orders.ledger".to_owned()]),
None,
]
);
Ok(())
}
fn hops(batches: &[TapBatch]) -> Vec<Vec<(String, u64)>> {
batches
.iter()
.flat_map(|batch| &batch.messages)
.map(|message| {
message
.hops
.iter()
.map(|hop| (hop.node.to_string(), hop.at_ms))
.collect()
})
.collect()
}
#[test]
fn a_kept_message_hops_from_its_publisher_through_the_server_to_its_receivers()
-> Result<(), TapError> {
use std::time::{Duration, Instant};
let received = Instant::now();
let ms = Duration::from_millis;
let recipients = [(30, received + ms(5)), (40, received + ms(7))];
let recorded = Recorded {
recipients: &recipients,
received_at: received,
appended_at: Some(received + ms(3)),
};
let durable = PublishFact {
recorded: Some(recorded),
..fact("orders", b"{}")
};
let passing = PublishFact {
recorded: Some(Recorded {
appended_at: None,
..recorded
}),
..durable
};
let named = |tap: &Tap| -> Result<(), TapError> {
tap.name_station(7, "orders.worker")?;
tap.name_station(3, "orders.ledger")?;
tap.name_receiver(30, 3)?;
tap.name_station(4, "orders.audit")?;
tap.name_receiver(40, 4)
};
let hop = |node: &str, at_ms: u64| (node.to_owned(), at_ms);
let tap = Tap::new("local", 64).with_depot(true);
let subscription = tap.subscribe(filter("orders"))?;
named(&tap)?;
tap.observe(&durable)?;
tap.observe(&passing)?;
let batches = subscription.take_batches()?;
assert_eq!(
hops(&batches),
[
vec![
hop("orders.worker", 0),
hop("local.depot", 3),
hop("local", 3),
hop("orders.ledger", 5),
hop("orders.audit", 7),
],
vec![
hop("orders.worker", 0),
hop("orders.ledger", 5),
hop("orders.audit", 7),
],
]
);
let first_sight: Vec<Instant> = batches
.iter()
.flat_map(|batch| &batch.messages)
.map(|message| message.seen_at)
.collect();
assert_eq!(first_sight, [received, received], "first sight is receipt");
let memory = Tap::new("local", 64);
let subscription = memory.subscribe(filter("orders"))?;
named(&memory)?;
memory.observe(&durable)?;
assert_eq!(
hops(&subscription.take_batches()?),
[vec![
hop("orders.worker", 0),
hop("local", 3),
hop("orders.ledger", 5),
hop("orders.audit", 7),
]],
"no persistent store, no depot hop"
);
Ok(())
}
#[test]
fn a_hop_measured_out_of_order_is_kept_as_measured_and_counted() -> Result<(), TapError> {
use std::time::{Duration, Instant};
let received = Instant::now();
let ms = Duration::from_millis;
let recipients = [(30, received + ms(5)), (40, received + ms(7))];
let tap = Tap::new("local", 64).with_depot(true);
let subscription = tap.subscribe(filter("orders"))?;
tap.name_station(7, "orders.worker")?;
tap.name_station(3, "orders.ledger")?;
tap.name_receiver(30, 3)?;
tap.name_station(4, "orders.audit")?;
tap.name_receiver(40, 4)?;
let observed = tap.observe(&PublishFact {
recorded: Some(Recorded {
recipients: &recipients,
received_at: received,
appended_at: Some(received + ms(6)),
}),
..fact("orders", b"{}")
});
assert_eq!(
observed,
Err(TapError::HopsOutOfOrder {
hop: 3,
at_ms: 5,
before_ms: 6,
})
);
let hop = |node: &str, at_ms: u64| (node.to_owned(), at_ms);
assert_eq!(
hops(&subscription.take_batches()?),
[vec![
hop("orders.worker", 0),
hop("local.depot", 6),
hop("local", 6),
hop("orders.ledger", 5),
hop("orders.audit", 7),
]],
"kept, each hop as measured"
);
Ok(())
}
fn named_tap() -> Result<Tap, TapError> {
let tap = Tap::new("local", 64).with_depot(true);
tap.name_station(7, "orders.worker")?;
tap.name_station(3, "orders.ledger")?;
tap.name_receiver(30, 3)?;
tap.name_station(4, "orders.audit")?;
tap.name_receiver(40, 4)?;
Ok(tap)
}
#[test]
fn a_hop_before_first_sight_is_left_out_and_counted() -> Result<(), Box<dyn std::error::Error>> {
use std::time::{Duration, Instant};
let received = Instant::now();
let ms = Duration::from_millis;
let appended = received.checked_sub(ms(2)).ok_or("no instant 2 ms back")?;
let recipients = [(30, received + ms(5)), (40, received + ms(7))];
let tap = named_tap()?;
let subscription = tap.subscribe(filter("orders"))?;
let observed = tap.observe(&PublishFact {
recorded: Some(Recorded {
recipients: &recipients,
received_at: received,
appended_at: Some(appended),
}),
..fact("orders", b"{}")
});
assert_eq!(
observed,
Err(TapError::HopBeforeFirstSight {
hop: 1,
early_ms: 2,
})
);
let hop = |node: &str, at_ms: u64| (node.to_owned(), at_ms);
assert_eq!(
hops(&subscription.take_batches()?),
[vec![
hop("orders.worker", 0),
hop("orders.ledger", 5),
hop("orders.audit", 7),
]],
"no depot hop, and no exchange hop at the append's time"
);
Ok(())
}
#[test]
fn every_failure_of_one_observe_is_reported() -> Result<(), Box<dyn std::error::Error>> {
use std::time::{Duration, Instant};
let received = Instant::now();
let ms = Duration::from_millis;
let appended = received.checked_sub(ms(2)).ok_or("no instant 2 ms back")?;
let recipients = [(30, received + ms(5)), (40, received + ms(3))];
let tap = named_tap()?;
let _subscription = tap.subscribe(filter("orders"))?;
let observed = tap.observe(&PublishFact {
recorded: Some(Recorded {
recipients: &recipients,
received_at: received,
appended_at: Some(appended),
}),
..fact("orders", b"{}")
});
let expected = TapError::Several(vec![
TapError::HopBeforeFirstSight {
hop: 1,
early_ms: 2,
},
TapError::HopsOutOfOrder {
hop: 4,
at_ms: 3,
before_ms: 5,
},
]);
assert_eq!(observed, Err(expected.clone()));
assert_eq!(expected.failures(), 2);
Ok(())
}
#[test]
fn an_exchange_no_clock_measured_has_no_hop() -> Result<(), Box<dyn std::error::Error>> {
use std::time::{Duration, Instant};
let received = Instant::now();
let ms = Duration::from_millis;
let recipients = [(30, received + ms(5)), (40, received + ms(7))];
let tap = named_tap()?;
let subscription = tap.subscribe(filter("orders"))?;
tap.observe(&PublishFact {
recorded: Some(Recorded {
recipients: &recipients,
received_at: received,
appended_at: None,
}),
..fact("orders", b"{}")
})?;
tap.observe(&fact("orders", b"{}"))?;
let hop = |node: &str, at_ms: u64| (node.to_owned(), at_ms);
assert_eq!(
hops(&subscription.take_batches()?),
[
vec![
hop("orders.worker", 0),
hop("orders.ledger", 5),
hop("orders.audit", 7),
],
vec![hop("orders.worker", 0)],
]
);
Ok(())
}
#[test]
fn an_ephemeral_publish_counts_no_failure() -> Result<(), Box<dyn std::error::Error>> {
use std::time::{Duration, Instant};
let received = Instant::now();
let recipients = [(30, received + Duration::from_millis(5))];
let tap = named_tap()?;
let _subscription = tap.subscribe(filter("orders"))?;
let observed = tap.observe(&PublishFact {
recorded: Some(Recorded {
recipients: &recipients,
received_at: received,
appended_at: None,
}),
..fact("orders", b"{}")
});
assert_eq!(observed, Ok(true), "kept, with no failure to count");
Ok(())
}