use tokio::sync::mpsc::UnboundedSender;
use tokio::task::JoinHandle;
use crate::processor::frame::Frame;
#[derive(Clone)]
pub struct SourceHandle {
head: UnboundedSender<Frame>,
}
impl SourceHandle {
pub fn new(head: UnboundedSender<Frame>) -> Self {
Self { head }
}
pub fn emit(&self, frame: Frame) -> Result<(), Frame> {
self.head.send(frame).map_err(|e| e.0)
}
pub fn end(&self) {
let _ = self.head.send(Frame::End { reason: None });
}
pub fn cancel(&self, reason: Option<String>) {
let _ = self.head.send(Frame::Cancel { reason });
}
}
pub struct SourcePump {
handle: JoinHandle<()>,
}
impl SourcePump {
pub fn spawn<F, Fut>(head: UnboundedSender<Frame>, f: F) -> Self
where
F: FnOnce(SourceHandle) -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
let handle = tokio::spawn(f(SourceHandle::new(head)));
Self { handle }
}
pub fn from_handle(handle: JoinHandle<()>) -> Self {
Self { handle }
}
pub fn abort(&self) {
self.handle.abort();
}
}
impl Drop for SourcePump {
fn drop(&mut self) {
self.handle.abort();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::{Pipeline, PipelineTask, PipelineTaskParams};
use crate::processor::frame::{Frame, StartParams};
use crate::processor::{Envelope, FrameProcessor, Link, ProcessorSetup};
use crate::Result;
use async_trait::async_trait;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct Guard {
started: Arc<AtomicUsize>,
data_before_start: Arc<AtomicUsize>,
data_after_start: Arc<AtomicUsize>,
}
#[async_trait]
impl FrameProcessor for Guard {
fn name(&self) -> &str {
"Guard"
}
async fn start(&mut self, _s: &ProcessorSetup, _p: &StartParams) -> Result<()> {
self.started.fetch_add(1, Ordering::SeqCst);
Ok(())
}
async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
if matches!(env.frame, Frame::Text(_)) {
if self.started.load(Ordering::SeqCst) == 0 {
self.data_before_start.fetch_add(1, Ordering::SeqCst);
} else {
self.data_after_start.fetch_add(1, Ordering::SeqCst);
}
}
link.push(env.meta, env.frame, env.direction).await;
Ok(())
}
}
#[tokio::test]
async fn source_pump_feeds_head_and_respects_start_handshake() {
let started = Arc::new(AtomicUsize::new(0));
let before = Arc::new(AtomicUsize::new(0));
let after = Arc::new(AtomicUsize::new(0));
let pipeline = Pipeline::new(vec![Box::new(Guard {
started: started.clone(),
data_before_start: before.clone(),
data_after_start: after.clone(),
})]);
let task = PipelineTask::new(pipeline, PipelineTaskParams::default(), vec![]);
let pump = SourcePump::spawn(task.queue_sender(), |head| async move {
for i in 0..3 {
if head.emit(Frame::Text(format!("m{i}"))).is_err() {
return;
}
}
head.end();
});
tokio::time::timeout(std::time::Duration::from_secs(5), task.run())
.await
.expect("task timed out")
.expect("run ok");
drop(pump);
assert_eq!(started.load(Ordering::SeqCst), 1, "start ran once");
assert_eq!(
before.load(Ordering::SeqCst),
0,
"no pumped data reached process_frame before start()"
);
assert_eq!(
after.load(Ordering::SeqCst),
3,
"all 3 pumped frames processed"
);
}
#[tokio::test]
async fn source_pump_aborts_reader_on_drop() {
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel::<Frame>();
let ticks = Arc::new(AtomicUsize::new(0));
let t2 = ticks.clone();
let pump = SourcePump::spawn(tx, |_head| async move {
loop {
t2.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
drop(pump);
let seen = ticks.load(Ordering::SeqCst);
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
assert_eq!(
ticks.load(Ordering::SeqCst),
seen,
"reader task stopped ticking after the pump was dropped"
);
}
}