use std::io;
use std::sync::Arc;
use thiserror::Error;
use crate::compressor::error::EncodeError;
#[derive(Error, Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum ParallelEncodeError {
#[error("a segment failed to compress")]
Encode(#[from] EncodeError),
#[error("the stream was declared as {declared} bytes but received {actual}")]
InputSizeMismatch {
declared: u64,
actual: u64,
},
#[error("the batch cannot be assembled: a task never reported")]
InvalidState,
#[error("the parallel batch failed")]
BatchFailed(#[source] Arc<ParallelEncodeError>),
#[error("the parallel stream failed")]
StreamFailed(#[source] Arc<ParallelEncodeError>),
#[error("every handle of the parallel task queue was dropped")]
TaskQueueClosed,
#[error("the task for segment {segment} was dropped without running")]
TaskAbandoned {
segment: u64,
},
#[error("the task for segment {segment} panicked")]
TaskPanicked {
segment: u64,
},
}
impl ParallelEncodeError {
fn io_kind(&self) -> io::ErrorKind {
match self {
Self::BatchFailed(cause) | Self::StreamFailed(cause) => cause.io_kind(),
Self::Encode(EncodeError::OutputTooSmall { .. }) => io::ErrorKind::WriteZero,
Self::Encode(EncodeError::InputSizeMismatch { .. } | EncodeError::Bound(_))
| Self::InputSizeMismatch { .. } => io::ErrorKind::InvalidInput,
Self::Encode(EncodeError::InvalidState | EncodeError::Internal)
| Self::InvalidState
| Self::TaskQueueClosed
| Self::TaskAbandoned { .. }
| Self::TaskPanicked { .. } => io::ErrorKind::Other,
}
}
}
impl From<ParallelEncodeError> for io::Error {
fn from(value: ParallelEncodeError) -> Self {
Self::new(value.io_kind(), value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compressor::config::SizeOverflow;
use std::error::Error as _;
fn kind(error: ParallelEncodeError) -> io::ErrorKind {
io::Error::from(error).kind()
}
#[test]
fn io_conversion_maps_every_variant_to_a_non_retryable_kind() {
let cases = [
(
ParallelEncodeError::Encode(EncodeError::OutputTooSmall { provided: 1 }),
io::ErrorKind::WriteZero,
),
(
ParallelEncodeError::Encode(EncodeError::InputSizeMismatch { declared: 1 }),
io::ErrorKind::InvalidInput,
),
(
ParallelEncodeError::Encode(EncodeError::Bound(SizeOverflow)),
io::ErrorKind::InvalidInput,
),
(
ParallelEncodeError::Encode(EncodeError::InvalidState),
io::ErrorKind::Other,
),
(
ParallelEncodeError::Encode(EncodeError::Internal),
io::ErrorKind::Other,
),
(
ParallelEncodeError::InputSizeMismatch {
declared: 1,
actual: 2,
},
io::ErrorKind::InvalidInput,
),
(ParallelEncodeError::InvalidState, io::ErrorKind::Other),
(ParallelEncodeError::TaskQueueClosed, io::ErrorKind::Other),
(
ParallelEncodeError::TaskAbandoned { segment: 0 },
io::ErrorKind::Other,
),
(
ParallelEncodeError::TaskPanicked { segment: 0 },
io::ErrorKind::Other,
),
];
for (error, expected) in cases {
let label = format!("{error:?}");
let kind = kind(error);
assert_eq!(kind, expected, "{label}");
assert_ne!(kind, io::ErrorKind::Interrupted);
assert_ne!(kind, io::ErrorKind::WouldBlock);
}
}
#[test]
fn io_conversion_takes_the_kind_of_a_wrapped_cause_and_keeps_the_chain() {
let root = Arc::new(ParallelEncodeError::Encode(EncodeError::OutputTooSmall {
provided: 3,
}));
let stream = ParallelEncodeError::StreamFailed(Arc::clone(&root));
let batch = ParallelEncodeError::BatchFailed(Arc::new(stream));
let error = io::Error::from(batch);
assert_eq!(error.kind(), io::ErrorKind::WriteZero);
let outer = error
.get_ref()
.and_then(|error| error.downcast_ref::<ParallelEncodeError>())
.unwrap();
assert!(matches!(outer, ParallelEncodeError::BatchFailed(_)));
let middle = outer.source().unwrap();
assert_eq!(middle.to_string(), "the parallel stream failed");
let cause = middle
.source()
.and_then(|error| error.downcast_ref::<Arc<ParallelEncodeError>>())
.unwrap();
assert_eq!(cause, &root);
}
#[test]
fn messages_name_what_failed() {
assert_eq!(
ParallelEncodeError::TaskAbandoned { segment: 3 }.to_string(),
"the task for segment 3 was dropped without running"
);
assert_eq!(
ParallelEncodeError::TaskPanicked { segment: 4 }.to_string(),
"the task for segment 4 panicked"
);
assert_eq!(
ParallelEncodeError::TaskQueueClosed.to_string(),
"every handle of the parallel task queue was dropped"
);
assert_eq!(
ParallelEncodeError::BatchFailed(Arc::new(ParallelEncodeError::InvalidState))
.to_string(),
"the parallel batch failed"
);
}
}