1use std::error::Error;
4use std::fmt;
5use std::future::Future;
6
7use crate::{
8 BoxFuture, Checkpoint, ChunkCounts, ComponentStateEnvelope, ExecutionContext, FailureCategory,
9 FaultProgress, JobExecutionId, SkipCounts, StepExecutionId, StopToken,
10};
11
12#[derive(Clone, Copy, Debug)]
14pub struct ReadContext<'a> {
15 stop: &'a StopToken,
16}
17
18impl<'a> ReadContext<'a> {
19 #[must_use]
21 pub const fn new(stop: &'a StopToken) -> Self {
22 Self { stop }
23 }
24
25 #[must_use]
27 pub const fn stop_token(self) -> &'a StopToken {
28 self.stop
29 }
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
34#[non_exhaustive]
35pub enum ReadOutcome<I> {
36 Item(I),
38 EndOfInput,
40 Stopped,
42}
43
44#[diagnostic::on_unimplemented(
46 message = "`{Self}` is not an OxideBatch item reader for `{I}`",
47 label = "this component cannot read `{I}`",
48 note = "implement `ItemReader<{I}>` with `async fn read(&mut self, context: ReadContext<'_>)`",
49 note = "the returned future must be `Send`: do not hold a non-`Send` value across an await"
50)]
51pub trait ItemReader<I>: Send {
52 fn read<'a>(
54 &'a mut self,
55 context: ReadContext<'a>,
56 ) -> impl Future<Output = Result<ReadOutcome<I>, ReaderError>> + Send + 'a;
57}
58
59#[derive(Clone, Copy, Debug)]
61pub struct ProcessContext<'a> {
62 stop: &'a StopToken,
63}
64
65impl<'a> ProcessContext<'a> {
66 #[must_use]
68 pub const fn new(stop: &'a StopToken) -> Self {
69 Self { stop }
70 }
71
72 #[must_use]
74 pub const fn stop_token(self) -> &'a StopToken {
75 self.stop
76 }
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
81#[non_exhaustive]
82pub enum ProcessOutcome<O> {
83 Item(O),
85 Filtered,
87 Stopped,
89}
90
91#[diagnostic::on_unimplemented(
93 message = "`{Self}` is not an OxideBatch item processor from `{I}` to `{O}`",
94 label = "this component cannot process `{I}` into `{O}`",
95 note = "implement `ItemProcessor<{I}, {O}>` with `async fn process(&self, item: &{I}, context: ProcessContext<'_>)`",
96 note = "a processor is shared across the chunk, so it takes `&self` and must be `Sync`"
97)]
98pub trait ItemProcessor<I, O>: Send + Sync {
99 fn process<'a>(
101 &'a self,
102 item: &'a I,
103 context: ProcessContext<'a>,
104 ) -> impl Future<Output = Result<ProcessOutcome<O>, ProcessorError>> + Send + 'a;
105}
106
107#[derive(Clone, Copy, Eq, PartialEq)]
109pub struct BusinessValue<'a>(BusinessValueInner<'a>);
110
111#[derive(Clone, Copy, Eq, PartialEq)]
112enum BusinessValueInner<'a> {
113 Text(&'a str),
114 Bytes(&'a [u8]),
115 I64(i64),
116 Bool(bool),
117 Null,
118}
119
120impl<'a> BusinessValue<'a> {
121 #[must_use]
123 pub const fn text(value: &'a str) -> Self {
124 Self(BusinessValueInner::Text(value))
125 }
126
127 #[must_use]
129 pub const fn bytes(value: &'a [u8]) -> Self {
130 Self(BusinessValueInner::Bytes(value))
131 }
132
133 #[must_use]
135 pub const fn i64(value: i64) -> Self {
136 Self(BusinessValueInner::I64(value))
137 }
138
139 #[must_use]
141 pub const fn boolean(value: bool) -> Self {
142 Self(BusinessValueInner::Bool(value))
143 }
144
145 #[must_use]
147 pub const fn null() -> Self {
148 Self(BusinessValueInner::Null)
149 }
150
151 #[must_use]
153 pub const fn kind(self) -> BusinessValueKind {
154 match self.0 {
155 BusinessValueInner::Text(_) => BusinessValueKind::Text,
156 BusinessValueInner::Bytes(_) => BusinessValueKind::Bytes,
157 BusinessValueInner::I64(_) => BusinessValueKind::I64,
158 BusinessValueInner::Bool(_) => BusinessValueKind::Bool,
159 BusinessValueInner::Null => BusinessValueKind::Null,
160 }
161 }
162
163 #[must_use]
165 pub const fn as_text(self) -> Option<&'a str> {
166 match self.0 {
167 BusinessValueInner::Text(value) => Some(value),
168 _ => None,
169 }
170 }
171
172 #[must_use]
174 pub const fn as_bytes(self) -> Option<&'a [u8]> {
175 match self.0 {
176 BusinessValueInner::Bytes(value) => Some(value),
177 _ => None,
178 }
179 }
180
181 #[must_use]
183 pub const fn as_i64(self) -> Option<i64> {
184 match self.0 {
185 BusinessValueInner::I64(value) => Some(value),
186 _ => None,
187 }
188 }
189
190 #[must_use]
192 pub const fn as_bool(self) -> Option<bool> {
193 match self.0 {
194 BusinessValueInner::Bool(value) => Some(value),
195 _ => None,
196 }
197 }
198}
199
200impl fmt::Debug for BusinessValue<'_> {
201 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202 formatter
203 .debug_struct("BusinessValue")
204 .field("kind", &self.kind())
205 .field("value", &"<redacted>")
206 .finish()
207 }
208}
209
210#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
212#[non_exhaustive]
213pub enum BusinessValueKind {
214 Text,
216 Bytes,
218 I64,
220 Bool,
222 Null,
224}
225
226pub struct BusinessStatement<'a> {
228 text: &'a str,
229 values: &'a [BusinessValue<'a>],
230}
231
232impl<'a> BusinessStatement<'a> {
233 #[must_use]
238 pub const fn new(text: &'a str, values: &'a [BusinessValue<'a>]) -> Self {
239 Self { text, values }
240 }
241
242 #[must_use]
244 pub const fn text(&self) -> &'a str {
245 self.text
246 }
247
248 #[must_use]
250 pub const fn values(&self) -> &'a [BusinessValue<'a>] {
251 self.values
252 }
253}
254
255impl fmt::Debug for BusinessStatement<'_> {
256 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
257 formatter
258 .debug_struct("BusinessStatement")
259 .field("text", &"<redacted>")
260 .field("value_count", &self.values.len())
261 .finish()
262 }
263}
264
265#[derive(Clone, Copy, Debug, Eq, PartialEq)]
267pub struct BusinessWriteResult {
268 rows_affected: u64,
269}
270
271impl BusinessWriteResult {
272 #[must_use]
274 pub const fn new(rows_affected: u64) -> Self {
275 Self { rows_affected }
276 }
277
278 #[must_use]
280 pub const fn rows_affected(self) -> u64 {
281 self.rows_affected
282 }
283}
284
285pub trait BusinessTransaction: Send {
291 fn execute<'a>(
293 &'a mut self,
294 statement: BusinessStatement<'a>,
295 ) -> BoxFuture<'a, Result<BusinessWriteResult, BusinessTransactionError>>;
296}
297
298#[derive(Clone, Debug, Eq, PartialEq)]
303pub struct ChunkCommitReceipt {
304 checkpoint: Checkpoint,
305 execution_context: ExecutionContext,
306}
307
308impl ChunkCommitReceipt {
309 #[must_use]
311 pub const fn new(checkpoint: Checkpoint, execution_context: ExecutionContext) -> Self {
312 Self {
313 checkpoint,
314 execution_context,
315 }
316 }
317
318 #[must_use]
320 pub const fn checkpoint(&self) -> &Checkpoint {
321 &self.checkpoint
322 }
323
324 #[must_use]
326 pub const fn execution_context(&self) -> &ExecutionContext {
327 &self.execution_context
328 }
329}
330
331#[derive(Clone, Copy, Debug, Eq, PartialEq)]
333#[non_exhaustive]
334pub enum ChunkTransactionError {
335 NotCommitted,
337 CommitOutcomeUnknown,
339 ComponentStateUnsupported,
347}
348
349impl fmt::Display for ChunkTransactionError {
350 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
351 formatter.write_str(match self {
352 Self::NotCommitted => "chunk transaction did not commit",
353 Self::CommitOutcomeUnknown => "chunk transaction commit outcome is unknown",
354 Self::ComponentStateUnsupported => {
355 "chunk transaction does not support committing component state"
356 }
357 })
358 }
359}
360
361impl Error for ChunkTransactionError {}
362
363#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
369pub struct ChunkFaultProgress {
370 skips: SkipCounts,
371 no_rollbacks: u64,
372}
373
374impl ChunkFaultProgress {
375 pub const NONE: Self = Self {
377 skips: SkipCounts::ZERO,
378 no_rollbacks: 0,
379 };
380
381 #[must_use]
383 pub const fn new(skips: SkipCounts, no_rollbacks: u64) -> Self {
384 Self {
385 skips,
386 no_rollbacks,
387 }
388 }
389
390 #[must_use]
392 pub const fn skips(self) -> SkipCounts {
393 self.skips
394 }
395
396 #[must_use]
398 pub const fn no_rollbacks(self) -> u64 {
399 self.no_rollbacks
400 }
401}
402
403pub trait ChunkTransaction: Send {
409 fn business_transaction(&mut self) -> Option<&mut dyn BusinessTransaction>;
412
413 fn commit(
421 &mut self,
422 counts: ChunkCounts,
423 fault: ChunkFaultProgress,
424 ) -> BoxFuture<'_, Result<ChunkCommitReceipt, ChunkTransactionError>>;
425
426 fn commit_with_component_state<'a>(
441 &'a mut self,
442 counts: ChunkCounts,
443 fault: ChunkFaultProgress,
444 component_state: &'a [ComponentStateEnvelope],
445 ) -> BoxFuture<'a, Result<ChunkCommitReceipt, ChunkTransactionError>> {
446 if component_state.is_empty() {
447 self.commit(counts, fault)
448 } else {
449 Box::pin(std::future::ready(Err(
450 ChunkTransactionError::ComponentStateUnsupported,
451 )))
452 }
453 }
454
455 fn rollback(&mut self) -> BoxFuture<'_, Result<(), ChunkTransactionError>>;
457}
458
459#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
465pub struct ChunkTransactionContext {
466 job_execution_id: JobExecutionId,
467 step_execution_id: StepExecutionId,
468}
469
470impl ChunkTransactionContext {
471 #[must_use]
473 pub const fn new(job_execution_id: JobExecutionId, step_execution_id: StepExecutionId) -> Self {
474 Self {
475 job_execution_id,
476 step_execution_id,
477 }
478 }
479
480 #[must_use]
482 pub const fn job_execution_id(self) -> JobExecutionId {
483 self.job_execution_id
484 }
485
486 #[must_use]
488 pub const fn step_execution_id(self) -> StepExecutionId {
489 self.step_execution_id
490 }
491}
492
493#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
499pub struct InheritedStepProgress {
500 read_ordinal: u64,
501 checkpoint_digest: [u8; 32],
502 fault: FaultProgress,
503}
504
505impl InheritedStepProgress {
506 pub const NONE: Self = Self {
508 read_ordinal: 0,
509 checkpoint_digest: [0; 32],
510 fault: FaultProgress::NONE,
511 };
512
513 #[must_use]
515 pub const fn new(read_ordinal: u64, checkpoint_digest: [u8; 32], fault: FaultProgress) -> Self {
516 Self {
517 read_ordinal,
518 checkpoint_digest,
519 fault,
520 }
521 }
522
523 #[must_use]
525 pub const fn read_ordinal(self) -> u64 {
526 self.read_ordinal
527 }
528
529 #[must_use]
534 pub const fn checkpoint_digest(self) -> [u8; 32] {
535 self.checkpoint_digest
536 }
537
538 #[must_use]
540 pub const fn fault(self) -> FaultProgress {
541 self.fault
542 }
543}
544
545pub trait ChunkTransactionManager: Send + Sync {
547 fn begin(&self)
549 -> BoxFuture<'_, Result<Box<dyn ChunkTransaction + '_>, ChunkTransactionError>>;
550
551 fn inherited_progress(
557 &self,
558 _context: ChunkTransactionContext,
559 ) -> BoxFuture<'_, Result<InheritedStepProgress, ChunkTransactionError>> {
560 Box::pin(std::future::ready(Ok(InheritedStepProgress::NONE)))
561 }
562
563 fn inherited_component_state(
570 &self,
571 _context: ChunkTransactionContext,
572 ) -> BoxFuture<'_, Result<Vec<ComponentStateEnvelope>, ChunkTransactionError>> {
573 Box::pin(std::future::ready(Ok(Vec::new())))
574 }
575
576 fn begin_for(
582 &self,
583 _context: ChunkTransactionContext,
584 ) -> BoxFuture<'_, Result<Box<dyn ChunkTransaction + '_>, ChunkTransactionError>> {
585 self.begin()
586 }
587}
588
589#[derive(Clone, Copy, Debug, Eq, PartialEq)]
591#[non_exhaustive]
592pub enum BusinessTransactionError {
593 Infrastructure,
595 Rejected,
597 Cancelled,
599}
600
601impl fmt::Display for BusinessTransactionError {
602 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
603 formatter.write_str(match self {
604 Self::Infrastructure => "business transaction infrastructure failed",
605 Self::Rejected => "business transaction statement was rejected",
606 Self::Cancelled => "business transaction operation was cancelled",
607 })
608 }
609}
610
611impl Error for BusinessTransactionError {}
612
613pub struct WriteContext<'a> {
615 stop: &'a StopToken,
616 transaction: Option<&'a mut dyn BusinessTransaction>,
617}
618
619impl<'a> WriteContext<'a> {
620 #[must_use]
622 pub const fn non_transactional(stop: &'a StopToken) -> Self {
623 Self {
624 stop,
625 transaction: None,
626 }
627 }
628
629 #[must_use]
631 pub fn enlisted(stop: &'a StopToken, transaction: &'a mut dyn BusinessTransaction) -> Self {
632 Self {
633 stop,
634 transaction: Some(transaction),
635 }
636 }
637
638 #[must_use]
640 pub const fn stop_token(&self) -> &'a StopToken {
641 self.stop
642 }
643
644 #[must_use]
646 pub fn transaction(&mut self) -> Option<&mut (dyn BusinessTransaction + 'a)> {
647 self.transaction.as_deref_mut()
648 }
649
650 #[must_use]
652 pub const fn is_enlisted(&self) -> bool {
653 self.transaction.is_some()
654 }
655}
656
657impl fmt::Debug for WriteContext<'_> {
658 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
659 formatter
660 .debug_struct("WriteContext")
661 .field("stop_requested", &self.stop.is_stop_requested())
662 .field("enlisted", &self.transaction.is_some())
663 .finish()
664 }
665}
666
667#[derive(Clone, Copy, Debug, Eq, PartialEq)]
669#[non_exhaustive]
670pub enum WriteOutcome {
671 Written,
673 Stopped,
675}
676
677#[diagnostic::on_unimplemented(
679 message = "`{Self}` is not an OxideBatch item writer for `{I}`",
680 label = "this component cannot write `{I}`",
681 note = "implement `ItemWriter<{I}>` with `async fn write(&self, items: &[{I}], context: WriteContext<'_>)`",
682 note = "a writer that delegates to another component must tie its lifetimes: `async fn write<'a>(&'a self, items: &'a [{I}], context: WriteContext<'a>)`"
683)]
684pub trait ItemWriter<I>: Send + Sync {
685 fn write<'a>(
690 &'a self,
691 items: &'a [I],
692 context: WriteContext<'a>,
693 ) -> impl Future<Output = Result<WriteOutcome, WriterError>> + Send + 'a;
694}
695
696mod sealed {
705 use super::{ItemProcessor, ItemReader, ItemWriter};
706 use crate::{
707 BoxFuture, ProcessContext, ProcessOutcome, ProcessorError, ReadContext, ReadOutcome,
708 ReaderError, WriteContext, WriteOutcome, WriterError,
709 };
710
711 pub trait ReaderObject<I>: Send {
712 fn read_boxed<'a>(
713 &'a mut self,
714 context: ReadContext<'a>,
715 ) -> BoxFuture<'a, Result<ReadOutcome<I>, ReaderError>>;
716 }
717
718 impl<I, R: ItemReader<I>> ReaderObject<I> for R {
719 fn read_boxed<'a>(
720 &'a mut self,
721 context: ReadContext<'a>,
722 ) -> BoxFuture<'a, Result<ReadOutcome<I>, ReaderError>> {
723 Box::pin(self.read(context))
724 }
725 }
726
727 pub trait ProcessorObject<I, O>: Send + Sync {
728 fn process_boxed<'a>(
729 &'a self,
730 item: &'a I,
731 context: ProcessContext<'a>,
732 ) -> BoxFuture<'a, Result<ProcessOutcome<O>, ProcessorError>>;
733 }
734
735 impl<I, O, P: ItemProcessor<I, O>> ProcessorObject<I, O> for P {
736 fn process_boxed<'a>(
737 &'a self,
738 item: &'a I,
739 context: ProcessContext<'a>,
740 ) -> BoxFuture<'a, Result<ProcessOutcome<O>, ProcessorError>> {
741 Box::pin(self.process(item, context))
742 }
743 }
744
745 pub trait WriterObject<I>: Send + Sync {
746 fn write_boxed<'a>(
747 &'a self,
748 items: &'a [I],
749 context: WriteContext<'a>,
750 ) -> BoxFuture<'a, Result<WriteOutcome, WriterError>>;
751 }
752
753 impl<I, W: ItemWriter<I>> WriterObject<I> for W {
754 fn write_boxed<'a>(
755 &'a self,
756 items: &'a [I],
757 context: WriteContext<'a>,
758 ) -> BoxFuture<'a, Result<WriteOutcome, WriterError>> {
759 Box::pin(self.write(items, context))
760 }
761 }
762}
763
764pub struct BoxedReader<I>(Box<dyn sealed::ReaderObject<I>>);
769
770impl<I> BoxedReader<I> {
771 pub fn new<R: ItemReader<I> + 'static>(reader: R) -> Self {
773 Self(Box::new(reader))
774 }
775}
776
777impl<I> ItemReader<I> for BoxedReader<I> {
778 fn read<'a>(
779 &'a mut self,
780 context: ReadContext<'a>,
781 ) -> impl Future<Output = Result<ReadOutcome<I>, ReaderError>> + Send + 'a {
782 self.0.read_boxed(context)
783 }
784}
785
786pub struct BoxedProcessor<I, O>(Box<dyn sealed::ProcessorObject<I, O>>);
788
789impl<I, O> BoxedProcessor<I, O> {
790 pub fn new<P: ItemProcessor<I, O> + 'static>(processor: P) -> Self {
792 Self(Box::new(processor))
793 }
794}
795
796impl<I, O> ItemProcessor<I, O> for BoxedProcessor<I, O> {
797 fn process<'a>(
798 &'a self,
799 item: &'a I,
800 context: ProcessContext<'a>,
801 ) -> impl Future<Output = Result<ProcessOutcome<O>, ProcessorError>> + Send + 'a {
802 self.0.process_boxed(item, context)
803 }
804}
805
806pub struct BoxedWriter<I>(Box<dyn sealed::WriterObject<I>>);
808
809impl<I> BoxedWriter<I> {
810 pub fn new<W: ItemWriter<I> + 'static>(writer: W) -> Self {
812 Self(Box::new(writer))
813 }
814}
815
816impl<I> ItemWriter<I> for BoxedWriter<I> {
817 fn write<'a>(
818 &'a self,
819 items: &'a [I],
820 context: WriteContext<'a>,
821 ) -> impl Future<Output = Result<WriteOutcome, WriterError>> + Send + 'a {
822 self.0.write_boxed(items, context)
823 }
824}
825
826#[derive(Clone, Copy, Debug)]
828pub struct ChunkCompletionContext<'a> {
829 checkpoint: &'a Checkpoint,
830 execution_context: &'a ExecutionContext,
831 counts: ChunkCounts,
832 stop: &'a StopToken,
833}
834
835impl<'a> ChunkCompletionContext<'a> {
836 #[must_use]
838 pub const fn new(
839 checkpoint: &'a Checkpoint,
840 execution_context: &'a ExecutionContext,
841 counts: ChunkCounts,
842 stop: &'a StopToken,
843 ) -> Self {
844 Self {
845 checkpoint,
846 execution_context,
847 counts,
848 stop,
849 }
850 }
851
852 #[must_use]
854 pub const fn checkpoint(self) -> &'a Checkpoint {
855 self.checkpoint
856 }
857
858 #[must_use]
860 pub const fn execution_context(self) -> &'a ExecutionContext {
861 self.execution_context
862 }
863
864 #[must_use]
866 pub const fn counts(self) -> ChunkCounts {
867 self.counts
868 }
869
870 #[must_use]
872 pub const fn stop_token(self) -> &'a StopToken {
873 self.stop
874 }
875}
876
877#[derive(Clone, Copy, Debug, Eq, PartialEq)]
879#[non_exhaustive]
880pub enum ChunkCompletionOutcome {
881 Acknowledged,
883 StoppedAfterCommit,
885}
886
887pub trait ChunkCompletion: Send + Sync {
889 fn after_commit<'a>(
891 &'a self,
892 context: ChunkCompletionContext<'a>,
893 ) -> BoxFuture<'a, Result<ChunkCompletionOutcome, ChunkCompletionError>>;
894}
895
896macro_rules! component_error {
897 (
898 $name:ident,
899 $message:literal
900 $(, $field:ident : $field_type:ty = $field_default:expr, $field_docs:literal)* $(,)?
901 ) => {
902 #[doc = $message]
903 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
908 pub struct $name {
909 category: FailureCategory,
910 $(
911 #[doc = $field_docs]
912 $field: $field_type,
913 )*
914 }
915
916 impl $name {
917 #[must_use]
920 pub const fn new() -> Self {
921 Self {
922 category: FailureCategory::UserComponent,
923 $($field: $field_default,)*
924 }
925 }
926
927 #[must_use]
932 pub const fn with_category(category: FailureCategory) -> Self {
933 Self {
934 category,
935 $($field: $field_default,)*
936 }
937 }
938
939 #[must_use]
942 pub fn from_error(error: impl Error + Send + Sync + 'static) -> Self {
943 drop(error);
944 Self::new()
945 }
946
947 #[must_use]
949 pub const fn category(self) -> FailureCategory {
950 self.category
951 }
952 }
953
954 impl Default for $name {
955 fn default() -> Self {
956 Self::new()
957 }
958 }
959
960 impl fmt::Display for $name {
961 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
962 formatter.write_str($message)
963 }
964 }
965
966 impl Error for $name {}
967 };
968}
969
970component_error!(
971 ReaderError,
972 "item reader failed",
973 checkpoint_advanced: bool = false,
974 "Whether the reader proved its checkpoint moved past one failed input.",
975);
976component_error!(ProcessorError, "item processor failed");
977component_error!(
978 WriterError,
979 "item writer failed",
980 rolled_back_output: Option<usize> = None,
981 "The located, known-rolled-back output index, when the writer supplied one.",
982);
983component_error!(ChunkCompletionError, "chunk completion callback failed");
984
985impl ReaderError {
986 #[must_use]
992 pub const fn with_checkpoint_advanced(mut self, advanced: bool) -> Self {
993 self.checkpoint_advanced = advanced;
994 self
995 }
996
997 #[must_use]
999 pub const fn has_checkpoint_advanced(self) -> bool {
1000 self.checkpoint_advanced
1001 }
1002}
1003
1004impl WriterError {
1005 #[must_use]
1011 pub const fn with_rolled_back_output(mut self, index: usize) -> Self {
1012 self.rolled_back_output = Some(index);
1013 self
1014 }
1015
1016 #[must_use]
1018 pub const fn rolled_back_output(self) -> Option<usize> {
1019 self.rolled_back_output
1020 }
1021}