use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::time::Duration;
use dashmap::DashMap;
use tokio_util::sync::CancellationToken;
use super::super::*;
use super::support::*;
use crate::observability::VeloMetrics;
use crate::streaming::messenger_mux::protocol::RecordType;
use crate::streaming::sender::cached_finalized;
#[tokio::test(flavor = "multi_thread")]
async fn a_stalled_batcher_coalesces_control_instead_of_queueing_it() {
let (transport, wire) = StallingTransport::new(tokio::runtime::Handle::current());
let sender = Messenger::builder()
.add_transport(transport)
.build()
.await
.expect("sender messenger");
let peer_instance = velo_ext::InstanceId::new_v4();
sender
.register_peer(velo_ext::PeerInfo::new(
peer_instance,
velo_ext::WorkerAddress::from_encoded(
rmp_serde::to_vec(&std::collections::HashMap::from([(
"stalling".to_string(),
b"stalling".to_vec(),
)]))
.expect("encode"),
),
))
.expect("register peer");
let registry = prometheus::Registry::new();
let metrics = Arc::new(VeloMetrics::register(®istry).expect("register metrics"));
let cancel = CancellationToken::new();
let handle = spawn(
peer_instance.worker_id(),
BatcherContext {
messenger: Arc::clone(&sender),
config: MuxConfig::default(),
metrics: Some(metrics.bind_mux()),
epochs: Arc::new(AtomicU64::new(1)),
batchers: Arc::new(DashMap::new()),
cancel: cancel.clone(),
hooks: None,
},
);
let (inlet, inlet_rx) = flume::bounded::<Vec<u8>>(512);
let (ack_tx, ack_rx) = tokio::sync::oneshot::channel();
handle
.open_slot(OpenSlotRequest {
anchor_id: 1,
session_id: 1,
inlet: inlet_rx,
credit: SlotCredit::new(0),
slot_byte_budget: MuxConfig::default().slot_byte_budget,
ack: ack_tx,
})
.await
.expect("queue OpenSlot");
let opened = tokio::time::timeout(RECV_TIMEOUT, ack_rx)
.await
.expect("ack")
.expect("ack delivered");
assert!(opened.is_ok());
let open_batch = OwnedBatch::decode(&{
let (_, payload) = tokio::time::timeout(RECV_TIMEOUT, wire.recv_async())
.await
.expect("the OpenSlot flush must reach the wire")
.expect("wire open");
payload
});
let id = open_batch.records[0].slot;
handle.grant(id, 1);
inlet.send(item(0)).expect("queue record");
eventually(|| wire.is_full()).await;
handle.reply(&[ReplyRecord::CloseSlot {
slot: SlotId::from_raw(u32::MAX),
reason: CloseReason::UnknownSlot,
}]);
let batches = |registry: &prometheus::Registry| {
crate::observability::test_helpers::MetricSnapshot::from_registry(registry)
.counter("velo_streaming_mux_batches_total", &[("direction", "sent")])
};
eventually(|| batches(®istry) >= 3.0).await;
let parked_at = batches(®istry);
const QUEUED: u32 = 100;
for n in 1..QUEUED {
inlet.send(item(n)).expect("queue record");
}
const MERGED: u32 = 10_000;
let mut peak_pending = 0;
for _ in 0..MERGED {
handle.grant(id, 1);
handle.reply(&[ReplyRecord::CreditUpdate { slot: id, delta: 1 }]);
peak_pending = peak_pending.max(handle.pending_control());
}
assert_eq!(
batches(®istry),
parked_at,
"nothing may reach the messenger while the peer's gate is full"
);
assert!(
peak_pending <= 3,
"control must coalesce per slot, not queue: peaked at {peak_pending} entries \
against 20 000 writes"
);
let mut records: Vec<OwnedRecord> = Vec::new();
let deadline = tokio::time::Instant::now() + RECV_TIMEOUT;
let settled = loop {
if tokio::time::Instant::now() >= deadline {
break false;
}
match wire.try_recv() {
Ok((_, payload)) => records.extend(OwnedBatch::decode(&payload).records),
Err(_) => tokio::time::sleep(Duration::from_millis(2)).await,
}
if handle.pending_control() == 0
&& records
.iter()
.filter(|r| r.kind == RecordType::Data)
.count()
== QUEUED as usize
&& records.iter().any(|r| r.kind == RecordType::CreditUpdate)
{
break true;
}
};
assert!(
settled,
"the coalesced control must deliver once the peer un-parks: \
{} entries still pending, {} of {QUEUED} records out",
handle.pending_control(),
records
.iter()
.filter(|r| r.kind == RecordType::Data)
.count()
);
let credit: Vec<u32> = records
.iter()
.filter(|r| r.kind == RecordType::CreditUpdate)
.map(|r| r.credit)
.collect();
assert_eq!(
credit.iter().sum::<u32>(),
MERGED,
"coalescing must neither drop nor duplicate a delta"
);
assert!(
credit.len() < MERGED as usize / 4,
"ten thousand replies arrived as {} records — that is not coalescing",
credit.len()
);
assert!(
records
.iter()
.any(|r| r.kind == RecordType::CloseSlot && r.slot.raw() == u32::MAX),
"the control written while parked has to survive the park"
);
for (n, record) in records
.iter()
.filter(|r| r.kind == RecordType::Data)
.enumerate()
{
assert_eq!(record.data, item(n as u32), "record {n} out of order");
}
cancel.cancel();
}
#[tokio::test(flavor = "multi_thread")]
async fn a_singleton_failing_after_its_slot_closed_does_not_fail_the_epoch() {
let harness = harness(MuxConfig::default()).await;
let (inlet, stale) = harness.open(1, 1).await;
harness.grant(stale, 8);
inlet
.send(cached_finalized().clone())
.expect("queue terminal");
eventually(|| inlet.is_disconnected()).await;
while harness.try_next_batch().is_some() {}
let (reopened_inlet, reopened) = harness.open(1, 2).await;
assert_eq!(reopened.index(), stale.index());
assert_ne!(reopened.generation(), stale.generation());
harness.handle.control.singleton_resolved(stale, false);
eventually(|| {
harness.snapshot().counter(
"velo_streaming_mux_records_dropped_total",
&[("reason", "stale_singleton")],
) > 0.0
})
.await;
assert!(
!reopened_inlet.is_disconnected(),
"the slot that reused the index must survive its predecessor's answer"
);
assert_eq!(
harness
.snapshot()
.counter("velo_streaming_mux_epoch_deaths_total", &[]),
0.0,
"a stale answer is not evidence about the connection this epoch has"
);
harness.grant(reopened, 8);
reopened_inlet.send(item(7)).expect("send on reopened slot");
let mut seen = None;
while seen.is_none() {
seen = harness
.next_batch()
.await
.records
.into_iter()
.find(|r| r.kind == RecordType::Data);
}
assert_eq!(seen.expect("record").data, item(7));
}
#[tokio::test(flavor = "multi_thread")]
async fn epoch_death_fails_every_live_slot_exactly_once() {
let harness = harness(MuxConfig::default()).await;
let mut inlets = Vec::new();
let mut ids = Vec::new();
for session in 0..3u64 {
let (inlet, id) = harness.open(1, session).await;
harness.grant(id, 8);
inlets.push(inlet);
ids.push(id);
}
assert_eq!(
harness
.snapshot()
.gauge("velo_streaming_mux_live_slots", &[]),
3.0
);
harness.handle.control.singleton_resolved(ids[0], false);
for inlet in &inlets {
eventually(|| inlet.is_disconnected()).await;
}
let snapshot = harness.snapshot();
assert_eq!(
snapshot.counter("velo_streaming_mux_epoch_deaths_total", &[]),
1.0,
"one death, not one per slot"
);
assert_eq!(
snapshot.gauge("velo_streaming_mux_live_slots", &[]),
0.0,
"slots do not survive an epoch"
);
let (_inlet, id) = harness.open(1, 99).await;
assert!(
ids.iter().any(|prior| prior.index() == id.index()),
"the freed dense indices are reused, which is what the generation tag exists for"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_new_epoch_restarts_batch_sequences_and_bumps_generations() {
let harness = harness(MuxConfig::default()).await;
let (_inlet, first) = harness.open_with_header(1, 1).await;
let (first_slot, first_header) = first;
harness.handle.control.singleton_resolved(first_slot, false);
eventually(|| harness.handle.live_slots.load(Ordering::Relaxed) == 0).await;
let (_inlet, (second_slot, second_header)) = harness.open_with_header(1, 2).await;
assert_eq!(second_slot.index(), first_slot.index());
assert_eq!(
second_slot.generation(),
first_slot.generation().wrapping_add(1),
"reuse of a dense index has to be distinguishable from the original"
);
assert!(
second_header.peer_epoch > first_header.peer_epoch,
"a reconnect is a new epoch, so the peer can discard the old one's \
batches by header inspection"
);
assert_eq!(
second_header.batch_seq, 0,
"batch sequences are scoped by the epoch above them"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn cancelling_the_transport_closes_every_producer_channel() {
let harness = harness(MuxConfig::default()).await;
let (inlet_a, _) = harness.open(1, 1).await;
let (inlet_b, _) = harness.open(1, 2).await;
harness.cancel.cancel();
eventually(|| inlet_a.is_disconnected() && inlet_b.is_disconnected()).await;
assert_eq!(
harness
.snapshot()
.gauge("velo_streaming_mux_live_slots", &[]),
0.0
);
}