#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum ExitClass {
Generic = 1,
Retryable = 2,
DataIntegrity = 3,
SchemaDrift = 4,
}
impl ExitClass {
pub fn code(self) -> i32 {
self as i32
}
}
#[derive(Debug)]
pub struct DataIntegrityError(String);
impl DataIntegrityError {
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl std::fmt::Display for DataIntegrityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for DataIntegrityError {}
#[derive(Debug)]
pub struct SchemaDriftError(String);
impl SchemaDriftError {
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl std::fmt::Display for SchemaDriftError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for SchemaDriftError {}
#[derive(Debug)]
pub struct PreclassifiedExit(pub i32);
impl std::fmt::Display for PreclassifiedExit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "child exited with status {}", self.0)
}
}
impl std::error::Error for PreclassifiedExit {}
pub fn classify_exit(err: &anyhow::Error) -> i32 {
if let Some(p) = err.downcast_ref::<PreclassifiedExit>() {
return p.0;
}
if err.downcast_ref::<SchemaDriftError>().is_some() {
return ExitClass::SchemaDrift.code();
}
if err.downcast_ref::<DataIntegrityError>().is_some()
|| err
.downcast_ref::<crate::manifest::ManifestInconsistency>()
.is_some()
{
return ExitClass::DataIntegrity.code();
}
if crate::pipeline::retry::classify_error(err).is_transient() {
return ExitClass::Retryable.code();
}
ExitClass::Generic.code()
}
pub type Result<T> = anyhow::Result<T>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_drift_marker_classifies_to_4() {
let err: anyhow::Error = SchemaDriftError::new("schema changed").into();
assert_eq!(classify_exit(&err), 4);
assert_eq!(ExitClass::SchemaDrift.code(), 4);
}
#[test]
fn data_integrity_marker_classifies_to_3() {
let err: anyhow::Error = DataIntegrityError::new("reconcile mismatch").into();
assert_eq!(classify_exit(&err), 3);
assert_eq!(ExitClass::DataIntegrity.code(), 3);
}
#[test]
fn manifest_inconsistency_classifies_to_3() {
let err: anyhow::Error = crate::manifest::ManifestInconsistency::DuplicatePartId(1).into();
assert_eq!(
classify_exit(&err),
3,
"manifest self-consistency failure is a data-integrity stop"
);
}
#[test]
fn transient_error_classifies_to_2_syntax_error_to_1() {
let transient = anyhow::anyhow!("connection reset by peer");
assert_eq!(
classify_exit(&transient),
2,
"connection reset is retryable"
);
let syntax = anyhow::anyhow!("syntax error at or near \"SELET\"");
assert_eq!(classify_exit(&syntax), 1, "a syntax error is not retryable");
}
#[test]
fn typed_markers_survive_anyhow_context_wrapping() {
let drift: anyhow::Error = SchemaDriftError::new("drift").into();
let wrapped = drift.context("export 'orders' failed");
assert_eq!(classify_exit(&wrapped), 4);
let dup: anyhow::Error = DataIntegrityError::new("dup").into();
let wrapped = dup.context("export 'orders' failed");
assert_eq!(classify_exit(&wrapped), 3);
}
#[test]
fn run_carries_typed_marker_through_multi_failure_context() {
let dup: anyhow::Error =
DataIntegrityError::new("export 'orders': cannot safely retry (would duplicate rows)")
.into();
let aggregated = dup.context("2 export(s) failed; representative error follows (also: export 'events': connection reset)");
assert_eq!(
classify_exit(&aggregated),
3,
"the carried data-integrity marker must survive run's multi-failure context wrapping"
);
}
#[test]
fn untyped_flattened_string_is_generic_not_string_matched() {
let bare = anyhow::anyhow!("export 'orders': 1 quality check(s) failed: row_count low");
assert_eq!(
classify_exit(&bare),
1,
"an un-typed string must NOT be string-matched into data-integrity"
);
}
#[test]
fn data_integrity_marker_display_is_verbatim() {
let msg = "export 'orders': 1 quality check(s) failed";
assert_eq!(format!("{}", DataIntegrityError::new(msg)), msg);
assert_eq!(format!("{}", SchemaDriftError::new(msg)), msg);
}
}