use crate::cancel::CancellationToken;
use crate::error::{Result, TimeoutError};
use crossbeam::channel;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
pub struct TimeoutGuard {
cancel_flag: Arc<AtomicBool>,
}
impl Drop for TimeoutGuard {
fn drop(&mut self) {
self.cancel_flag.store(true, Ordering::Release);
}
}
pub fn run_with_timeout<F, T>(timeout: Duration, operation: F) -> Result<T>
where
F: FnOnce(Arc<AtomicBool>) -> Result<T> + Send + 'static,
T: Send + 'static,
{
let effective_timeout = if timeout == Duration::from_secs(0) {
Duration::MAX
} else {
timeout
};
let cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag_thread = Arc::clone(&cancel_flag);
let cancel_flag_guard = Arc::clone(&cancel_flag);
let (tx, rx) = channel::bounded(1);
std::thread::spawn(move || {
let _guard = TimeoutGuard {
cancel_flag: cancel_flag_guard,
};
let result = operation(cancel_flag_thread);
let _ = tx.send(result); });
match rx.recv_timeout(effective_timeout) {
Ok(result) => result,
Err(channel::RecvTimeoutError::Timeout) => {
eprintln!("Warning: Plugin operation timed out after {timeout:?}");
Err(TimeoutError::Timeout(timeout).into())
}
Err(channel::RecvTimeoutError::Disconnected) => {
eprintln!("Warning: Plugin thread panicked during execution");
Err(TimeoutError::PluginPanic.into())
}
}
}
pub fn run_with_timeout_and_cancel<F, T>(
timeout: Duration,
cancel_token: Option<Arc<dyn CancellationToken>>,
operation: F,
) -> Result<T>
where
F: FnOnce(Arc<AtomicBool>) -> Result<T> + Send + 'static,
T: Send + 'static,
{
if let Some(ref token) = cancel_token {
if token.is_cancelled() {
return Err(crate::error::CrushError::Cancelled);
}
}
let effective_timeout = if timeout == Duration::from_secs(0) {
Duration::MAX
} else {
timeout
};
let cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag_thread = Arc::clone(&cancel_flag);
let cancel_flag_guard = Arc::clone(&cancel_flag);
let cancel_flag_monitor = Arc::clone(&cancel_flag);
let monitor_handle = if let Some(token) = cancel_token {
let handle = std::thread::spawn(move || {
while !cancel_flag_monitor.load(Ordering::Acquire) {
if token.is_cancelled() {
cancel_flag_monitor.store(true, Ordering::Release);
break;
}
std::thread::sleep(Duration::from_micros(100));
}
});
Some(handle)
} else {
None
};
let (tx, rx) = channel::bounded(1);
std::thread::spawn(move || {
let _guard = TimeoutGuard {
cancel_flag: cancel_flag_guard,
};
let result = operation(cancel_flag_thread);
let _ = tx.send(result); });
let result = match rx.recv_timeout(effective_timeout) {
Ok(result) => {
match result {
Err(crate::error::CrushError::Plugin(crate::error::PluginError::Cancelled)) => {
Err(crate::error::CrushError::Cancelled)
}
other => other,
}
}
Err(channel::RecvTimeoutError::Timeout) => {
cancel_flag.store(true, Ordering::Release);
eprintln!("Warning: Plugin operation timed out after {timeout:?}");
Err(TimeoutError::Timeout(timeout).into())
}
Err(channel::RecvTimeoutError::Disconnected) => {
eprintln!("Warning: Plugin thread panicked during execution");
Err(TimeoutError::PluginPanic.into())
}
};
cancel_flag.store(true, Ordering::Release);
if let Some(handle) = monitor_handle {
let _ = handle.join(); }
result
}
pub(crate) fn run_with_timeout_scoped<'scope, 'env, F, T>(
scope: &'scope std::thread::Scope<'scope, 'env>,
timeout: Duration,
operation: F,
) -> Result<T>
where
F: FnOnce(Arc<AtomicBool>) -> Result<T> + Send + 'scope,
T: Send + 'scope,
{
let effective_timeout = if timeout == Duration::from_secs(0) {
Duration::MAX
} else {
timeout
};
let cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag_thread = Arc::clone(&cancel_flag);
let cancel_flag_guard = Arc::clone(&cancel_flag);
let (tx, rx) = channel::bounded(1);
scope.spawn(move || {
let _guard = TimeoutGuard {
cancel_flag: cancel_flag_guard,
};
let result = operation(cancel_flag_thread);
let _ = tx.send(result);
});
match rx.recv_timeout(effective_timeout) {
Ok(result) => result,
Err(channel::RecvTimeoutError::Timeout) => {
cancel_flag.store(true, Ordering::Release);
eprintln!("Warning: Plugin operation timed out after {timeout:?}");
Err(TimeoutError::Timeout(timeout).into())
}
Err(channel::RecvTimeoutError::Disconnected) => {
eprintln!("Warning: Plugin thread panicked during execution");
Err(TimeoutError::PluginPanic.into())
}
}
}
pub(crate) fn run_with_timeout_and_cancel_scoped<'scope, 'env, F, T>(
scope: &'scope std::thread::Scope<'scope, 'env>,
timeout: Duration,
cancel_token: Option<Arc<dyn CancellationToken>>,
operation: F,
) -> Result<T>
where
F: FnOnce(Arc<AtomicBool>) -> Result<T> + Send + 'scope,
T: Send + 'scope,
{
if let Some(ref token) = cancel_token {
if token.is_cancelled() {
return Err(crate::error::CrushError::Cancelled);
}
}
let effective_timeout = if timeout == Duration::from_secs(0) {
Duration::MAX
} else {
timeout
};
let cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag_thread = Arc::clone(&cancel_flag);
let cancel_flag_guard = Arc::clone(&cancel_flag);
let cancel_flag_monitor = Arc::clone(&cancel_flag);
let monitor_handle = cancel_token.map(|token| {
scope.spawn(move || {
while !cancel_flag_monitor.load(Ordering::Acquire) {
if token.is_cancelled() {
cancel_flag_monitor.store(true, Ordering::Release);
break;
}
std::thread::sleep(Duration::from_micros(100));
}
})
});
let (tx, rx) = channel::bounded(1);
scope.spawn(move || {
let _guard = TimeoutGuard {
cancel_flag: cancel_flag_guard,
};
let result = operation(cancel_flag_thread);
let _ = tx.send(result);
});
let result = match rx.recv_timeout(effective_timeout) {
Ok(result) => match result {
Err(crate::error::CrushError::Plugin(crate::error::PluginError::Cancelled)) => {
Err(crate::error::CrushError::Cancelled)
}
other => other,
},
Err(channel::RecvTimeoutError::Timeout) => {
cancel_flag.store(true, Ordering::Release);
eprintln!("Warning: Plugin operation timed out after {timeout:?}");
Err(TimeoutError::Timeout(timeout).into())
}
Err(channel::RecvTimeoutError::Disconnected) => {
eprintln!("Warning: Plugin thread panicked during execution");
Err(TimeoutError::PluginPanic.into())
}
};
cancel_flag.store(true, Ordering::Release);
if let Some(handle) = monitor_handle {
let _ = handle.join();
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::PluginError;
#[test]
#[allow(clippy::unwrap_used)]
fn test_operation_completes_within_timeout() {
let timeout = Duration::from_secs(1);
let result = run_with_timeout(timeout, |_cancel| {
Ok(42)
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_operation_respects_cancellation() {
let timeout = Duration::from_millis(50);
let result = run_with_timeout(timeout, |cancel_flag| {
for _ in 0..1000 {
if cancel_flag.load(Ordering::Acquire) {
return Err(PluginError::Cancelled.into());
}
std::thread::sleep(Duration::from_millis(10));
}
Ok(42)
});
assert!(result.is_err());
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_zero_timeout_means_no_timeout() {
let timeout = Duration::from_secs(0);
let result = run_with_timeout(timeout, |_cancel| Ok(42));
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_timeout_guard_sets_flag_on_drop() {
let cancel_flag = Arc::new(AtomicBool::new(false));
{
let _guard = TimeoutGuard {
cancel_flag: Arc::clone(&cancel_flag),
};
assert!(!cancel_flag.load(Ordering::Acquire));
}
assert!(cancel_flag.load(Ordering::Acquire));
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_run_with_timeout_basic_success() {
let timeout = Duration::from_secs(1);
let result = run_with_timeout(timeout, |_cancel| Ok(100));
assert!(result.is_ok());
assert_eq!(result.unwrap(), 100);
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_run_with_timeout_operation_error() {
let timeout = Duration::from_secs(1);
let result: Result<i32> = run_with_timeout(timeout, |_cancel| {
Err(PluginError::OperationFailed("test error".to_string()).into())
});
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("test error"));
}
#[test]
fn test_timeout_error_display() {
let timeout_err = TimeoutError::Timeout(Duration::from_secs(30));
assert!(timeout_err.to_string().contains("30"));
let panic_err = TimeoutError::PluginPanic;
assert!(panic_err.to_string().contains("panicked"));
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_run_with_timeout_error_propagation() {
let timeout = Duration::from_secs(1);
let result: Result<i32> = run_with_timeout(timeout, |_cancel| {
Err(PluginError::OperationFailed("custom error".to_string()).into())
});
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("custom error"));
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_effective_timeout_conversion() {
let timeout = Duration::from_secs(0);
let result = run_with_timeout(timeout, |_cancel| {
std::thread::sleep(Duration::from_millis(10));
Ok("done".to_string())
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), "done");
}
}