pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("required group is empty: {type_name}")]
EmptyGroup {
type_name: &'static str,
},
#[error("provide_group_dyn type mismatch: expected {expected}, got {actual}")]
GroupMemberTypeMismatch {
expected: &'static str,
actual: &'static str,
},
#[error("type already provided: {0}")]
AlreadyProvided(&'static str),
#[error("type already provided privately in module '{module}': {type_name}")]
AlreadyProvidedPrivate {
module: &'static str,
type_name: &'static str,
},
#[error("dependency cycle detected involving: {0}")]
Cycle(String),
#[error("dependency not constructed yet: {0}")]
NotConstructed(&'static str),
#[error("missing provider for type: {type_name} (from module '{module}')")]
MissingProvider {
type_name: &'static str,
module: &'static str,
},
#[error(
"provider for {provider} in module '{module}' needs a dependency nothing provides: {dependency}{}",
group_registration_hint(.dependency)
)]
ProviderMissingDep {
provider: &'static str,
module: &'static str,
dependency: &'static str,
},
#[error(
"invoker in module '{module}' needs a dependency nothing provides: {dependency}{}",
group_registration_hint(.dependency)
)]
InvokerMissingDep {
module: &'static str,
dependency: &'static str,
},
#[error("failed to downcast value to {0}")]
Downcast(&'static str),
#[error("constructor for {type_name} failed: {source}")]
ConstructorFailed {
type_name: &'static str,
#[source]
source: BoxError,
},
#[error("invoker {name} failed: {source}")]
InvokerFailed {
name: &'static str,
#[source]
source: BoxError,
},
#[error("cannot append lifecycle hook after start has finished")]
AppendAfterStart,
#[error("cannot append lifecycle hook while stopping")]
AppendWhileStopping,
#[error("application build timed out after {0:?}")]
BuildTimeout(std::time::Duration),
#[error("application start timed out after {0:?}")]
StartTimeout(std::time::Duration),
#[error("application stop timed out after {0:?}")]
StopTimeout(std::time::Duration),
#[error("application stop timed out after {0:?} while unwinding")]
UnwindTimeout(std::time::Duration),
#[error("background task failed during start")]
TaskFailedDuringStart,
#[error("failed to listen for SIGINT: {0}")]
SigintListen(#[source] std::io::Error),
#[error("failed to listen for SIGTERM: {0}")]
SigtermListen(#[source] std::io::Error),
#[error("failed to listen for process signal: {0}")]
SignalListen(#[source] std::io::Error),
#[error(transparent)]
MultipleStop(MultipleStopError),
#[error("cleanup failed after an earlier phase error: {cleanup}; earlier: {earlier}")]
CleanupAfterFailure {
#[source]
cleanup: Box<Error>,
earlier: Box<Error>,
},
#[error("{}", hook_failed_message(.name, .source))]
Hook {
name: &'static str,
#[source]
source: BoxError,
},
#[error("{context}: {source}")]
Io {
context: String,
#[source]
source: std::io::Error,
},
}
#[derive(Debug)]
#[non_exhaustive]
pub struct MultipleStopError {
count: usize,
summary: String,
errors: Vec<Error>,
}
impl MultipleStopError {
#[must_use]
pub fn count(&self) -> usize {
self.count
}
#[must_use]
pub fn summary(&self) -> &str {
&self.summary
}
#[must_use]
pub fn errors(&self) -> &[Error] {
&self.errors
}
}
impl std::fmt::Display for MultipleStopError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} OnStop hooks failed: {}", self.count, self.summary)
}
}
impl std::error::Error for MultipleStopError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.errors
.first()
.map(|e| e as &(dyn std::error::Error + 'static))
}
}
impl Error {
pub fn hook(err: impl Into<BoxError>) -> Self {
Self::Hook {
name: "unnamed",
source: err.into(),
}
}
pub(crate) fn with_hook_name(self, hook_name: &'static str) -> Self {
match self {
Self::Hook { source, .. } => Self::Hook {
name: hook_name,
source,
},
other => other,
}
}
pub fn io(context: impl Into<String>, source: std::io::Error) -> Self {
Self::Io {
context: context.into(),
source,
}
}
pub(crate) fn constructor_failed<T: ?Sized>(err: impl Into<BoxError>) -> Self {
Self::ConstructorFailed {
type_name: std::any::type_name::<T>(),
source: err.into(),
}
}
pub(crate) fn invoker_failed(name: &'static str, err: impl Into<BoxError>) -> Self {
Self::InvokerFailed {
name,
source: err.into(),
}
}
pub(crate) fn multiple_stop(errors: Vec<Error>) -> Self {
let count = errors.len();
let summary = errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ");
Self::MultipleStop(MultipleStopError {
count,
summary,
errors,
})
}
}
pub(crate) fn user_ctor_err<T: ?Sized>(err: impl Into<BoxError>) -> Error {
Error::constructor_failed::<T>(err)
}
pub(crate) fn user_invoke_err(name: &'static str, err: impl Into<BoxError>) -> Error {
Error::invoker_failed(name, err)
}
fn hook_failed_message(name: &'static str, source: &BoxError) -> String {
format!("hook '{name}' failed: {source}")
}
fn group_registration_hint(dependency: &str) -> &'static str {
if dependency.contains("Group<") {
"; register the group with init_group, provide_group, or require_group"
} else {
""
}
}
pub(crate) fn aggregate_errors(errors: Vec<Error>) -> Result<()> {
match errors.len() {
0 => Ok(()),
1 => Err(errors.into_iter().next().expect("len checked")),
_ => Err(Error::multiple_stop(errors)),
}
}
pub(crate) fn combine_results(earlier: Result<()>, later: Result<()>) -> Result<()> {
match earlier {
Ok(()) => later,
Err(e) => Err(with_cleanup(e, later)),
}
}
pub(crate) fn with_cleanup(earlier: Error, cleanup: Result<()>) -> Error {
match cleanup {
Ok(()) => earlier,
Err(cleanup) => Error::CleanupAfterFailure {
cleanup: Box::new(cleanup),
earlier: Box::new(earlier),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
#[test]
fn hook_preserves_io_source() {
let err = Error::hook(std::io::Error::other("disk full"));
let src = StdError::source(&err).expect("source");
assert!(src.to_string().contains("disk full"), "source was {src}");
}
#[test]
fn constructor_failed_preserves_source() {
let err = Error::constructor_failed::<u32>("nope");
let src = StdError::source(&err).expect("source");
assert!(src.to_string().contains("nope"), "source was {src}");
assert!(
err.to_string().contains("constructor for"),
"display was {err}"
);
}
#[test]
fn io_helper_sets_context_and_source() {
let err = Error::io("bind", std::io::Error::other("addr in use"));
match &err {
Error::Io { context, source } => {
assert_eq!(context, "bind");
assert!(source.to_string().contains("addr in use"));
}
other => panic!("expected Io, got {other}"),
}
assert!(StdError::source(&err).is_some());
}
#[test]
fn cleanup_after_failure_source_is_cleanup() {
let err = Error::CleanupAfterFailure {
cleanup: Box::new(Error::hook("cleanup")),
earlier: Box::new(Error::hook("earlier")),
};
let src = StdError::source(&err).expect("source");
assert!(src.to_string().contains("cleanup"), "source was {src}");
}
#[test]
fn with_cleanup_ok_is_the_phase_error() {
let err = with_cleanup(Error::hook("earlier"), Ok(()));
assert!(err.to_string().contains("earlier"), "display was {err}");
assert!(!err.to_string().contains("cleanup failed"));
}
#[test]
fn with_cleanup_err_wraps_both() {
let err = with_cleanup(Error::hook("earlier"), Err(Error::hook("cleanup")));
match err {
Error::CleanupAfterFailure { cleanup, earlier } => {
assert!(cleanup.to_string().contains("cleanup"));
assert!(earlier.to_string().contains("earlier"));
}
other => panic!("expected CleanupAfterFailure, got {other}"),
}
}
#[test]
fn multiple_stop_source_is_first_error() {
let err = Error::multiple_stop(vec![Error::hook("stop-a"), Error::hook("stop-b")]);
let src = StdError::source(&err).expect("source");
assert!(src.to_string().contains("stop-a"), "source was {src}");
match &err {
Error::MultipleStop(inner) => {
assert_eq!(inner.count(), 2);
assert!(inner.summary().contains("stop-a"));
assert!(inner.errors()[1].to_string().contains("stop-b"));
}
other => panic!("expected MultipleStop, got {other}"),
}
}
#[test]
fn hook_display_includes_context() {
let err = Error::hook("boom");
let msg = err.to_string();
assert!(msg.contains("boom"), "unexpected: {msg}");
assert!(msg.contains("hook 'unnamed' failed"), "unexpected: {msg}");
}
#[test]
fn with_hook_name_labels_unnamed_hook() {
let err = Error::hook("boom").with_hook_name("http");
match &err {
Error::Hook { name, .. } => assert_eq!(*name, "http"),
other => panic!("expected Hook, got {other}"),
}
let msg = err.to_string();
assert!(msg.contains("http"), "unexpected: {msg}");
assert!(msg.contains("boom"), "unexpected: {msg}");
}
#[test]
fn with_hook_name_leaves_io_unchanged() {
let err =
Error::io("bind", std::io::Error::other("addr in use")).with_hook_name("http.serve");
match err {
Error::Io { context, .. } => assert_eq!(context, "bind"),
other => panic!("expected Io, got {other}"),
}
}
#[test]
fn invoker_failed_includes_function_name() {
let err = Error::invoker_failed("boot", "nope");
let msg = err.to_string();
assert!(msg.contains("boot"), "unexpected: {msg}");
assert!(msg.contains("nope"), "unexpected: {msg}");
let src = StdError::source(&err).expect("source");
assert!(src.to_string().contains("nope"), "source was {src}");
}
}