use std::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::error::Result;
use super::PipelineTask;
pub struct PipelineRunner {
handle_sigint: bool,
handle_sigterm: bool,
tokens: Mutex<Vec<CancellationToken>>,
}
impl PipelineRunner {
pub fn new(handle_sigint: bool, handle_sigterm: bool) -> Self {
Self {
handle_sigint,
handle_sigterm,
tokens: Mutex::new(Vec::new()),
}
}
pub async fn run(&self, task: PipelineTask) -> Result<()> {
let token = task.cancel_token();
if let Ok(mut t) = self.tokens.lock() {
t.push(token.clone());
}
let signal_token = token.clone();
let watcher_done = token.child_token();
let watcher_done2 = watcher_done.clone();
let handle_sigint = self.handle_sigint;
let handle_sigterm = self.handle_sigterm;
let signal_task = tokio::spawn(async move {
tokio::select! {
_ = watcher_done2.cancelled() => {}
_ = wait_for_signal(handle_sigint, handle_sigterm, &signal_token) => {}
}
});
let result = task.run().await;
watcher_done.cancel();
let _ = signal_task.await;
if let Ok(mut t) = self.tokens.lock() {
t.retain(|c| !c.is_cancelled());
}
result
}
pub async fn cancel_all(&self) {
let tokens = self.tokens.lock().map(|t| t.clone()).unwrap_or_default();
for token in tokens {
token.cancel();
}
}
}
async fn wait_for_signal(handle_sigint: bool, handle_sigterm: bool, token: &CancellationToken) {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let mut sigint = handle_sigint
.then(|| signal(SignalKind::interrupt()).ok())
.flatten();
let mut sigterm = handle_sigterm
.then(|| signal(SignalKind::terminate()).ok())
.flatten();
tokio::select! {
_ = token.cancelled() => {}
_ = async { if let Some(s) = sigint.as_mut() { s.recv().await; } else { std::future::pending::<()>().await } }, if sigint.is_some() => {
token.cancel();
}
_ = async { if let Some(s) = sigterm.as_mut() { s.recv().await; } else { std::future::pending::<()>().await } }, if sigterm.is_some() => {
token.cancel();
}
}
}
#[cfg(not(unix))]
{
let _ = (handle_sigint, handle_sigterm);
token.cancelled().await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::{Pipeline, PipelineTaskParams};
use crate::processor::{Envelope, FrameProcessor, Link, StopReason};
use async_trait::async_trait;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
struct Idle;
#[async_trait]
impl FrameProcessor for Idle {
fn name(&self) -> &str {
"Idle"
}
async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
link.push(env.meta, env.frame, env.direction).await;
Ok(())
}
}
#[tokio::test]
async fn simulated_signal_cancels_a_running_task() {
let params = PipelineTaskParams {
idle_timeout: None,
..Default::default()
};
let pipeline = Pipeline::new(vec![Box::new(Idle)]);
let cancelled = Arc::new(AtomicBool::new(false));
let c2 = cancelled.clone();
let mut task = PipelineTask::new(pipeline, params, vec![]);
task.on_finished(move |r| {
if r == StopReason::Cancelled {
c2.store(true, Ordering::Relaxed);
}
});
let runner = Arc::new(PipelineRunner::new(false, false));
let token = task.cancel_token();
let runner2 = runner.clone();
let run_handle = tokio::spawn(async move { runner2.run(task).await });
tokio::time::sleep(Duration::from_millis(50)).await;
let _ = token; runner.cancel_all().await;
tokio::time::timeout(Duration::from_secs(3), run_handle)
.await
.expect("runner timed out")
.expect("join")
.expect("run ok");
assert!(
cancelled.load(Ordering::Relaxed),
"task should cancel on signal"
);
}
#[tokio::test]
async fn multiple_tasks_join_cleanly() {
let runner = PipelineRunner::new(false, false);
for _ in 0..3 {
let pipeline = Pipeline::new(vec![Box::new(Idle)]);
let task = PipelineTask::new(pipeline, PipelineTaskParams::default(), vec![]);
task.stop_when_done().await;
runner.run(task).await.unwrap();
}
}
}