use std::future::Future;
pub(super) struct JobSignals {
#[cfg(unix)]
int: tokio::signal::unix::Signal,
#[cfg(unix)]
term: tokio::signal::unix::Signal,
}
impl JobSignals {
#[cfg(unix)]
pub(super) fn arm() -> Self {
Self {
int: tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
.expect("Failed to install SIGINT handler"), term: tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("Failed to install SIGTERM handler"), }
}
#[cfg(not(unix))]
pub(super) fn arm() -> Self {
Self {}
}
pub(super) async fn next(&mut self) {
#[cfg(unix)]
tokio::select! {
_ = self.int.recv() => {}
_ = self.term.recv() => {}
}
#[cfg(not(unix))]
let _ = tokio::signal::ctrl_c().await;
}
pub(super) async fn force_exit(mut self) -> ! {
self.next().await;
tracing::warn!("camel job: second stop signal — forcing exit");
std::process::exit(1)
}
}
#[derive(Debug)]
pub(super) enum JobWaitOutcome<T> {
Completed(T),
Signaled,
}
pub(super) async fn await_job_operation_or_signal<S, F, T>(
signal: S,
operation: F,
) -> JobWaitOutcome<T>
where
S: Future<Output = ()>,
F: Future<Output = T>,
{
tokio::select! {
biased;
() = signal => JobWaitOutcome::Signaled,
value = operation => JobWaitOutcome::Completed(value),
}
}
#[cfg(test)]
mod signal_wait_tests {
use super::{JobWaitOutcome, await_job_operation_or_signal};
#[tokio::test]
async fn job_signal_wins_ready_tie() {
let outcome =
await_job_operation_or_signal(std::future::ready(()), std::future::ready("op")).await;
assert!(
matches!(outcome, JobWaitOutcome::Signaled),
"ready signal must win the tie, got {outcome:?}"
);
}
}