use std::time::Duration;
use lmflow::{register_kernel, Graph, Kernel, KernelCtx, Packet, State, Timestamp};
#[derive(Default)]
struct BatchProbe;
impl Kernel for BatchProbe {
fn process(&mut self, cc: &mut KernelCtx) -> lmflow::Result<()> {
let mut parts = Vec::new();
for port in 0..cc.num_inputs() {
let tss: Vec<String> = (0..cc.input_count(port))
.filter_map(|k| cc.input_at(port, k).map(|p| p.timestamp().0.to_string()))
.collect();
parts.push(format!("p{port}=[{}]", tss.join(",")));
}
cc.emit(0, Packet::new(parts.join(" ")))
}
}
fn init() {
let _ = register_kernel::<BatchProbe>("BatchProbe");
}
fn drain(poller: &lmflow::Poller) -> Vec<String> {
std::iter::from_fn(|| poller.next())
.map(|p| p.get::<String>().cloned().unwrap_or_default())
.collect()
}
fn two_port_graph(capacity: usize, executor: bool) -> String {
let (execs, on) = if executor {
(
"executors:\n - { name: cpu, type: ThreadPoolExecutor, num_threads: 2 }\n",
" executor: cpu\n",
)
} else {
("", "")
};
format!(
r#"{execs}nodes:
- name: b
kernel: BatchProbe
{on} input_ports: ["x", "y"]
output_ports: ["out"]
input_policy: {{ type: batch, capacity: {capacity} }}
input_ports: ["x", "y"]
output_ports: ["out"]
"#
)
}
#[test]
fn aligned_streams_batch_together() {
init();
let graph = Graph::from_yaml(&two_port_graph(3, false)).unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let (x, y) = (graph.input("x").unwrap(), graph.input("y").unwrap());
for i in 0..6i64 {
x.send(Packet::new(i).at(Timestamp(i))).unwrap();
y.send(Packet::new(i * 10).at(Timestamp(i))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
assert_eq!(graph.state(), State::Terminated);
assert_eq!(
drain(&poller),
vec!["p0=[0,1,2] p1=[0,1,2]", "p0=[3,4,5] p1=[3,4,5]"],
"两口时间戳一致 → 每批 3 个对齐元组"
);
}
#[test]
fn sparse_port_yields_unequal_batch_sizes() {
init();
let graph = Graph::from_yaml(&two_port_graph(2, false)).unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let (x, y) = (graph.input("x").unwrap(), graph.input("y").unwrap());
for t in [0i64, 1, 2, 3] {
x.send(Packet::new(t).at(Timestamp(t))).unwrap();
}
for t in [0i64, 2] {
y.send(Packet::new(t).at(Timestamp(t))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
let got = drain(&poller);
assert_eq!(
got,
vec!["p0=[0,1] p1=[0]", "p0=[2,3] p1=[2]"],
"各口取数按对齐结果走,不硬凑成等长(否则就是错帧配对)"
);
}
#[test]
fn partial_batch_flushed_only_after_close() {
init();
let graph = Graph::from_yaml(&two_port_graph(4, false)).unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let (x, y) = (graph.input("x").unwrap(), graph.input("y").unwrap());
for i in 0..2i64 {
x.send(Packet::new(i).at(Timestamp(i))).unwrap();
y.send(Packet::new(i).at(Timestamp(i))).unwrap();
}
assert!(poller.try_next().is_none(), "不足一批且未关流时不该触发");
graph.close_all_inputs();
graph.wait_done().unwrap();
assert_eq!(
drain(&poller),
vec!["p0=[0,1] p1=[0,1]"],
"关流后把不足一批的余量刷出,不丢数据"
);
}
#[test]
fn does_not_fire_while_one_port_is_starved_and_open() {
init();
let graph = Graph::from_yaml(&two_port_graph(2, false)).unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let (x, y) = (graph.input("x").unwrap(), graph.input("y").unwrap());
for i in 0..4i64 {
x.send(Packet::new(i).at(Timestamp(i))).unwrap();
}
std::thread::sleep(Duration::from_millis(30));
assert!(
poller.try_next().is_none(),
"y 口空且未关闭 → 它还可能送来更早的包,不能先把 x 的批定下来"
);
for i in 0..2i64 {
y.send(Packet::new(i).at(Timestamp(i))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
let got = drain(&poller);
assert_eq!(got.first().map(String::as_str), Some("p0=[0,1] p1=[0,1]"));
}
#[test]
fn closing_one_port_unblocks_the_other() {
init();
let graph = Graph::from_yaml(&two_port_graph(2, false)).unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let (x, y) = (graph.input("x").unwrap(), graph.input("y").unwrap());
for i in 0..4i64 {
x.send(Packet::new(i).at(Timestamp(i))).unwrap();
}
drop(y);
graph.close_input("y").unwrap();
graph.close_input("x").unwrap();
graph.wait_done().unwrap();
assert_eq!(
drain(&poller),
vec!["p0=[0,1] p1=[]", "p0=[2,3] p1=[]"],
"y 关闭后 x 独自成批;y 这一侧本批为空"
);
}
#[test]
fn multi_port_batch_on_thread_pool() {
init();
let graph = Graph::from_yaml(&two_port_graph(4, true)).unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let (x, y) = (graph.input("x").unwrap(), graph.input("y").unwrap());
for i in 0..8i64 {
x.send(Packet::new(i).at(Timestamp(i))).unwrap();
y.send(Packet::new(i).at(Timestamp(i))).unwrap();
}
graph.close_all_inputs();
graph
.wait_done_timeout(Duration::from_secs(30))
.expect("should terminate");
assert_eq!(
drain(&poller),
vec!["p0=[0,1,2,3] p1=[0,1,2,3]", "p0=[4,5,6,7] p1=[4,5,6,7]"],
"线程池下批的划分与顺序都不该变"
);
}
#[test]
fn single_port_batch_unchanged() {
init();
let graph = Graph::from_yaml(
r#"
nodes:
- name: b
kernel: BatchProbe
input_ports: ["in"]
output_ports: ["out"]
input_policy: { type: batch, capacity: 3 }
input_ports: ["in"]
output_ports: ["out"]
"#,
)
.unwrap();
let poller = graph.add_poller("out").unwrap();
graph.start().unwrap();
let input = graph.input("in").unwrap();
for i in 0..7i64 {
input.send(Packet::new(i).at(Timestamp(i))).unwrap();
}
graph.close_all_inputs();
graph.wait_done().unwrap();
assert_eq!(
drain(&poller),
vec!["p0=[0,1,2]", "p0=[3,4,5]", "p0=[6]"],
"满批 ×2 + 关流刷余批"
);
}