use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum RuntimeError {
#[error("shutdown timeout {grace:?} exceeded; stuck: {stuck:?}; forcing termination")]
GraceExceeded {
grace: Duration,
stuck: Vec<Arc<str>>,
},
#[error("task '{name}' already exists in registry")]
TaskAlreadyExists {
name: Arc<str>,
},
#[error("task '{name}' not found in registry")]
TaskNotFound {
name: Arc<str>,
},
#[error("timeout waiting for task '{name}' removal after {timeout:?}")]
TaskRemoveTimeout {
name: Arc<str>,
timeout: Duration,
},
#[error("timeout waiting for task '{name}' registration after {timeout:?}")]
TaskAddTimeout {
name: Arc<str>,
timeout: Duration,
},
#[error("supervisor is shutting down")]
ShuttingDown,
}
impl RuntimeError {
pub fn as_label(&self) -> &'static str {
match self {
RuntimeError::GraceExceeded { .. } => "runtime_grace_exceeded",
RuntimeError::TaskAlreadyExists { .. } => "runtime_task_already_exists",
RuntimeError::TaskNotFound { .. } => "runtime_task_not_found",
RuntimeError::TaskRemoveTimeout { .. } => "runtime_task_remove_timeout",
RuntimeError::TaskAddTimeout { .. } => "runtime_task_add_timeout",
RuntimeError::ShuttingDown => "runtime_shutting_down",
}
}
}
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum TaskError {
#[error("timed out after {timeout:?}")]
Timeout { timeout: Duration },
#[error("fatal error (no retry): {reason}")]
Fatal {
reason: String,
exit_code: Option<i32>,
},
#[error("execution failed: {reason}")]
Fail {
reason: String,
exit_code: Option<i32>,
},
#[error("context canceled")]
Canceled,
}
impl TaskError {
pub fn as_label(&self) -> &'static str {
match self {
TaskError::Timeout { .. } => "task_timeout",
TaskError::Fatal { .. } => "task_fatal",
TaskError::Fail { .. } => "task_failed",
TaskError::Canceled => "task_canceled",
}
}
pub fn is_retryable(&self) -> bool {
matches!(self, TaskError::Timeout { .. } | TaskError::Fail { .. })
}
pub fn is_fatal(&self) -> bool {
matches!(self, TaskError::Fatal { .. })
}
pub fn exit_code(&self) -> Option<i32> {
match self {
TaskError::Fatal { exit_code, .. } | TaskError::Fail { exit_code, .. } => *exit_code,
TaskError::Timeout { .. } | TaskError::Canceled => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exit_code_is_some_for_fail_with_code() {
let e = TaskError::Fail {
reason: "x".into(),
exit_code: Some(5),
};
assert_eq!(e.exit_code(), Some(5));
assert!(e.is_retryable());
assert!(!e.is_fatal());
}
#[test]
fn exit_code_is_some_for_fatal_with_code() {
let e = TaskError::Fatal {
reason: "x".into(),
exit_code: Some(137),
};
assert_eq!(e.exit_code(), Some(137));
assert!(!e.is_retryable());
assert!(e.is_fatal());
}
#[test]
fn exit_code_is_none_for_logical_fail() {
let e = TaskError::Fail {
reason: "logical".into(),
exit_code: None,
};
assert_eq!(e.exit_code(), None);
}
#[test]
fn exit_code_is_none_for_timeout_and_canceled() {
use std::time::Duration;
assert_eq!(
TaskError::Timeout {
timeout: Duration::from_secs(1),
}
.exit_code(),
None,
);
assert_eq!(TaskError::Canceled.exit_code(), None);
}
#[test]
fn display_still_renders_reason_only() {
let e = TaskError::Fail {
reason: "boom".into(),
exit_code: Some(1),
};
assert_eq!(e.to_string(), "execution failed: boom");
}
}