use std::sync::Arc;
use std::time::Duration;
use super::super::super::{AutoFlush, FlushPolicy};
use super::support::{Harness, RECV_TIMEOUT, harness, harness_with_hooks, item};
use crate::streaming::messenger_mux::MuxConfig;
use crate::streaming::messenger_mux::protocol::{CloseReason, RecordType, SlotId};
use crate::streaming::sender::cached_finalized;
const CREDIT: u32 = 4096;
fn manual() -> MuxConfig {
MuxConfig {
flush_policy: FlushPolicy::Manual,
..MuxConfig::default()
}
}
fn auto(on_admission: bool, max_linger: Option<Duration>) -> MuxConfig {
MuxConfig {
flush_policy: FlushPolicy::Auto(AutoFlush {
on_admission,
max_linger,
}),
..MuxConfig::default()
}
}
async fn assert_nothing_written(harness: &Harness) {
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
harness.try_next_batch().is_none(),
"the policy said hold, so nothing may reach the wire"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn one_kick_writes_one_batch_carrying_every_staged_record() {
const SLOTS: u64 = 16;
let harness = harness(manual()).await;
let mut inlets = Vec::new();
for id in 0..SLOTS {
let (inlet, slot) = harness.open_credited(id, id, CREDIT).await;
inlets.push((inlet, slot));
}
for (index, (inlet, _)) in inlets.iter().enumerate() {
inlet.send(item(index as u32)).expect("stage a record");
}
harness.await_staged(SLOTS as usize).await;
assert_nothing_written(&harness).await;
harness.flush_batch();
let batch = harness.next_batch().await;
assert_eq!(
batch.records.len(),
SLOTS as usize,
"the pass must arrive as one batch, not several"
);
assert_eq!(
batch.slots().len(),
SLOTS as usize,
"and must carry every slot that was staged"
);
for record in &batch.records {
assert_eq!(record.kind, RecordType::Data);
}
assert_eq!(
harness.staged(),
0.0,
"and the write leaves nothing behind it"
);
assert!(
harness.try_next_batch().is_none(),
"one kick is one batch: nothing follows it"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_record_queued_between_the_drain_and_the_kick_still_makes_that_batch() {
let hooks = Arc::new(super::super::test_hooks::TestHooks::default());
let harness = harness_with_hooks(manual(), Some(Arc::clone(&hooks))).await;
let (inlet, _) = harness.open_credited(1, 1, CREDIT).await;
hooks.pause();
inlet.send(item(0)).expect("stage record A");
hooks.wait_until_parked().await;
inlet.send(item(1)).expect("queue record B");
harness.flush_batch();
hooks.release();
let batch = harness.next_batch().await;
assert_eq!(
batch.records.len(),
2,
"the first batch after the kick must carry both records — B was queued \
before the flush, so the flush owes it"
);
assert_eq!(batch.records[0].data, item(0));
assert_eq!(batch.records[1].data, item(1));
assert_eq!(harness.staged(), 0.0, "and nothing is left behind");
}
#[tokio::test(flavor = "multi_thread")]
async fn manual_has_no_timer_behind_it() {
let harness = harness(manual()).await;
let (inlet, _) = harness.open_credited(1, 1, CREDIT).await;
for n in 0..4u32 {
inlet.send(item(n)).expect("stage a record");
}
harness.await_staged(4).await;
tokio::time::sleep(Duration::from_secs(1)).await;
assert!(
harness.try_next_batch().is_none(),
"manual means manual: a forgotten flush is not rescued by a timer"
);
assert_eq!(
harness.staged(),
4.0,
"and the gauge is where an operator sees the producer that forgot"
);
harness.flush_batch();
assert_eq!(harness.next_batch().await.records.len(), 4);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_flush_with_nothing_staged_writes_nothing() {
let harness = harness(manual()).await;
let (_inlet, _slot) = harness.open_credited(1, 1, CREDIT).await;
for _ in 0..10 {
harness.flush_batch();
}
assert_nothing_written(&harness).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn a_credit_reply_moves_under_manual() {
let harness = harness(manual()).await;
let peer_slot = SlotId::new(7, 0).expect("index fits u24");
harness
.handle
.reply(&[super::super::ReplyRecord::CreditUpdate {
slot: peer_slot,
delta: 32,
}]);
let batch = harness.next_batch().await;
assert_eq!(batch.records.len(), 1);
assert_eq!(batch.records[0].kind, RecordType::CreditUpdate);
assert_eq!(batch.records[0].slot, peer_slot);
assert_eq!(batch.records[0].credit, 32);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_close_reply_moves_under_manual() {
let harness = harness(manual()).await;
let peer_slot = SlotId::new(9, 1).expect("index fits u24");
harness
.handle
.reply(&[super::super::ReplyRecord::CloseSlot {
slot: peer_slot,
reason: CloseReason::UnknownSlot,
}]);
let batch = harness.next_batch().await;
assert_eq!(batch.records.len(), 1);
assert_eq!(batch.records[0].kind, RecordType::CloseSlot);
assert_eq!(batch.records[0].slot, peer_slot);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_terminal_and_its_close_move_under_manual() {
let harness = harness(manual()).await;
let (inlet, slot) = harness.open_credited(1, 1, CREDIT).await;
inlet.send(item(0)).expect("stage a record");
inlet
.send(cached_finalized().clone())
.expect("stage the terminal");
let batch = harness.next_batch().await;
assert_eq!(
batch.records.len(),
3,
"the record staged ahead of it rides along: it was owed to the consumer first"
);
assert_eq!(batch.records[0].kind, RecordType::Data);
assert_eq!(batch.records[1].kind, RecordType::Data);
assert_eq!(
batch.records[2].kind,
RecordType::CloseSlot,
"terminal-then-close is atomic in one batch"
);
assert_eq!(batch.records[2].slot, slot);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_byte_clamp_still_splits_a_batch_under_manual() {
const CAP: usize = 1024;
let harness = harness(MuxConfig {
max_batch_bytes: CAP,
..manual()
})
.await;
let (inlet, _) = harness.open_credited(1, 1, CREDIT).await;
for n in 0..256u32 {
inlet.send(item(n)).expect("stage a record");
}
let batch = harness.next_batch().await;
assert!(
batch.encoded_len <= CAP,
"the clamp cut the batch without anyone asking it to: {} bytes over a {CAP} cap",
batch.encoded_len
);
assert!(
!batch.records.is_empty() && batch.records.len() < 256,
"a cut, not the whole burst and not nothing: {} records",
batch.records.len()
);
let staged_before = harness.staged();
assert!(staged_before > 0.0, "the tail of the burst is still staged");
harness.flush_batch();
let mut seen = batch.records.len();
while seen < 256 {
seen += harness.next_batch().await.records.len();
}
assert_eq!(
seen, 256,
"every record arrives, across however many batches"
);
assert_eq!(harness.staged(), 0.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_kick_beats_an_auto_window_that_has_not_elapsed() {
let harness = harness(auto(false, Some(Duration::from_secs(30)))).await;
let (inlet, _) = harness.open_credited(1, 1, CREDIT).await;
for n in 0..5u32 {
inlet.send(item(n)).expect("stage a record");
}
harness.await_staged(5).await;
assert_nothing_written(&harness).await;
harness.flush_batch();
let batch = harness.next_batch().await;
assert_eq!(batch.records.len(), 5);
}
#[tokio::test(flavor = "multi_thread")]
async fn an_auto_window_writes_without_a_kick() {
let harness = harness(auto(false, Some(Duration::from_millis(50)))).await;
let (inlet, _) = harness.open_credited(1, 1, CREDIT).await;
for n in 0..3u32 {
inlet.send(item(n)).expect("stage a record");
}
let batch = tokio::time::timeout(RECV_TIMEOUT, async { harness.next_batch().await })
.await
.expect("the window must fire on its own");
assert_eq!(batch.records.len(), 3);
assert_eq!(harness.staged(), 0.0);
}
#[tokio::test(flavor = "current_thread")]
async fn kicks_during_an_admission_park_neither_double_send_nor_reorder() {
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use dashmap::DashMap;
use tokio_util::sync::CancellationToken;
use super::super::{BatcherContext, OpenSlotRequest, spawn};
use super::support::{OwnedBatch, StallingTransport, eventually};
use crate::messenger::Messenger;
use crate::observability::VeloMetrics;
use crate::streaming::messenger_mux::flow_control::SlotCredit;
const QUEUED: u32 = 120;
let (transport, wire) = StallingTransport::new(tokio::runtime::Handle::current());
let sender = Messenger::builder()
.add_transport(Arc::clone(&transport) as Arc<dyn velo_ext::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: manual(),
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(CREDIT),
slot_byte_budget: MuxConfig::default().slot_byte_budget,
ack: ack_tx,
})
.await
.expect("queue OpenSlot");
tokio::time::timeout(RECV_TIMEOUT, ack_rx)
.await
.expect("ack")
.expect("ack delivered")
.expect("slot allocated");
let open = 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
});
assert_eq!(open.records[0].kind, RecordType::OpenSlot);
let batches = |registry: &prometheus::Registry| {
crate::observability::test_helpers::MetricSnapshot::from_registry(registry)
.counter("velo_streaming_mux_batches_total", &[("direction", "sent")])
};
eventually(|| batches(®istry) == 1.0).await;
inlet.send(item(0)).expect("stage a record");
handle.kick_flush();
eventually(|| wire.is_full() && batches(®istry) == 2.0).await;
inlet.send(item(1)).expect("stage a record");
handle.kick_flush();
eventually(|| transport.stalled() == 1).await;
let parked_at = batches(®istry);
let offered_at_park = transport.offered();
for n in 2..QUEUED {
inlet.send(item(n)).expect("stage a record");
handle.kick_flush();
tokio::task::yield_now().await;
}
assert_eq!(
transport.offered(),
offered_at_park,
"once the batcher is parked in an admission nothing more may be offered \
to the messenger, however many times the application asks"
);
assert_eq!(
batches(®istry),
parked_at,
"and no further batch may even be built behind the parked one"
);
let mut seen: Vec<u32> = Vec::new();
while (seen.len() as u32) < QUEUED {
let (_, payload) = tokio::time::timeout(RECV_TIMEOUT, wire.recv_async())
.await
.expect("the batcher must resume once the gate drains")
.expect("wire open");
for record in OwnedBatch::decode(&payload).records {
assert_eq!(record.kind, RecordType::Data);
seen.push(record.frame_seq);
}
}
cancel.cancel();
let expected: Vec<u32> = (1..=QUEUED).collect();
assert_eq!(
seen, expected,
"every record exactly once and in order: a kick during a park must not \
re-send what the parked flush already took, nor let anything overtake it"
);
}