Skip to main content

oxide_batch/
chunk.rs

1//! Runtime-neutral chunk component and transaction-enlistment contracts.
2
3use std::error::Error;
4use std::fmt;
5
6use crate::{
7    BoxFuture, Checkpoint, ChunkCounts, ExecutionContext, FailureCategory, FaultProgress,
8    JobExecutionId, SkipCounts, StepExecutionId, StopToken,
9};
10
11/// Borrowed call state for a reader.
12#[derive(Clone, Copy, Debug)]
13pub struct ReadContext<'a> {
14    stop: &'a StopToken,
15}
16
17impl<'a> ReadContext<'a> {
18    /// Constructs a reader call scope.
19    #[must_use]
20    pub const fn new(stop: &'a StopToken) -> Self {
21        Self { stop }
22    }
23
24    /// Borrows the cooperative stop token.
25    #[must_use]
26    pub const fn stop_token(self) -> &'a StopToken {
27        self.stop
28    }
29}
30
31/// One item-reader call outcome.
32#[derive(Clone, Debug, Eq, PartialEq)]
33#[non_exhaustive]
34pub enum ReadOutcome<I> {
35    /// The next input item.
36    Item(I),
37    /// The source is exhausted normally.
38    EndOfInput,
39    /// Cooperative stop was observed before another item was produced.
40    Stopped,
41}
42
43/// A stateful asynchronous item source.
44pub trait ItemReader<I>: Send {
45    /// Reads at most one item while borrowing the reader and call scope.
46    fn read<'a>(
47        &'a mut self,
48        context: ReadContext<'a>,
49    ) -> BoxFuture<'a, Result<ReadOutcome<I>, ReaderError>>;
50}
51
52/// Borrowed call state for a processor.
53#[derive(Clone, Copy, Debug)]
54pub struct ProcessContext<'a> {
55    stop: &'a StopToken,
56}
57
58impl<'a> ProcessContext<'a> {
59    /// Constructs a processor call scope.
60    #[must_use]
61    pub const fn new(stop: &'a StopToken) -> Self {
62        Self { stop }
63    }
64
65    /// Borrows the cooperative stop token.
66    #[must_use]
67    pub const fn stop_token(self) -> &'a StopToken {
68        self.stop
69    }
70}
71
72/// One item-processor call outcome.
73#[derive(Clone, Debug, Eq, PartialEq)]
74#[non_exhaustive]
75pub enum ProcessOutcome<O> {
76    /// An output item was produced.
77    Item(O),
78    /// The input was intentionally filtered without producing output.
79    Filtered,
80    /// Cooperative stop was observed before output was produced.
81    Stopped,
82}
83
84/// A dynamically dispatchable asynchronous item transformer.
85pub trait ItemProcessor<I, O>: Send + Sync {
86    /// Processes one borrowed item.
87    fn process<'a>(
88        &'a self,
89        item: &'a I,
90        context: ProcessContext<'a>,
91    ) -> BoxFuture<'a, Result<ProcessOutcome<O>, ProcessorError>>;
92}
93
94/// A stable bound-value type for enlisted business statements.
95#[derive(Clone, Copy, Eq, PartialEq)]
96pub struct BusinessValue<'a>(BusinessValueInner<'a>);
97
98#[derive(Clone, Copy, Eq, PartialEq)]
99enum BusinessValueInner<'a> {
100    Text(&'a str),
101    Bytes(&'a [u8]),
102    I64(i64),
103    Bool(bool),
104    Null,
105}
106
107impl<'a> BusinessValue<'a> {
108    /// Constructs a borrowed UTF-8 value.
109    #[must_use]
110    pub const fn text(value: &'a str) -> Self {
111        Self(BusinessValueInner::Text(value))
112    }
113
114    /// Constructs a borrowed byte value.
115    #[must_use]
116    pub const fn bytes(value: &'a [u8]) -> Self {
117        Self(BusinessValueInner::Bytes(value))
118    }
119
120    /// Constructs a signed integer value.
121    #[must_use]
122    pub const fn i64(value: i64) -> Self {
123        Self(BusinessValueInner::I64(value))
124    }
125
126    /// Constructs a boolean value.
127    #[must_use]
128    pub const fn boolean(value: bool) -> Self {
129        Self(BusinessValueInner::Bool(value))
130    }
131
132    /// Constructs a database null value.
133    #[must_use]
134    pub const fn null() -> Self {
135        Self(BusinessValueInner::Null)
136    }
137
138    /// Returns the stable value kind.
139    #[must_use]
140    pub const fn kind(self) -> BusinessValueKind {
141        match self.0 {
142            BusinessValueInner::Text(_) => BusinessValueKind::Text,
143            BusinessValueInner::Bytes(_) => BusinessValueKind::Bytes,
144            BusinessValueInner::I64(_) => BusinessValueKind::I64,
145            BusinessValueInner::Bool(_) => BusinessValueKind::Bool,
146            BusinessValueInner::Null => BusinessValueKind::Null,
147        }
148    }
149
150    /// Borrows the UTF-8 value when present.
151    #[must_use]
152    pub const fn as_text(self) -> Option<&'a str> {
153        match self.0 {
154            BusinessValueInner::Text(value) => Some(value),
155            _ => None,
156        }
157    }
158
159    /// Borrows the byte value when present.
160    #[must_use]
161    pub const fn as_bytes(self) -> Option<&'a [u8]> {
162        match self.0 {
163            BusinessValueInner::Bytes(value) => Some(value),
164            _ => None,
165        }
166    }
167
168    /// Returns the signed integer when present.
169    #[must_use]
170    pub const fn as_i64(self) -> Option<i64> {
171        match self.0 {
172            BusinessValueInner::I64(value) => Some(value),
173            _ => None,
174        }
175    }
176
177    /// Returns the boolean when present.
178    #[must_use]
179    pub const fn as_bool(self) -> Option<bool> {
180        match self.0 {
181            BusinessValueInner::Bool(value) => Some(value),
182            _ => None,
183        }
184    }
185}
186
187impl fmt::Debug for BusinessValue<'_> {
188    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189        formatter
190            .debug_struct("BusinessValue")
191            .field("kind", &self.kind())
192            .field("value", &"<redacted>")
193            .finish()
194    }
195}
196
197/// Stable discriminator for a bound business value.
198#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
199#[non_exhaustive]
200pub enum BusinessValueKind {
201    /// UTF-8 text.
202    Text,
203    /// Arbitrary bytes.
204    Bytes,
205    /// Signed 64-bit integer.
206    I64,
207    /// Boolean.
208    Bool,
209    /// Database null.
210    Null,
211}
212
213/// A parameterized business write borrowed for one transaction call.
214pub struct BusinessStatement<'a> {
215    text: &'a str,
216    values: &'a [BusinessValue<'a>],
217}
218
219impl<'a> BusinessStatement<'a> {
220    /// Constructs a statement from SQL text and separately bound values.
221    ///
222    /// The `PostgreSQL` adapter binds every value; it never interpolates these
223    /// values into `text`.
224    #[must_use]
225    pub const fn new(text: &'a str, values: &'a [BusinessValue<'a>]) -> Self {
226        Self { text, values }
227    }
228
229    /// Borrows statement text for the authorized database adapter.
230    #[must_use]
231    pub const fn text(&self) -> &'a str {
232        self.text
233    }
234
235    /// Borrows separately bound values.
236    #[must_use]
237    pub const fn values(&self) -> &'a [BusinessValue<'a>] {
238        self.values
239    }
240}
241
242impl fmt::Debug for BusinessStatement<'_> {
243    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
244        formatter
245            .debug_struct("BusinessStatement")
246            .field("text", &"<redacted>")
247            .field("value_count", &self.values.len())
248            .finish()
249    }
250}
251
252/// Successful effect from one enlisted business statement.
253#[derive(Clone, Copy, Debug, Eq, PartialEq)]
254pub struct BusinessWriteResult {
255    rows_affected: u64,
256}
257
258impl BusinessWriteResult {
259    /// Constructs a result reported by a transaction adapter.
260    #[must_use]
261    pub const fn new(rows_affected: u64) -> Self {
262        Self { rows_affected }
263    }
264
265    /// Returns the database-reported affected-row count.
266    #[must_use]
267    pub const fn rows_affected(self) -> u64 {
268        self.rows_affected
269    }
270}
271
272/// OxideBatch-owned port for the currently enlisted business transaction.
273///
274/// The durable adapter owns the concrete transaction and lends this port to a
275/// writer only for the call. `SQLx` pool, connection, row, error, and transaction
276/// types do not cross this boundary.
277pub trait BusinessTransaction: Send {
278    /// Executes one parameterized business write.
279    fn execute<'a>(
280        &'a mut self,
281        statement: BusinessStatement<'a>,
282    ) -> BoxFuture<'a, Result<BusinessWriteResult, BusinessTransactionError>>;
283}
284
285/// Evidence returned after one chunk transaction is known to have committed.
286///
287/// The receipt owns the durable checkpoint and execution context so
288/// post-commit observers cannot borrow adapter-internal transaction state.
289#[derive(Clone, Debug, Eq, PartialEq)]
290pub struct ChunkCommitReceipt {
291    checkpoint: Checkpoint,
292    execution_context: ExecutionContext,
293}
294
295impl ChunkCommitReceipt {
296    /// Constructs committed durable-state evidence.
297    #[must_use]
298    pub const fn new(checkpoint: Checkpoint, execution_context: ExecutionContext) -> Self {
299        Self {
300            checkpoint,
301            execution_context,
302        }
303    }
304
305    /// Borrows the committed reader checkpoint.
306    #[must_use]
307    pub const fn checkpoint(&self) -> &Checkpoint {
308        &self.checkpoint
309    }
310
311    /// Borrows the committed execution context.
312    #[must_use]
313    pub const fn execution_context(&self) -> &ExecutionContext {
314        &self.execution_context
315    }
316}
317
318/// Stable, payload-redacted chunk-transaction failure.
319#[derive(Clone, Copy, Debug, Eq, PartialEq)]
320#[non_exhaustive]
321pub enum ChunkTransactionError {
322    /// The operation is known not to have committed.
323    NotCommitted,
324    /// The adapter cannot determine whether commit reached durable storage.
325    CommitOutcomeUnknown,
326}
327
328impl fmt::Display for ChunkTransactionError {
329    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
330        formatter.write_str(match self {
331            Self::NotCommitted => "chunk transaction did not commit",
332            Self::CommitOutcomeUnknown => "chunk transaction commit outcome is unknown",
333        })
334    }
335}
336
337impl Error for ChunkTransactionError {}
338
339/// The fault-tolerance progress one chunk commit makes authoritative.
340///
341/// The values are deltas contributed by a single chunk attempt. A durable
342/// adapter adds them to the committed totals it read when the transaction
343/// began, so replaying an uncommitted chunk after a crash cannot double-count.
344#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
345pub struct ChunkFaultProgress {
346    skips: SkipCounts,
347    no_rollbacks: u64,
348}
349
350impl ChunkFaultProgress {
351    /// The progress of a chunk that accepted no skip.
352    pub const NONE: Self = Self {
353        skips: SkipCounts::ZERO,
354        no_rollbacks: 0,
355    };
356
357    /// Constructs the delta contributed by one chunk attempt.
358    #[must_use]
359    pub const fn new(skips: SkipCounts, no_rollbacks: u64) -> Self {
360        Self {
361            skips,
362            no_rollbacks,
363        }
364    }
365
366    /// Returns the per-phase skips this chunk accepted.
367    #[must_use]
368    pub const fn skips(self) -> SkipCounts {
369        self.skips
370    }
371
372    /// Returns the accepted commit-safe skips this chunk committed.
373    #[must_use]
374    pub const fn no_rollbacks(self) -> u64 {
375        self.no_rollbacks
376    }
377}
378
379/// One adapter-owned transaction for a bounded chunk attempt.
380///
381/// The runtime invokes the writer while this value is open, then commits the
382/// supplied checked counters or rolls the transaction back. Implementations
383/// keep database-driver and serialization types private.
384pub trait ChunkTransaction: Send {
385    /// Reborrows an enlisted business transaction when the selected delivery
386    /// mode supports same-resource atomicity.
387    fn business_transaction(&mut self) -> Option<&mut dyn BusinessTransaction>;
388
389    /// Commits business work and the supplied progress, returning the durable
390    /// checkpoint and context that became authoritative.
391    ///
392    /// `fault` carries the skips this chunk accepted. A durable adapter also
393    /// clears the retained fault state of the superseded checkpoint generation
394    /// in this transaction, so a skip, its counters, and the checkpoint that
395    /// makes it authoritative commit or roll back together.
396    fn commit(
397        &mut self,
398        counts: ChunkCounts,
399        fault: ChunkFaultProgress,
400    ) -> BoxFuture<'_, Result<ChunkCommitReceipt, ChunkTransactionError>>;
401
402    /// Rolls back all provisional work in this chunk attempt.
403    fn rollback(&mut self) -> BoxFuture<'_, Result<(), ChunkTransactionError>>;
404}
405
406/// Repository execution identity for one launched chunk transaction.
407///
408/// Standalone chunk execution has no durable execution graph and therefore
409/// uses [`ChunkTransactionManager::begin`]. The repository-backed launcher
410/// supplies this context through [`ChunkTransactionManager::begin_for`].
411#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
412pub struct ChunkTransactionContext {
413    job_execution_id: JobExecutionId,
414    step_execution_id: StepExecutionId,
415}
416
417impl ChunkTransactionContext {
418    /// Constructs a durable chunk-transaction scope.
419    #[must_use]
420    pub const fn new(job_execution_id: JobExecutionId, step_execution_id: StepExecutionId) -> Self {
421        Self {
422            job_execution_id,
423            step_execution_id,
424        }
425    }
426
427    /// Returns the enclosing job execution.
428    #[must_use]
429    pub const fn job_execution_id(self) -> JobExecutionId {
430        self.job_execution_id
431    }
432
433    /// Returns the step execution whose progress is committed.
434    #[must_use]
435    pub const fn step_execution_id(self) -> StepExecutionId {
436        self.step_execution_id
437    }
438}
439
440/// Committed step progress one chunk-step attempt inherits.
441///
442/// A restart resumes bounded policy limits and stable retry-key identity from
443/// the durable state its attempt inherited, so a retry budget is not refilled
444/// by restarting the process.
445#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
446pub struct InheritedStepProgress {
447    read_ordinal: u64,
448    checkpoint_digest: [u8; 32],
449    fault: FaultProgress,
450}
451
452impl InheritedStepProgress {
453    /// The progress a standalone or first-attempt chunk step inherits.
454    pub const NONE: Self = Self {
455        read_ordinal: 0,
456        checkpoint_digest: [0; 32],
457        fault: FaultProgress::NONE,
458    };
459
460    /// Constructs inherited progress from durable step state.
461    #[must_use]
462    pub const fn new(read_ordinal: u64, checkpoint_digest: [u8; 32], fault: FaultProgress) -> Self {
463        Self {
464            read_ordinal,
465            checkpoint_digest,
466            fault,
467        }
468    }
469
470    /// Returns the stable reader ordinal the next chunk continues from.
471    #[must_use]
472    pub const fn read_ordinal(self) -> u64 {
473        self.read_ordinal
474    }
475
476    /// Returns the digest of the last committed checkpoint.
477    ///
478    /// Retry keys are derived from this generation, so an inherited digest
479    /// makes a reserved ordinal resumable after restart.
480    #[must_use]
481    pub const fn checkpoint_digest(self) -> [u8; 32] {
482        self.checkpoint_digest
483    }
484
485    /// Returns the inherited committed fault-tolerance totals.
486    #[must_use]
487    pub const fn fault(self) -> FaultProgress {
488        self.fault
489    }
490}
491
492/// Begins isolated adapter-owned chunk transactions.
493pub trait ChunkTransactionManager: Send + Sync {
494    /// Starts one transaction for a bounded chunk attempt.
495    fn begin(&self)
496    -> BoxFuture<'_, Result<Box<dyn ChunkTransaction + '_>, ChunkTransactionError>>;
497
498    /// Returns the durable progress this step attempt inherits.
499    ///
500    /// The default suits managers without durable state. A durable adapter
501    /// overrides it and fails closed rather than restarting bounded policy
502    /// limits from zero.
503    fn inherited_progress(
504        &self,
505        _context: ChunkTransactionContext,
506    ) -> BoxFuture<'_, Result<InheritedStepProgress, ChunkTransactionError>> {
507        Box::pin(std::future::ready(Ok(InheritedStepProgress::NONE)))
508    }
509
510    /// Starts one transaction bound to a durable repository execution.
511    ///
512    /// The default preserves managers that do not need repository identity.
513    /// Durable adapters override this method and reject unbound
514    /// [`Self::begin`] calls rather than guessing an execution target.
515    fn begin_for(
516        &self,
517        _context: ChunkTransactionContext,
518    ) -> BoxFuture<'_, Result<Box<dyn ChunkTransaction + '_>, ChunkTransactionError>> {
519        self.begin()
520    }
521}
522
523/// Stable, value-redacted enlisted-transaction failure.
524#[derive(Clone, Copy, Debug, Eq, PartialEq)]
525#[non_exhaustive]
526pub enum BusinessTransactionError {
527    /// The transaction cannot safely continue after an infrastructure failure.
528    Infrastructure,
529    /// The statement was rejected permanently.
530    Rejected,
531    /// Cooperative cancellation interrupted the operation.
532    Cancelled,
533}
534
535impl fmt::Display for BusinessTransactionError {
536    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
537        formatter.write_str(match self {
538            Self::Infrastructure => "business transaction infrastructure failed",
539            Self::Rejected => "business transaction statement was rejected",
540            Self::Cancelled => "business transaction operation was cancelled",
541        })
542    }
543}
544
545impl Error for BusinessTransactionError {}
546
547/// Borrowed call state for a writer.
548pub struct WriteContext<'a> {
549    stop: &'a StopToken,
550    transaction: Option<&'a mut dyn BusinessTransaction>,
551}
552
553impl<'a> WriteContext<'a> {
554    /// Constructs a non-enlisted writer call scope.
555    #[must_use]
556    pub const fn non_transactional(stop: &'a StopToken) -> Self {
557        Self {
558            stop,
559            transaction: None,
560        }
561    }
562
563    /// Constructs a writer call enlisted in an OxideBatch-owned transaction.
564    #[must_use]
565    pub fn enlisted(stop: &'a StopToken, transaction: &'a mut dyn BusinessTransaction) -> Self {
566        Self {
567            stop,
568            transaction: Some(transaction),
569        }
570    }
571
572    /// Borrows the cooperative stop token.
573    #[must_use]
574    pub const fn stop_token(&self) -> &'a StopToken {
575        self.stop
576    }
577
578    /// Reborrows the enlisted transaction when one is present.
579    #[must_use]
580    pub fn transaction(&mut self) -> Option<&mut (dyn BusinessTransaction + 'a)> {
581        self.transaction.as_deref_mut()
582    }
583
584    /// Returns whether this call participates in the chunk transaction.
585    #[must_use]
586    pub const fn is_enlisted(&self) -> bool {
587        self.transaction.is_some()
588    }
589}
590
591impl fmt::Debug for WriteContext<'_> {
592    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
593        formatter
594            .debug_struct("WriteContext")
595            .field("stop_requested", &self.stop.is_stop_requested())
596            .field("enlisted", &self.transaction.is_some())
597            .finish()
598    }
599}
600
601/// One item-writer call outcome.
602#[derive(Clone, Copy, Debug, Eq, PartialEq)]
603#[non_exhaustive]
604pub enum WriteOutcome {
605    /// Every supplied item was accepted by the writer.
606    Written,
607    /// Cooperative stop was observed before the batch was accepted.
608    Stopped,
609}
610
611/// A dynamically dispatchable asynchronous batch writer.
612pub trait ItemWriter<I>: Send + Sync {
613    /// Writes one borrowed, nonempty batch.
614    ///
615    /// A durable `PostgreSQL` step supplies an enlisted transaction in
616    /// `context`. External writers receive a non-transactional context and
617    /// retain the documented at-least-once boundary.
618    fn write<'a>(
619        &'a self,
620        items: &'a [I],
621        context: WriteContext<'a>,
622    ) -> BoxFuture<'a, Result<WriteOutcome, WriterError>>;
623}
624
625/// Read-only evidence passed after the chunk transaction commits.
626#[derive(Clone, Copy, Debug)]
627pub struct ChunkCompletionContext<'a> {
628    checkpoint: &'a Checkpoint,
629    execution_context: &'a ExecutionContext,
630    counts: ChunkCounts,
631    stop: &'a StopToken,
632}
633
634impl<'a> ChunkCompletionContext<'a> {
635    /// Constructs committed chunk evidence.
636    #[must_use]
637    pub const fn new(
638        checkpoint: &'a Checkpoint,
639        execution_context: &'a ExecutionContext,
640        counts: ChunkCounts,
641        stop: &'a StopToken,
642    ) -> Self {
643        Self {
644            checkpoint,
645            execution_context,
646            counts,
647            stop,
648        }
649    }
650
651    /// Borrows the committed checkpoint.
652    #[must_use]
653    pub const fn checkpoint(self) -> &'a Checkpoint {
654        self.checkpoint
655    }
656
657    /// Borrows the committed execution context.
658    #[must_use]
659    pub const fn execution_context(self) -> &'a ExecutionContext {
660        self.execution_context
661    }
662
663    /// Returns the committed chunk counts.
664    #[must_use]
665    pub const fn counts(self) -> ChunkCounts {
666        self.counts
667    }
668
669    /// Borrows the cooperative stop token.
670    #[must_use]
671    pub const fn stop_token(self) -> &'a StopToken {
672        self.stop
673    }
674}
675
676/// Post-commit acknowledgement from a chunk-completion component.
677#[derive(Clone, Copy, Debug, Eq, PartialEq)]
678#[non_exhaustive]
679pub enum ChunkCompletionOutcome {
680    /// The component observed and acknowledged the durable commit.
681    Acknowledged,
682    /// Stop was observed after commit; the commit remains authoritative.
683    StoppedAfterCommit,
684}
685
686/// An asynchronous observer called only after a durable chunk commit.
687pub trait ChunkCompletion: Send + Sync {
688    /// Acknowledges committed state without becoming a correctness authority.
689    fn after_commit<'a>(
690        &'a self,
691        context: ChunkCompletionContext<'a>,
692    ) -> BoxFuture<'a, Result<ChunkCompletionOutcome, ChunkCompletionError>>;
693}
694
695macro_rules! component_error {
696    (
697        $name:ident,
698        $message:literal
699        $(, $field:ident : $field_type:ty = $field_default:expr, $field_docs:literal)* $(,)?
700    ) => {
701        #[doc = $message]
702        ///
703        /// The adapter translates its own typed error into a stable
704        /// [`FailureCategory`] at this boundary. The payload, display text, and
705        /// source chain are dropped, so classification never inspects them.
706        #[derive(Clone, Copy, Debug, Eq, PartialEq)]
707        pub struct $name {
708            category: FailureCategory,
709            $(
710                #[doc = $field_docs]
711                $field: $field_type,
712            )*
713        }
714
715        impl $name {
716            /// Constructs a value-redacted [`FailureCategory::UserComponent`]
717            /// failure.
718            #[must_use]
719            pub const fn new() -> Self {
720                Self {
721                    category: FailureCategory::UserComponent,
722                    $($field: $field_default,)*
723                }
724            }
725
726            /// Constructs a failure that declares its own stable category.
727            ///
728            /// A category that is not policy-eligible fails closed: the fault
729            /// is never retried or skipped.
730            #[must_use]
731            pub const fn with_category(category: FailureCategory) -> Self {
732                Self {
733                    category,
734                    $($field: $field_default,)*
735                }
736            }
737
738            /// Classifies an arbitrary user error without retaining its
739            /// payload or display text.
740            #[must_use]
741            pub fn from_error(error: impl Error + Send + Sync + 'static) -> Self {
742                drop(error);
743                Self::new()
744            }
745
746            /// Returns the stable category supplied by the adapter.
747            #[must_use]
748            pub const fn category(self) -> FailureCategory {
749                self.category
750            }
751        }
752
753        impl Default for $name {
754            fn default() -> Self {
755                Self::new()
756            }
757        }
758
759        impl fmt::Display for $name {
760            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
761                formatter.write_str($message)
762            }
763        }
764
765        impl Error for $name {}
766    };
767}
768
769component_error!(
770    ReaderError,
771    "item reader failed",
772    checkpoint_advanced: bool = false,
773    "Whether the reader proved its checkpoint moved past one failed input.",
774);
775component_error!(ProcessorError, "item processor failed");
776component_error!(
777    WriterError,
778    "item writer failed",
779    rolled_back_output: Option<usize> = None,
780    "The located, known-rolled-back output index, when the writer supplied one.",
781);
782component_error!(ChunkCompletionError, "chunk completion callback failed");
783
784impl ReaderError {
785    /// Records that the reader moved its checkpoint past exactly one failed
786    /// input.
787    ///
788    /// A read skip requires this proof. Without it a repeated failure at the
789    /// same position fails the step instead of skipping forever.
790    #[must_use]
791    pub const fn with_checkpoint_advanced(mut self, advanced: bool) -> Self {
792        self.checkpoint_advanced = advanced;
793        self
794    }
795
796    /// Returns whether the reader proved forward checkpoint progress.
797    #[must_use]
798    pub const fn has_checkpoint_advanced(self) -> bool {
799        self.checkpoint_advanced
800    }
801}
802
803impl WriterError {
804    /// Records that the batch is known to have rolled back and identifies the
805    /// single failed output by its zero-based index in the supplied batch.
806    ///
807    /// A write skip requires this evidence. An unlocated, partially visible, or
808    /// ambiguous write cannot be skipped.
809    #[must_use]
810    pub const fn with_rolled_back_output(mut self, index: usize) -> Self {
811        self.rolled_back_output = Some(index);
812        self
813    }
814
815    /// Returns the located failed output index, when the writer supplied one.
816    #[must_use]
817    pub const fn rolled_back_output(self) -> Option<usize> {
818        self.rolled_back_output
819    }
820}