use super::{SegmentId, TaskId};
use crate::compressor::{ConfigError, EncodeError};
use std::io;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ParallelConfigError {
#[error("invalid parallel segment size: {bytes}")]
InvalidSegmentSize {
bytes: usize,
},
#[error("invalid parallel task count: {count}")]
InvalidTaskCount {
count: usize,
},
#[error("invalid encoder configuration: {0}")]
Encoder(#[from] ConfigError),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ParallelEncodeError {
#[error("parallel frame header failed: {0}")]
FrameHeader(#[source] EncodeError),
#[error("parallel batch failed: {0}")]
BatchFailed(#[source] std::sync::Arc<ParallelEncodeError>),
#[error("parallel stream failed: {0}")]
StreamFailed(#[source] std::sync::Arc<ParallelEncodeError>),
#[error("parallel task queue was closed by its executors")]
TaskQueueClosed,
#[error("invalid parallel configuration: {0}")]
Config(#[from] ParallelConfigError),
#[error("parallel size calculation overflowed")]
SizeOverflow,
#[error("parallel allocation failed: {0}")]
Allocation(#[from] std::collections::TryReserveError),
#[error("parallel memory staging bound exceeds configured limit")]
MemoryStagingLimit,
#[error("parallel worker memory estimate exceeds configured limit")]
WorkerMemoryLimit,
#[error("source metadata failed: {0}")]
SourceMetadata(#[source] io::Error),
#[error("source changed during compression")]
SourceChanged,
#[error("source read failed for {task:?}, {segment:?}: {source}")]
SourceRead {
task: TaskId,
segment: SegmentId,
#[source]
source: io::Error,
},
#[error("encoding failed for {task:?}, {segment:?}: {source}")]
Encode {
task: TaskId,
segment: SegmentId,
#[source]
source: EncodeError,
},
#[error("staging failed for {task:?}: {source}")]
Staging {
task: TaskId,
#[source]
source: io::Error,
},
#[error("parallel task {task:?} was abandoned")]
TaskAbandoned {
task: TaskId,
},
#[error("parallel task {task:?} panicked")]
TaskPanicked {
task: TaskId,
},
#[error("parallel batch was cancelled")]
Cancelled,
#[error("parallel tasks were already taken")]
TasksAlreadyTaken,
#[error("parallel tasks are not ready")]
NotReady,
#[error("parallel fragment invariant failed: {0}")]
FragmentInvariant(&'static str),
#[error("assembly failed after {bytes_written} bytes: {source}")]
AssemblyIo {
bytes_written: u64,
#[source]
source: io::Error,
},
}
impl From<ParallelEncodeError> for io::Error {
fn from(value: ParallelEncodeError) -> Self {
Self::new(value.io_kind(), value)
}
}
impl ParallelEncodeError {
fn io_kind(&self) -> io::ErrorKind {
match self {
Self::BatchFailed(cause) | Self::StreamFailed(cause) => cause.io_kind(),
Self::Encode { .. } | Self::FrameHeader(_) => io::ErrorKind::Other,
Self::Allocation(_) | Self::MemoryStagingLimit | Self::WorkerMemoryLimit => {
io::ErrorKind::OutOfMemory
}
Self::Config(_) | Self::SizeOverflow => io::ErrorKind::InvalidInput,
Self::SourceChanged => io::ErrorKind::InvalidData,
Self::SourceMetadata(source)
| Self::SourceRead { source, .. }
| Self::Staging { source, .. }
| Self::AssemblyIo { source, .. } => match source.kind() {
io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock => io::ErrorKind::Other,
kind => kind,
},
Self::TaskQueueClosed
| Self::TaskAbandoned { .. }
| Self::TaskPanicked { .. }
| Self::Cancelled
| Self::TasksAlreadyTaken
| Self::NotReady
| Self::FragmentInvariant(_) => io::ErrorKind::Other,
}
}
}
#[derive(Debug)]
pub struct ParallelFinishError<W> {
pub writer: W,
pub error: ParallelEncodeError,
pub bytes_written: u64,
}
impl<W> std::fmt::Display for ParallelFinishError<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.error.fmt(f)
}
}
impl<W: std::fmt::Debug> std::error::Error for ParallelFinishError<W> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{error::Error as _, sync::Arc};
fn kind(error: ParallelEncodeError) -> io::ErrorKind {
io::Error::from(error).kind()
}
fn io_cause(kind: io::ErrorKind) -> io::Error {
io::Error::new(kind, "cause")
}
#[test]
fn io_conversion_maps_every_variant_to_a_non_retryable_kind() {
let task = TaskId(1);
let segment = SegmentId(2);
let allocation = Vec::<u8>::new().try_reserve(usize::MAX).unwrap_err();
let cases = [
(
ParallelEncodeError::FrameHeader(EncodeError::Internal),
io::ErrorKind::Other,
),
(
ParallelEncodeError::Allocation(allocation),
io::ErrorKind::OutOfMemory,
),
(
ParallelEncodeError::MemoryStagingLimit,
io::ErrorKind::OutOfMemory,
),
(
ParallelEncodeError::WorkerMemoryLimit,
io::ErrorKind::OutOfMemory,
),
(
ParallelEncodeError::Config(ParallelConfigError::InvalidTaskCount { count: 0 }),
io::ErrorKind::InvalidInput,
),
(
ParallelEncodeError::SizeOverflow,
io::ErrorKind::InvalidInput,
),
(
ParallelEncodeError::SourceChanged,
io::ErrorKind::InvalidData,
),
(
ParallelEncodeError::Encode {
task,
segment,
source: EncodeError::Internal,
},
io::ErrorKind::Other,
),
(
ParallelEncodeError::SourceMetadata(io_cause(io::ErrorKind::NotFound)),
io::ErrorKind::NotFound,
),
(
ParallelEncodeError::SourceRead {
task,
segment,
source: io_cause(io::ErrorKind::Interrupted),
},
io::ErrorKind::Other,
),
(
ParallelEncodeError::Staging {
task,
source: io_cause(io::ErrorKind::WouldBlock),
},
io::ErrorKind::Other,
),
(
ParallelEncodeError::AssemblyIo {
bytes_written: 3,
source: io_cause(io::ErrorKind::BrokenPipe),
},
io::ErrorKind::BrokenPipe,
),
(ParallelEncodeError::TaskQueueClosed, io::ErrorKind::Other),
(
ParallelEncodeError::TaskAbandoned { task },
io::ErrorKind::Other,
),
(
ParallelEncodeError::TaskPanicked { task },
io::ErrorKind::Other,
),
(ParallelEncodeError::Cancelled, io::ErrorKind::Other),
(ParallelEncodeError::TasksAlreadyTaken, io::ErrorKind::Other),
(ParallelEncodeError::NotReady, io::ErrorKind::Other),
(
ParallelEncodeError::FragmentInvariant("detail"),
io::ErrorKind::Other,
),
];
for (error, expected) in cases {
let label = format!("{error:?}");
assert_eq!(kind(error), expected, "{label}");
}
}
#[test]
fn io_conversion_takes_the_kind_of_a_wrapped_cause_and_keeps_the_chain() {
let root = Arc::new(ParallelEncodeError::WorkerMemoryLimit);
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::OutOfMemory);
let inner = error
.get_ref()
.and_then(|e| e.downcast_ref::<ParallelEncodeError>())
.unwrap();
assert!(matches!(inner, ParallelEncodeError::BatchFailed(_)));
assert!(inner.source().is_some());
}
}