1use std::fmt;
4use std::panic::{AssertUnwindSafe, catch_unwind};
5use std::sync::{Arc, Mutex};
6use std::time::Duration;
7
8use futures_util::FutureExt;
9use tokio::sync::Mutex as AsyncMutex;
10
11use crate::runtime::{lower_one_step, one_step_node};
12use crate::{
13 BackoffOutcome, BoxFuture, ChunkCommitReceipt, ChunkCompletion, ChunkCompletionContext,
14 ChunkCompletionOutcome, ChunkComponentRevisions, ChunkCount, ChunkCounts, ChunkFaultProgress,
15 ChunkSize, ChunkTransaction, ChunkTransactionContext, ChunkTransactionError,
16 ChunkTransactionManager, CompiledExecutionPlan, DefinitionError, DefinitionIdentity,
17 DefinitionRevision, ExecutionCorrelation, FailureCategory, FailureId, FailureSummary,
18 FaultDecision, FaultDescriptor, FaultEvidence, FaultPhase, FaultProgress, FaultRuntime,
19 InFlightPolicy, InheritedStepProgress, ItemListenerContext, ItemListenerFailure,
20 ItemListenerSet, ItemProcessor, ItemReader, ItemWriter, JobExecutionListener, JobLauncher,
21 JobName, JobParameters, LaunchError, LaunchReport, LifecycleEventKind, ListenerFailureKind,
22 ProcessContext, ProcessOutcome, ProcessorError, ReadContext, ReadOutcome, ReaderError,
23 RetryCounts, RetryKey, RetryOrdinal, RetryOutcome, RetryReservation, RollbackDisposition,
24 SkipCounts, StepComponents, StepExecutionListener, StepName, StopToken, Tasklet,
25 TaskletContext, TaskletError, TaskletJob, TaskletOutcome, TaskletStep, WriteContext,
26 WriteOutcome, WriterError,
27};
28
29pub struct ChunkStep<I, O> {
31 name: StepName,
32 size: ChunkSize,
33 reader: Box<dyn ItemReader<I>>,
34 processor: Arc<dyn ItemProcessor<I, O>>,
35 writer: Arc<dyn ItemWriter<O>>,
36 transactions: Arc<dyn ChunkTransactionManager>,
37 completion: Arc<dyn ChunkCompletion>,
38 listeners: Vec<Arc<dyn ChunkListener>>,
39 step_listeners: Vec<Arc<dyn StepExecutionListener>>,
40 item_listeners: ItemListenerSet<I, O>,
41 fault: Option<FaultRuntime>,
42 in_flight_policy: InFlightPolicy,
43 definition_digest: [u8; 32],
44}
45
46impl<I, O> ChunkStep<I, O> {
47 #[must_use]
50 #[allow(clippy::too_many_arguments)]
51 pub fn new(
52 name: StepName,
53 size: ChunkSize,
54 reader: Box<dyn ItemReader<I>>,
55 processor: Arc<dyn ItemProcessor<I, O>>,
56 writer: Arc<dyn ItemWriter<O>>,
57 transactions: Arc<dyn ChunkTransactionManager>,
58 completion: Arc<dyn ChunkCompletion>,
59 ) -> Self {
60 Self {
61 name,
62 size,
63 reader,
64 processor,
65 writer,
66 transactions,
67 completion,
68 listeners: Vec::new(),
69 step_listeners: Vec::new(),
70 item_listeners: ItemListenerSet::new(),
71 fault: None,
72 in_flight_policy: InFlightPolicy::FinishChunk,
73 definition_digest: [0; 32],
74 }
75 }
76
77 #[must_use]
79 pub fn with_chunk_listener(mut self, listener: Arc<dyn ChunkListener>) -> Self {
80 self.listeners.push(listener);
81 self
82 }
83
84 #[must_use]
88 pub fn with_item_listeners(mut self, listeners: ItemListenerSet<I, O>) -> Self {
89 self.item_listeners = listeners;
90 self
91 }
92
93 #[must_use]
98 pub fn with_fault_runtime(mut self, fault: FaultRuntime) -> Self {
99 self.fault = Some(fault);
100 self
101 }
102
103 #[must_use]
105 pub fn with_listener(mut self, listener: Arc<dyn StepExecutionListener>) -> Self {
106 self.step_listeners.push(listener);
107 self
108 }
109
110 #[must_use]
112 pub const fn name(&self) -> &StepName {
113 &self.name
114 }
115
116 pub async fn execute(
123 &mut self,
124 correlation: &ExecutionCorrelation,
125 stop: &StopToken,
126 ) -> ChunkExecutionReport
127 where
128 I: Send + Sync,
129 O: Send + Sync,
130 {
131 execute_chunk_step(self, correlation, stop, None, |_| {}).await
132 }
133}
134
135impl<I, O> fmt::Debug for ChunkStep<I, O> {
136 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 formatter
138 .debug_struct("ChunkStep")
139 .field("name", &self.name)
140 .field("size", &self.size)
141 .field("chunk_listener_count", &self.listeners.len())
142 .field("step_listener_count", &self.step_listeners.len())
143 .finish_non_exhaustive()
144 }
145}
146
147pub struct ChunkJob<I, O> {
149 name: JobName,
150 step_name: StepName,
151 plan: CompiledExecutionPlan,
152 tasklet: Arc<ChunkTasklet<I, O>>,
153 step_listeners: Vec<Arc<dyn StepExecutionListener>>,
154 listeners: Vec<Arc<dyn JobExecutionListener>>,
155}
156
157impl<I, O> ChunkJob<I, O> {
158 pub fn new(
167 name: JobName,
168 mut step: ChunkStep<I, O>,
169 revision: DefinitionRevision,
170 components: &ChunkComponentRevisions,
171 ) -> Result<Self, DefinitionError> {
172 if let Some(fault) = step.fault.as_ref()
173 && fault.delivery_mode() != components.delivery_mode()
174 {
175 return Err(DefinitionError::DeliveryModeMismatch);
176 }
177 let step_name = step.name.clone();
178 let definition =
179 DefinitionIdentity::chunk(&name, &step_name, step.size, revision, components)?;
180 step.in_flight_policy = components.in_flight_policy();
181 step.definition_digest = *definition.manifest_digest();
182 let mut node = one_step_node(
183 &step_name,
184 StepComponents::Chunk {
185 size: step.size,
186 revisions: Box::new(components.clone()),
187 },
188 )?;
189 if let Some(fault) = step.fault.as_ref() {
190 node = node.with_fault_policy(fault.policy().clone());
191 }
192 let plan = lower_one_step(definition, node)?;
193 let step_listeners = step.step_listeners.clone();
194 Ok(Self {
195 name,
196 step_name,
197 plan,
198 tasklet: Arc::new(ChunkTasklet::new(step)),
199 step_listeners,
200 listeners: Vec::new(),
201 })
202 }
203
204 #[must_use]
209 pub const fn compiled_plan(&self) -> &CompiledExecutionPlan {
210 &self.plan
211 }
212
213 #[must_use]
215 pub const fn definition_identity(&self) -> &DefinitionIdentity {
216 self.plan.definition_identity()
217 }
218
219 #[must_use]
221 pub fn with_listener(mut self, listener: Arc<dyn JobExecutionListener>) -> Self {
222 self.listeners.push(listener);
223 self
224 }
225
226 #[must_use]
228 pub const fn name(&self) -> &JobName {
229 &self.name
230 }
231
232 #[must_use]
234 pub const fn step_name(&self) -> &StepName {
235 &self.step_name
236 }
237}
238
239impl<I, O> fmt::Debug for ChunkJob<I, O> {
240 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
241 formatter
242 .debug_struct("ChunkJob")
243 .field("name", &self.name)
244 .field("step_name", &self.step_name)
245 .field("definition", self.plan.definition_identity())
246 .field("listener_count", &self.listeners.len())
247 .field("step_listener_count", &self.step_listeners.len())
248 .finish_non_exhaustive()
249 }
250}
251
252impl crate::FlowJob {
253 pub fn with_chunk_step<I, O>(
266 mut self,
267 node_id: crate::NodeId,
268 mut step: ChunkStep<I, O>,
269 revisions: &ChunkComponentRevisions,
270 ) -> Result<Self, crate::FlowJobError>
271 where
272 I: Send + Sync + 'static,
273 O: Send + Sync + 'static,
274 {
275 let Some(crate::FlowNode::Step(compiled)) = self.compiled_plan().node(&node_id) else {
276 return Err(crate::FlowJobError::WrongNodeKind { node: node_id });
277 };
278 let expected = StepComponents::Chunk {
279 size: step.size,
280 revisions: Box::new(revisions.clone()),
281 };
282 if compiled.step_name() != step.name()
283 || compiled.components() != &expected
284 || compiled.fault_policy() != step.fault.as_ref().map(FaultRuntime::policy)
285 {
286 return Err(crate::FlowJobError::ComponentMismatch { node: node_id });
287 }
288 step.definition_digest = *self.compiled_plan().fingerprint();
289 step.in_flight_policy = revisions.in_flight_policy();
290 let listeners = step.step_listeners.clone();
291 let tasklet: Arc<dyn Tasklet> = Arc::new(ChunkTasklet::new(step));
292 let mut tasklet_step = TaskletStep::new(compiled.step_name().clone(), tasklet);
293 for listener in listeners {
294 tasklet_step = tasklet_step.with_listener(listener);
295 }
296 self.bind_chunk_tasklet(node_id, tasklet_step)?;
297 Ok(self)
298 }
299}
300
301struct ChunkTasklet<I, O> {
302 step: AsyncMutex<ChunkStep<I, O>>,
303 last_report: Mutex<Option<ChunkExecutionReport>>,
304}
305
306impl<I, O> ChunkTasklet<I, O> {
307 fn new(step: ChunkStep<I, O>) -> Self {
308 Self {
309 step: AsyncMutex::new(step),
310 last_report: Mutex::new(None),
311 }
312 }
313
314 fn take_last_report(&self) -> Option<ChunkExecutionReport> {
315 self.last_report
316 .lock()
317 .unwrap_or_else(std::sync::PoisonError::into_inner)
318 .take()
319 }
320
321 fn clear_last_report(&self) {
322 *self
323 .last_report
324 .lock()
325 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
326 }
327}
328
329impl<I, O> Tasklet for ChunkTasklet<I, O>
330where
331 I: Send + Sync + 'static,
332 O: Send + Sync + 'static,
333{
334 fn execute<'a>(
335 &'a self,
336 context: TaskletContext<'a>,
337 ) -> BoxFuture<'a, Result<TaskletOutcome, TaskletError>> {
338 Box::pin(async move {
339 let mut step = self.step.lock().await;
340 let transaction_context = ChunkTransactionContext::new(
341 context.job_execution_id(),
342 context.step_execution_id(),
343 );
344 let report = execute_chunk_step(
345 &mut step,
346 context.correlation(),
347 context.stop_token(),
348 Some(transaction_context),
349 |event| match event {
350 ChunkRuntimeEvent::Started(sequence) => {
351 context.emit_chunk_event(LifecycleEventKind::ChunkStarted, sequence);
352 }
353 ChunkRuntimeEvent::Committed(sequence) => {
354 context.emit_chunk_event(LifecycleEventKind::ChunkCommitted, sequence);
355 }
356 ChunkRuntimeEvent::RolledBack(sequence) => {
357 context.emit_chunk_event(LifecycleEventKind::ChunkRolledBack, sequence);
358 }
359 ChunkRuntimeEvent::Unknown(sequence) => {
360 context.emit_chunk_event(LifecycleEventKind::ChunkUnknown, sequence);
361 }
362 ChunkRuntimeEvent::Fault(fault) => context.emit_fault_event(&fault),
363 },
364 )
365 .await;
366 let outcome = match report.outcome() {
367 ChunkExecutionOutcome::Completed => Ok(TaskletOutcome::Completed),
368 ChunkExecutionOutcome::Stopped => Ok(TaskletOutcome::Stopped),
369 ChunkExecutionOutcome::Failed(_) => Err(TaskletError::new()),
370 ChunkExecutionOutcome::Unknown => Ok(TaskletOutcome::CommitOutcomeUnknown),
371 };
372 if report.terminal_rollback {
373 context.acknowledge_terminal_rollback();
374 }
375 *self
376 .last_report
377 .lock()
378 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(report);
379 outcome
380 })
381 }
382}
383
384#[derive(Clone, Debug, Eq, PartialEq)]
386pub struct ChunkLaunchReport {
387 launch: LaunchReport,
388 chunk: Option<ChunkExecutionReport>,
389}
390
391impl ChunkLaunchReport {
392 #[must_use]
394 pub const fn launch(&self) -> &LaunchReport {
395 &self.launch
396 }
397
398 #[must_use]
403 pub const fn chunk(&self) -> Option<&ChunkExecutionReport> {
404 self.chunk.as_ref()
405 }
406}
407
408impl JobLauncher<'_> {
409 pub async fn launch_chunk<I, O>(
419 &self,
420 job: &mut ChunkJob<I, O>,
421 parameters: &JobParameters,
422 stop: &StopToken,
423 ) -> Result<ChunkLaunchReport, LaunchError>
424 where
425 I: Send + Sync + 'static,
426 O: Send + Sync + 'static,
427 {
428 job.tasklet.clear_last_report();
429 let tasklet: Arc<dyn Tasklet> = job.tasklet.clone();
430 let mut tasklet_step = TaskletStep::new(job.step_name.clone(), tasklet);
431 for listener in &job.step_listeners {
432 tasklet_step = tasklet_step.with_listener(Arc::clone(listener));
433 }
434 let mut tasklet_job =
435 TaskletJob::from_lowered_plan(job.name.clone(), tasklet_step, job.plan.clone());
436 for listener in &job.listeners {
437 tasklet_job = tasklet_job.with_listener(Arc::clone(listener));
438 }
439
440 let launch = self.launch(&tasklet_job, parameters, stop).await?;
441 let chunk = job.tasklet.take_last_report();
442 Ok(ChunkLaunchReport { launch, chunk })
443 }
444}
445
446#[derive(Clone, Copy, Debug)]
448pub struct ChunkListenerContext<'a> {
449 sequence: ChunkCount,
450 committed_counts: ChunkCounts,
451 stop: &'a StopToken,
452}
453
454impl<'a> ChunkListenerContext<'a> {
455 const fn new(sequence: ChunkCount, committed_counts: ChunkCounts, stop: &'a StopToken) -> Self {
456 Self {
457 sequence,
458 committed_counts,
459 stop,
460 }
461 }
462
463 #[must_use]
465 pub const fn sequence(self) -> ChunkCount {
466 self.sequence
467 }
468
469 #[must_use]
471 pub const fn committed_counts(self) -> ChunkCounts {
472 self.committed_counts
473 }
474
475 #[must_use]
477 pub const fn stop_token(self) -> &'a StopToken {
478 self.stop
479 }
480}
481
482#[derive(Clone, Copy, Debug, Eq, PartialEq)]
484#[non_exhaustive]
485pub enum ChunkAttemptOutcome {
486 Committed,
488 RolledBack,
490 Stopped,
492 Unknown,
494}
495
496#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
498pub struct ChunkListenerError;
499
500impl ChunkListenerError {
501 #[must_use]
503 pub const fn new() -> Self {
504 Self
505 }
506}
507
508impl fmt::Display for ChunkListenerError {
509 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
510 formatter.write_str("chunk listener failed")
511 }
512}
513
514impl std::error::Error for ChunkListenerError {}
515
516pub trait ChunkListener: Send + Sync {
518 fn before_chunk<'a>(
520 &'a self,
521 context: ChunkListenerContext<'a>,
522 ) -> BoxFuture<'a, Result<(), ChunkListenerError>>;
523
524 fn after_chunk<'a>(
526 &'a self,
527 context: ChunkListenerContext<'a>,
528 outcome: ChunkAttemptOutcome,
529 ) -> BoxFuture<'a, Result<(), ChunkListenerError>>;
530}
531
532#[derive(Clone, Copy, Debug, Eq, PartialEq)]
534#[non_exhaustive]
535pub enum ChunkListenerFailureKind {
536 Error,
538 Panic,
540}
541
542#[derive(Clone, Copy, Debug, Eq, PartialEq)]
544#[non_exhaustive]
545pub enum ChunkListenerPhase {
546 BeforeChunk,
548 AfterChunk,
550}
551
552#[derive(Clone, Copy, Debug, Eq, PartialEq)]
554pub struct ChunkListenerFailure {
555 phase: ChunkListenerPhase,
556 registration_index: usize,
557 kind: ChunkListenerFailureKind,
558}
559
560impl ChunkListenerFailure {
561 const fn new(
562 phase: ChunkListenerPhase,
563 registration_index: usize,
564 kind: ChunkListenerFailureKind,
565 ) -> Self {
566 Self {
567 phase,
568 registration_index,
569 kind,
570 }
571 }
572
573 #[must_use]
575 pub const fn phase(self) -> ChunkListenerPhase {
576 self.phase
577 }
578
579 #[must_use]
581 pub const fn registration_index(self) -> usize {
582 self.registration_index
583 }
584
585 #[must_use]
587 pub const fn kind(self) -> ChunkListenerFailureKind {
588 self.kind
589 }
590}
591
592#[derive(Clone, Copy, Debug, Eq, PartialEq)]
594#[non_exhaustive]
595pub enum ChunkFailure {
596 Count,
598 Reader,
600 ReaderPanic,
602 Processor,
604 ProcessorPanic,
606 Writer,
608 WriterPanic,
610 TransactionBegin,
612 TransactionCommit,
614 TransactionRollback,
616 Completion,
618 CompletionPanic,
620 Listener,
622 ListenerPanic,
624 ItemListener,
626 ItemListenerPanic,
628 RetryReservation,
630 FaultState,
632 RetryStateExhausted,
634 UnsupportedCapability,
636}
637
638#[derive(Clone, Copy, Debug, Eq, PartialEq)]
640#[non_exhaustive]
641pub enum ChunkExecutionOutcome {
642 Completed,
644 Stopped,
646 Failed(ChunkFailure),
648 Unknown,
650}
651
652#[derive(Clone, Debug, Eq, PartialEq)]
654pub struct ChunkExecutionReport {
655 outcome: ChunkExecutionOutcome,
656 original_outcome: Option<ChunkExecutionOutcome>,
657 committed_counts: ChunkCounts,
658 committed_chunks: ChunkCount,
659 rolled_back_chunks: ChunkCount,
660 listener_failures: Vec<ChunkListenerFailure>,
661 item_listener_failures: Vec<ItemListenerFailure>,
662 skip_counts: SkipCounts,
663 retry_counts: RetryCounts,
664 rollback_count: u64,
665 no_rollback_count: u64,
666 terminal_rollback: bool,
667}
668
669impl ChunkExecutionReport {
670 #[must_use]
672 pub const fn outcome(&self) -> ChunkExecutionOutcome {
673 self.outcome
674 }
675
676 #[must_use]
678 pub const fn original_outcome(&self) -> Option<ChunkExecutionOutcome> {
679 self.original_outcome
680 }
681
682 #[must_use]
684 pub const fn committed_counts(&self) -> ChunkCounts {
685 self.committed_counts
686 }
687
688 #[must_use]
690 pub const fn committed_chunks(&self) -> ChunkCount {
691 self.committed_chunks
692 }
693
694 #[must_use]
696 pub const fn rolled_back_chunks(&self) -> ChunkCount {
697 self.rolled_back_chunks
698 }
699
700 #[must_use]
702 pub fn listener_failures(&self) -> &[ChunkListenerFailure] {
703 &self.listener_failures
704 }
705
706 #[must_use]
708 pub fn item_listener_failures(&self) -> &[ItemListenerFailure] {
709 &self.item_listener_failures
710 }
711
712 #[must_use]
719 pub const fn skip_counts(&self) -> SkipCounts {
720 self.skip_counts
721 }
722
723 #[must_use]
727 pub const fn retry_counts(&self) -> RetryCounts {
728 self.retry_counts
729 }
730
731 #[must_use]
737 pub const fn rollback_count(&self) -> u64 {
738 self.rollback_count
739 }
740
741 #[must_use]
744 pub const fn no_rollback_count(&self) -> u64 {
745 self.no_rollback_count
746 }
747}
748
749#[derive(Clone, Copy, Debug, Eq, PartialEq)]
750pub(crate) enum ChunkRuntimeEvent {
751 Started(ChunkCount),
752 Committed(ChunkCount),
753 RolledBack(ChunkCount),
754 Unknown(ChunkCount),
755 Fault(FaultRuntimeEvent),
756}
757
758#[derive(Clone, Copy, Debug, Eq, PartialEq)]
760pub(crate) struct FaultRuntimeEvent {
761 pub(crate) kind: LifecycleEventKind,
762 pub(crate) sequence: ChunkCount,
763 pub(crate) phase: FaultPhase,
764 pub(crate) summary: Option<FailureSummary>,
765 pub(crate) ordinal: Option<RetryOrdinal>,
766 pub(crate) backoff: Option<Duration>,
767}
768
769impl FaultRuntimeEvent {
770 const fn new(kind: LifecycleEventKind, sequence: ChunkCount, phase: FaultPhase) -> Self {
771 Self {
772 kind,
773 sequence,
774 phase,
775 summary: None,
776 ordinal: None,
777 backoff: None,
778 }
779 }
780
781 const fn with_summary(mut self, summary: FailureSummary) -> Self {
782 self.summary = Some(summary);
783 self
784 }
785
786 const fn with_ordinal(mut self, ordinal: RetryOrdinal) -> Self {
787 self.ordinal = Some(ordinal);
788 self
789 }
790
791 const fn with_backoff(mut self, backoff: Duration) -> Self {
792 self.backoff = Some(backoff);
793 self
794 }
795}
796
797struct ExecutionState {
798 committed_counts: ChunkCounts,
799 committed_chunks: ChunkCount,
800 rolled_back_chunks: ChunkCount,
801 listener_failures: Vec<ChunkListenerFailure>,
802 item_listener_failures: Vec<ItemListenerFailure>,
803 skip_counts: SkipCounts,
804 retry_counts: RetryCounts,
805 rollback_count: u64,
806 no_rollback_count: u64,
807 terminal_rollback: bool,
808 next_failure_id: u64,
809}
810
811impl ExecutionState {
812 fn new() -> Self {
813 Self::inheriting(FaultProgress::NONE)
814 }
815
816 fn inheriting(inherited: FaultProgress) -> Self {
818 Self {
819 committed_counts: ChunkCounts::default(),
820 committed_chunks: ChunkCount::ZERO,
821 rolled_back_chunks: ChunkCount::ZERO,
822 listener_failures: Vec::new(),
823 item_listener_failures: Vec::new(),
824 skip_counts: inherited.skips(),
825 retry_counts: inherited.retries(),
826 rollback_count: 0,
827 no_rollback_count: inherited.no_rollbacks(),
828 terminal_rollback: false,
829 next_failure_id: 0,
830 }
831 }
832
833 fn drain(&mut self) -> Self {
835 let mut replacement = Self::new();
836 replacement.skip_counts = self.skip_counts;
837 replacement.retry_counts = self.retry_counts;
838 replacement.rollback_count = self.rollback_count;
839 replacement.no_rollback_count = self.no_rollback_count;
840 replacement.terminal_rollback = self.terminal_rollback;
841 replacement.next_failure_id = self.next_failure_id;
842 std::mem::replace(self, replacement)
843 }
844
845 fn report(
846 self,
847 outcome: ChunkExecutionOutcome,
848 original_outcome: Option<ChunkExecutionOutcome>,
849 ) -> ChunkExecutionReport {
850 ChunkExecutionReport {
851 outcome,
852 original_outcome,
853 committed_counts: self.committed_counts,
854 committed_chunks: self.committed_chunks,
855 rolled_back_chunks: self.rolled_back_chunks,
856 listener_failures: self.listener_failures,
857 item_listener_failures: self.item_listener_failures,
858 skip_counts: self.skip_counts,
859 retry_counts: self.retry_counts,
860 rollback_count: self.rollback_count,
861 no_rollback_count: self.no_rollback_count,
862 terminal_rollback: self.terminal_rollback,
863 }
864 }
865}
866
867struct ItemSlot<I> {
869 item: I,
870 ordinal: u64,
871 skipped: bool,
872}
873
874struct PendingSkip<O> {
876 phase: FaultPhase,
877 fault: FaultDescriptor,
878 disposition: RollbackDisposition,
879 slot: Option<usize>,
880 output: Option<O>,
881}
882
883struct PendingRetry {
885 key: RetryKey,
886 fault: FaultDescriptor,
887 entered: usize,
888}
889
890struct ChunkBuffer<I, O> {
896 slots: Vec<ItemSlot<I>>,
897 skips: Vec<PendingSkip<O>>,
898 retry: Option<PendingRetry>,
899 end_of_input: bool,
900 base_ordinal: u64,
901 read_ordinal: u64,
902 checkpoint_digest: [u8; 32],
903}
904
905impl<I, O> ChunkBuffer<I, O> {
906 const fn new(base_ordinal: u64, checkpoint_digest: [u8; 32]) -> Self {
907 Self {
908 slots: Vec::new(),
909 skips: Vec::new(),
910 retry: None,
911 end_of_input: false,
912 base_ordinal,
913 read_ordinal: base_ordinal,
914 checkpoint_digest,
915 }
916 }
917}
918
919fn accepted_fault_progress<O>(skips: &[PendingSkip<O>]) -> Option<ChunkFaultProgress> {
921 let mut counts = SkipCounts::ZERO;
922 let mut no_rollbacks = 0_u64;
923 for skip in skips {
924 counts = counts.checked_increment(skip.phase).ok()?;
925 if skip.disposition == RollbackDisposition::CommitSafeSkip {
926 no_rollbacks = no_rollbacks.checked_add(1)?;
927 }
928 }
929 Some(ChunkFaultProgress::new(counts, no_rollbacks))
930}
931
932fn projected_skips<O>(committed: SkipCounts, skips: &[PendingSkip<O>]) -> Option<SkipCounts> {
934 skips.iter().try_fold(committed, |counts, skip| {
935 counts.checked_increment(skip.phase).ok()
936 })
937}
938
939enum Verdict {
941 Commit,
943 Retry(RetryRequest),
945 Replay,
947 Terminal(ChunkExecutionOutcome),
949}
950
951struct RetryRequest {
953 key: RetryKey,
954 phase: FaultPhase,
955 fault: FaultDescriptor,
956 ordinal: RetryOrdinal,
957 delay: Duration,
958}
959
960enum AttemptResult {
962 Committed {
964 counts: ChunkCounts,
965 receipt: ChunkCommitReceipt,
966 },
967 Replay,
969 RolledBack(ChunkExecutionOutcome),
971 RollbackFailed(Option<ChunkExecutionOutcome>),
973 Unknown,
975}
976
977struct Components<'a, I, O> {
979 processor: &'a dyn ItemProcessor<I, O>,
980 writer: &'a dyn ItemWriter<O>,
981 item_listeners: &'a ItemListenerSet<I, O>,
982 fault: Option<&'a FaultRuntime>,
983 step_name: &'a StepName,
984 definition_digest: [u8; 32],
985 size: ChunkSize,
986}
987
988#[derive(Clone, Copy)]
990struct AttemptScope<'a> {
991 correlation: &'a ExecutionCorrelation,
992 stop: &'a StopToken,
993 sequence: ChunkCount,
994}
995
996impl<'a> AttemptScope<'a> {
997 const fn listener_context(self) -> ItemListenerContext<'a> {
998 ItemListenerContext::new(self.correlation, self.sequence, self.stop)
999 }
1000}
1001
1002struct AttemptOutputs<O> {
1004 values: Vec<O>,
1005 slots: Vec<usize>,
1006 filtered: u64,
1007}
1008
1009impl<O> AttemptOutputs<O> {
1010 const fn new() -> Self {
1011 Self {
1012 values: Vec::new(),
1013 slots: Vec::new(),
1014 filtered: 0,
1015 }
1016 }
1017
1018 fn reset(&mut self) {
1019 self.values.clear();
1020 self.slots.clear();
1021 self.filtered = 0;
1022 }
1023}
1024
1025enum Invoked<T, E> {
1027 Completed(T),
1028 Failed(E),
1029 Panicked,
1030}
1031
1032#[allow(
1033 clippy::similar_names,
1034 clippy::too_many_lines,
1035 reason = "the chunk loop keeps the canonical attempt, commit, and stop order visible"
1036)]
1037pub(crate) async fn execute_chunk_step<I, O>(
1038 step: &mut ChunkStep<I, O>,
1039 correlation: &ExecutionCorrelation,
1040 stop: &StopToken,
1041 transaction_context: Option<ChunkTransactionContext>,
1042 mut emit: impl FnMut(ChunkRuntimeEvent),
1043) -> ChunkExecutionReport
1044where
1045 I: Send + Sync,
1046 O: Send + Sync,
1047{
1048 let ChunkStep {
1049 name,
1050 size,
1051 reader,
1052 processor,
1053 writer,
1054 transactions,
1055 completion,
1056 listeners,
1057 item_listeners,
1058 fault,
1059 in_flight_policy,
1060 definition_digest,
1061 ..
1062 } = step;
1063 let components = Components {
1064 processor: processor.as_ref(),
1065 writer: writer.as_ref(),
1066 item_listeners,
1067 fault: fault.as_ref(),
1068 step_name: name,
1069 definition_digest: *definition_digest,
1070 size: *size,
1071 };
1072
1073 let inherited = match inherited_progress(
1074 transactions.as_ref(),
1075 fault.as_ref(),
1076 transaction_context,
1077 )
1078 .await
1079 {
1080 Ok(inherited) => inherited,
1081 Err(outcome) => return ExecutionState::new().report(outcome, None),
1082 };
1083 let base_ordinal = inherited.read_ordinal();
1084 let mut state = ExecutionState::inheriting(inherited.fault());
1085 let mut sequence = ChunkCount::ZERO;
1086 let mut buffer = ChunkBuffer::new(base_ordinal, inherited.checkpoint_digest());
1087
1088 loop {
1089 if stop.is_stop_requested() {
1090 return state.report(ChunkExecutionOutcome::Stopped, None);
1091 }
1092 sequence = match sequence.checked_increment() {
1093 Ok(value) => value,
1094 Err(_) => {
1095 return state.report(ChunkExecutionOutcome::Failed(ChunkFailure::Count), None);
1096 }
1097 };
1098 let listener_context = ChunkListenerContext::new(sequence, state.committed_counts, stop);
1099
1100 if let Some(failure) = run_before_listeners(listeners, listener_context).await {
1101 let outcome = listener_failure_outcome(failure.kind());
1102 state.listener_failures.push(failure);
1103 return state.report(outcome, None);
1104 }
1105 if stop.is_stop_requested() {
1106 return state.report(ChunkExecutionOutcome::Stopped, None);
1107 }
1108
1109 let begun = match transaction_context {
1110 Some(context) => transactions.begin_for(context).await,
1111 None => transactions.begin().await,
1112 };
1113 let mut transaction = match begun {
1114 Ok(transaction) => transaction,
1115 Err(ChunkTransactionError::NotCommitted) => {
1116 return finish_failed_attempt(
1117 listeners,
1118 listener_context,
1119 ChunkAttemptOutcome::RolledBack,
1120 ChunkExecutionOutcome::Failed(ChunkFailure::TransactionBegin),
1121 &mut state,
1122 )
1123 .await;
1124 }
1125 Err(ChunkTransactionError::CommitOutcomeUnknown) => {
1126 return finish_failed_attempt(
1127 listeners,
1128 listener_context,
1129 ChunkAttemptOutcome::Unknown,
1130 ChunkExecutionOutcome::Unknown,
1131 &mut state,
1132 )
1133 .await;
1134 }
1135 };
1136 emit(ChunkRuntimeEvent::Started(sequence));
1137
1138 let masked_stop;
1143 let attempt_stop = match in_flight_policy {
1144 InFlightPolicy::FinishChunk => {
1145 let (_, token) = crate::StopSource::new();
1146 masked_stop = token;
1147 &masked_stop
1148 }
1149 _ => stop,
1153 };
1154 let scope = AttemptScope {
1155 correlation,
1156 stop: attempt_stop,
1157 sequence,
1158 };
1159
1160 let result = run_attempt(
1161 &components,
1162 reader.as_mut(),
1163 scope,
1164 &mut buffer,
1165 &mut state,
1166 transaction.as_mut(),
1167 &mut emit,
1168 )
1169 .await;
1170 drop(transaction);
1171
1172 match result {
1173 AttemptResult::Committed { counts, receipt } => {
1174 let Ok(next_counts) = state.committed_counts.checked_add(counts) else {
1175 return finish_failed_attempt(
1176 listeners,
1177 listener_context,
1178 ChunkAttemptOutcome::Committed,
1179 ChunkExecutionOutcome::Failed(ChunkFailure::Count),
1180 &mut state,
1181 )
1182 .await;
1183 };
1184 let Ok(next_chunks) = state.committed_chunks.checked_increment() else {
1185 return finish_failed_attempt(
1186 listeners,
1187 listener_context,
1188 ChunkAttemptOutcome::Committed,
1189 ChunkExecutionOutcome::Failed(ChunkFailure::Count),
1190 &mut state,
1191 )
1192 .await;
1193 };
1194 state.committed_counts = next_counts;
1195 state.committed_chunks = next_chunks;
1196 emit(ChunkRuntimeEvent::Committed(sequence));
1197 emit_committed_skips(&buffer, sequence, &mut emit);
1198
1199 let end_of_input = buffer.end_of_input;
1200 let checkpoint_digest = checkpoint_digest(receipt.checkpoint());
1201 let Some(next_ordinal) =
1202 base_ordinal.checked_add(state.committed_counts.read().get())
1203 else {
1204 return finish_failed_attempt(
1205 listeners,
1206 listener_context,
1207 ChunkAttemptOutcome::Committed,
1208 ChunkExecutionOutcome::Failed(ChunkFailure::Count),
1209 &mut state,
1210 )
1211 .await;
1212 };
1213 buffer = ChunkBuffer::new(next_ordinal, checkpoint_digest);
1214
1215 let completion_context = ChunkCompletionContext::new(
1216 receipt.checkpoint(),
1217 receipt.execution_context(),
1218 counts,
1219 stop,
1220 );
1221 let terminal_outcome =
1222 match invoke_completion(completion.as_ref(), completion_context).await {
1223 Ok(ChunkCompletionOutcome::Acknowledged) => {
1224 if stop.is_stop_requested() {
1225 Some(ChunkExecutionOutcome::Stopped)
1226 } else if end_of_input {
1227 Some(ChunkExecutionOutcome::Completed)
1228 } else {
1229 None
1230 }
1231 }
1232 Ok(ChunkCompletionOutcome::StoppedAfterCommit) => {
1233 Some(ChunkExecutionOutcome::Stopped)
1234 }
1235 Err(failure) => Some(ChunkExecutionOutcome::Failed(failure)),
1236 };
1237
1238 let after_context =
1239 ChunkListenerContext::new(sequence, state.committed_counts, stop);
1240 let after_failures =
1241 run_after_listeners(listeners, after_context, ChunkAttemptOutcome::Committed)
1242 .await;
1243 if let Some(first) = after_failures.first().copied() {
1244 state.listener_failures.extend(after_failures);
1245 let original = terminal_outcome.or(Some(ChunkExecutionOutcome::Completed));
1246 return state.report(listener_failure_outcome(first.kind()), original);
1247 }
1248 if let Some(outcome) = terminal_outcome {
1249 return state.report(outcome, None);
1250 }
1251 }
1252 AttemptResult::Replay => {
1253 if let Err(report) =
1254 record_rolled_back_attempt(listeners, listener_context, &mut state, &mut emit)
1255 .await
1256 {
1257 return report;
1258 }
1259 }
1260 AttemptResult::RolledBack(ChunkExecutionOutcome::Completed) => {
1261 emit(ChunkRuntimeEvent::RolledBack(sequence));
1264 let failures = run_after_listeners(
1265 listeners,
1266 listener_context,
1267 ChunkAttemptOutcome::RolledBack,
1268 )
1269 .await;
1270 if let Some(first) = failures.first().copied() {
1271 state.listener_failures.extend(failures);
1272 return state
1273 .drain()
1274 .report(listener_failure_outcome(first.kind()), None);
1275 }
1276 return state.report(ChunkExecutionOutcome::Completed, None);
1277 }
1278 AttemptResult::RolledBack(outcome) => {
1279 state.rollback_count = state.rollback_count.saturating_add(1);
1280 state.terminal_rollback = true;
1281 state.rolled_back_chunks = match state.rolled_back_chunks.checked_increment() {
1282 Ok(count) => count,
1283 Err(_) => {
1284 return state.drain().report(
1285 ChunkExecutionOutcome::Failed(ChunkFailure::Count),
1286 Some(outcome),
1287 );
1288 }
1289 };
1290 emit(ChunkRuntimeEvent::RolledBack(sequence));
1291 let attempt_outcome = match outcome {
1292 ChunkExecutionOutcome::Stopped => ChunkAttemptOutcome::Stopped,
1293 _ => ChunkAttemptOutcome::RolledBack,
1294 };
1295 return finish_failed_attempt(
1296 listeners,
1297 listener_context,
1298 attempt_outcome,
1299 outcome,
1300 &mut state,
1301 )
1302 .await;
1303 }
1304 AttemptResult::RollbackFailed(original) => {
1305 return state.drain().report(
1306 ChunkExecutionOutcome::Failed(ChunkFailure::TransactionRollback),
1307 original,
1308 );
1309 }
1310 AttemptResult::Unknown => {
1311 emit(ChunkRuntimeEvent::Unknown(sequence));
1312 return finish_failed_attempt(
1313 listeners,
1314 listener_context,
1315 ChunkAttemptOutcome::Unknown,
1316 ChunkExecutionOutcome::Unknown,
1317 &mut state,
1318 )
1319 .await;
1320 }
1321 }
1322 }
1323}
1324
1325async fn inherited_progress(
1331 transactions: &dyn ChunkTransactionManager,
1332 fault: Option<&FaultRuntime>,
1333 context: Option<ChunkTransactionContext>,
1334) -> Result<InheritedStepProgress, ChunkExecutionOutcome> {
1335 let Some(context) = context else {
1336 return Ok(InheritedStepProgress::NONE);
1337 };
1338 if let Some(fault) = fault
1339 && fault.state().bind(context).await.is_err()
1340 {
1341 return Err(ChunkExecutionOutcome::Failed(ChunkFailure::FaultState));
1342 }
1343 match transactions.inherited_progress(context).await {
1344 Ok(inherited) => Ok(inherited),
1345 Err(ChunkTransactionError::CommitOutcomeUnknown) => Err(ChunkExecutionOutcome::Unknown),
1346 Err(ChunkTransactionError::NotCommitted) => {
1347 Err(ChunkExecutionOutcome::Failed(ChunkFailure::FaultState))
1348 }
1349 }
1350}
1351
1352async fn record_rolled_back_attempt<E>(
1354 listeners: &[Arc<dyn ChunkListener>],
1355 context: ChunkListenerContext<'_>,
1356 state: &mut ExecutionState,
1357 emit: &mut E,
1358) -> Result<(), ChunkExecutionReport>
1359where
1360 E: FnMut(ChunkRuntimeEvent),
1361{
1362 state.rolled_back_chunks = match state.rolled_back_chunks.checked_increment() {
1363 Ok(count) => count,
1364 Err(_) => {
1365 return Err(state
1366 .drain()
1367 .report(ChunkExecutionOutcome::Failed(ChunkFailure::Count), None));
1368 }
1369 };
1370 emit(ChunkRuntimeEvent::RolledBack(context.sequence()));
1371 let failures = run_after_listeners(listeners, context, ChunkAttemptOutcome::RolledBack).await;
1372 if let Some(first) = failures.first().copied() {
1373 state.listener_failures.extend(failures);
1374 return Err(state
1375 .drain()
1376 .report(listener_failure_outcome(first.kind()), None));
1377 }
1378 Ok(())
1379}
1380
1381async fn run_attempt<I, O, E>(
1382 components: &Components<'_, I, O>,
1383 reader: &mut dyn ItemReader<I>,
1384 scope: AttemptScope<'_>,
1385 buffer: &mut ChunkBuffer<I, O>,
1386 state: &mut ExecutionState,
1387 transaction: &mut dyn ChunkTransaction,
1388 emit: &mut E,
1389) -> AttemptResult
1390where
1391 I: Send + Sync,
1392 O: Send + Sync,
1393 E: FnMut(ChunkRuntimeEvent),
1394{
1395 let mut outputs = AttemptOutputs::new();
1396
1397 let verdict = 'body: {
1398 if let Some(fault) = components.fault
1399 && fault.policy().requires_commit_safe_skip()
1400 && transaction.business_transaction().is_none()
1401 {
1402 break 'body Verdict::Terminal(ChunkExecutionOutcome::Failed(
1403 ChunkFailure::UnsupportedCapability,
1404 ));
1405 }
1406
1407 match read_phase(components, reader, scope, buffer, state, emit).await {
1408 Verdict::Commit => {}
1409 other => break 'body other,
1410 }
1411
1412 if buffer.slots.is_empty() && buffer.end_of_input && buffer.skips.is_empty() {
1413 break 'body Verdict::Terminal(ChunkExecutionOutcome::Completed);
1414 }
1415
1416 match process_phase(components, scope, buffer, state, &mut outputs, emit).await {
1417 Verdict::Commit => {}
1418 other => break 'body other,
1419 }
1420
1421 write_phase(
1422 components,
1423 scope,
1424 buffer,
1425 state,
1426 &mut outputs,
1427 transaction,
1428 emit,
1429 )
1430 .await
1431 };
1432
1433 match verdict {
1434 Verdict::Commit => {
1435 commit_attempt(components, scope, buffer, state, transaction, &outputs).await
1436 }
1437 Verdict::Retry(request) => {
1438 schedule_retry(components, scope, buffer, state, transaction, request, emit).await
1439 }
1440 Verdict::Replay => {
1441 if transaction.rollback().await.is_err() {
1442 return AttemptResult::RollbackFailed(None);
1443 }
1444 AttemptResult::Replay
1445 }
1446 Verdict::Terminal(ChunkExecutionOutcome::Unknown) => AttemptResult::Unknown,
1447 Verdict::Terminal(outcome) => {
1448 if transaction.rollback().await.is_err() {
1449 return AttemptResult::RollbackFailed(Some(outcome));
1450 }
1451 AttemptResult::RolledBack(outcome)
1452 }
1453 }
1454}
1455
1456#[allow(
1458 clippy::too_many_lines,
1459 reason = "one phase keeps its listener, classification, and skip order visible"
1460)]
1461async fn read_phase<I, O, E>(
1462 components: &Components<'_, I, O>,
1463 reader: &mut dyn ItemReader<I>,
1464 scope: AttemptScope<'_>,
1465 buffer: &mut ChunkBuffer<I, O>,
1466 state: &mut ExecutionState,
1467 emit: &mut E,
1468) -> Verdict
1469where
1470 I: Send + Sync,
1471 O: Send + Sync,
1472 E: FnMut(ChunkRuntimeEvent),
1473{
1474 let listener_context = scope.listener_context();
1475 while !buffer.end_of_input && buffer.slots.len() < components.size.get() as usize {
1476 if scope.stop.is_stop_requested() {
1477 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
1478 }
1479 let ordinal = buffer.read_ordinal;
1480 let key = retry_key(
1481 components,
1482 buffer.checkpoint_digest,
1483 FaultPhase::Read,
1484 ordinal,
1485 );
1486
1487 let before = components
1488 .item_listeners
1489 .before_read(listener_context)
1490 .await;
1491 if let Some(failure) = before.failure() {
1492 state.item_listener_failures.push(failure);
1493 return Verdict::Terminal(item_listener_outcome(failure.kind()));
1494 }
1495
1496 match invoke_reader(reader, ReadContext::new(scope.stop)).await {
1497 Invoked::Completed(ReadOutcome::Item(item)) => {
1498 let failures = components
1499 .item_listeners
1500 .after_read(before.entered(), &item, listener_context)
1501 .await;
1502 if let Some(first) = failures.first().copied() {
1503 state.item_listener_failures.extend(failures);
1504 return Verdict::Terminal(item_listener_outcome(first.kind()));
1505 }
1506 if let Some(outcome) = complete_retry(
1507 components,
1508 listener_context,
1509 &mut buffer.retry,
1510 state,
1511 key,
1512 RetryOutcome::Recovered,
1513 )
1514 .await
1515 {
1516 return Verdict::Terminal(outcome);
1517 }
1518 resolve_key(components, key).await;
1519 buffer.slots.push(ItemSlot {
1520 item,
1521 ordinal,
1522 skipped: false,
1523 });
1524 buffer.read_ordinal = buffer.read_ordinal.saturating_add(1);
1525 }
1526 Invoked::Completed(ReadOutcome::EndOfInput) => buffer.end_of_input = true,
1527 Invoked::Completed(ReadOutcome::Stopped) => {
1528 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
1529 }
1530 invoked => {
1531 let (error, panicked) = match invoked {
1532 Invoked::Failed(error) => (error, false),
1533 _ => (ReaderError::new(), true),
1534 };
1535 let terminal = if panicked {
1536 ChunkFailure::ReaderPanic
1537 } else {
1538 ChunkFailure::Reader
1539 };
1540 let advanced = !panicked && error.has_checkpoint_advanced();
1541 let Some(fault) = descriptor(
1542 components,
1543 state,
1544 &buffer.skips,
1545 FaultPhase::Read,
1546 error.category(),
1547 ) else {
1548 return Verdict::Terminal(ChunkExecutionOutcome::Failed(ChunkFailure::Count));
1549 };
1550 let fault = with_reserved_ordinal(components, key, fault).await;
1551
1552 let failures = components
1553 .item_listeners
1554 .on_read_error(before.entered(), fault, listener_context)
1555 .await;
1556 if let Some(first) = failures.first().copied() {
1557 state.item_listener_failures.extend(failures);
1558 return Verdict::Terminal(item_listener_outcome(first.kind()));
1559 }
1560
1561 let evidence = FaultEvidence::new(advanced, true, advanced);
1562 let decision = match classify(
1563 components,
1564 listener_context,
1565 &mut buffer.retry,
1566 state,
1567 key,
1568 fault,
1569 evidence,
1570 scope.sequence,
1571 emit,
1572 )
1573 .await
1574 {
1575 Ok(decision) => decision,
1576 Err(outcome) => return Verdict::Terminal(outcome),
1577 };
1578 match decision {
1579 FaultDecision::Retry { ordinal, delay } => {
1580 return Verdict::Retry(RetryRequest {
1581 key,
1582 phase: FaultPhase::Read,
1583 fault,
1584 ordinal,
1585 delay,
1586 });
1587 }
1588 FaultDecision::Skip { disposition } => {
1589 resolve_key(components, key).await;
1590 buffer.read_ordinal = buffer.read_ordinal.saturating_add(1);
1591 buffer.skips.push(PendingSkip {
1592 phase: FaultPhase::Read,
1593 fault,
1594 disposition,
1595 slot: None,
1596 output: None,
1597 });
1598 if disposition == RollbackDisposition::Rollback {
1599 return Verdict::Replay;
1600 }
1601 }
1602 FaultDecision::Unknown => {
1603 return Verdict::Terminal(ChunkExecutionOutcome::Unknown);
1604 }
1605 FaultDecision::Stop => {
1606 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
1607 }
1608 _ => {
1614 return Verdict::Terminal(ChunkExecutionOutcome::Failed(terminal));
1615 }
1616 }
1617 }
1618 }
1619 }
1620 Verdict::Commit
1621}
1622
1623#[allow(
1625 clippy::too_many_lines,
1626 reason = "one phase keeps its listener, classification, and skip order visible"
1627)]
1628async fn process_phase<I, O, E>(
1629 components: &Components<'_, I, O>,
1630 scope: AttemptScope<'_>,
1631 buffer: &mut ChunkBuffer<I, O>,
1632 state: &mut ExecutionState,
1633 outputs: &mut AttemptOutputs<O>,
1634 emit: &mut E,
1635) -> Verdict
1636where
1637 I: Send + Sync,
1638 O: Send + Sync,
1639 E: FnMut(ChunkRuntimeEvent),
1640{
1641 let listener_context = scope.listener_context();
1642 outputs.reset();
1643 for index in 0..buffer.slots.len() {
1644 if buffer.slots[index].skipped {
1645 continue;
1646 }
1647 if scope.stop.is_stop_requested() {
1648 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
1649 }
1650 let ordinal = buffer.slots[index].ordinal;
1651 let key = retry_key(
1652 components,
1653 buffer.checkpoint_digest,
1654 FaultPhase::Process,
1655 ordinal,
1656 );
1657
1658 let before = components
1659 .item_listeners
1660 .before_process(&buffer.slots[index].item, listener_context)
1661 .await;
1662 if let Some(failure) = before.failure() {
1663 state.item_listener_failures.push(failure);
1664 return Verdict::Terminal(item_listener_outcome(failure.kind()));
1665 }
1666
1667 let invoked = invoke_processor(
1668 components.processor,
1669 &buffer.slots[index].item,
1670 ProcessContext::new(scope.stop),
1671 )
1672 .await;
1673 match invoked {
1674 Invoked::Completed(ProcessOutcome::Item(output)) => {
1675 let failures = components
1676 .item_listeners
1677 .after_process(
1678 before.entered(),
1679 &buffer.slots[index].item,
1680 Some(&output),
1681 listener_context,
1682 )
1683 .await;
1684 if let Some(first) = failures.first().copied() {
1685 state.item_listener_failures.extend(failures);
1686 return Verdict::Terminal(item_listener_outcome(first.kind()));
1687 }
1688 if let Some(outcome) = complete_retry(
1689 components,
1690 listener_context,
1691 &mut buffer.retry,
1692 state,
1693 key,
1694 RetryOutcome::Recovered,
1695 )
1696 .await
1697 {
1698 return Verdict::Terminal(outcome);
1699 }
1700 resolve_key(components, key).await;
1701 outputs.values.push(output);
1702 outputs.slots.push(index);
1703 }
1704 Invoked::Completed(ProcessOutcome::Filtered) => {
1705 let failures = components
1706 .item_listeners
1707 .after_process(
1708 before.entered(),
1709 &buffer.slots[index].item,
1710 None,
1711 listener_context,
1712 )
1713 .await;
1714 if let Some(first) = failures.first().copied() {
1715 state.item_listener_failures.extend(failures);
1716 return Verdict::Terminal(item_listener_outcome(first.kind()));
1717 }
1718 if let Some(outcome) = complete_retry(
1719 components,
1720 listener_context,
1721 &mut buffer.retry,
1722 state,
1723 key,
1724 RetryOutcome::Recovered,
1725 )
1726 .await
1727 {
1728 return Verdict::Terminal(outcome);
1729 }
1730 resolve_key(components, key).await;
1731 outputs.filtered = outputs.filtered.saturating_add(1);
1732 }
1733 Invoked::Completed(ProcessOutcome::Stopped) => {
1734 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
1735 }
1736 invoked => {
1737 let (error, panicked) = match invoked {
1738 Invoked::Failed(error) => (error, false),
1739 _ => (ProcessorError::new(), true),
1740 };
1741 let terminal = if panicked {
1742 ChunkFailure::ProcessorPanic
1743 } else {
1744 ChunkFailure::Processor
1745 };
1746 let Some(fault) = descriptor(
1747 components,
1748 state,
1749 &buffer.skips,
1750 FaultPhase::Process,
1751 error.category(),
1752 ) else {
1753 return Verdict::Terminal(ChunkExecutionOutcome::Failed(ChunkFailure::Count));
1754 };
1755 let fault = with_reserved_ordinal(components, key, fault).await;
1756
1757 let failures = components
1758 .item_listeners
1759 .on_process_error(
1760 before.entered(),
1761 &buffer.slots[index].item,
1762 fault,
1763 listener_context,
1764 )
1765 .await;
1766 if let Some(first) = failures.first().copied() {
1767 state.item_listener_failures.extend(failures);
1768 return Verdict::Terminal(item_listener_outcome(first.kind()));
1769 }
1770
1771 let evidence = FaultEvidence::new(true, true, true);
1774 let decision = match classify(
1775 components,
1776 listener_context,
1777 &mut buffer.retry,
1778 state,
1779 key,
1780 fault,
1781 evidence,
1782 scope.sequence,
1783 emit,
1784 )
1785 .await
1786 {
1787 Ok(decision) => decision,
1788 Err(outcome) => return Verdict::Terminal(outcome),
1789 };
1790 match decision {
1791 FaultDecision::Retry { ordinal, delay } => {
1792 return Verdict::Retry(RetryRequest {
1793 key,
1794 phase: FaultPhase::Process,
1795 fault,
1796 ordinal,
1797 delay,
1798 });
1799 }
1800 FaultDecision::Skip { disposition } => {
1801 resolve_key(components, key).await;
1802 buffer.slots[index].skipped = true;
1803 buffer.skips.push(PendingSkip {
1804 phase: FaultPhase::Process,
1805 fault,
1806 disposition,
1807 slot: Some(index),
1808 output: None,
1809 });
1810 if disposition == RollbackDisposition::Rollback {
1811 return Verdict::Replay;
1812 }
1813 }
1814 FaultDecision::Unknown => {
1815 return Verdict::Terminal(ChunkExecutionOutcome::Unknown);
1816 }
1817 FaultDecision::Stop => {
1818 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
1819 }
1820 _ => {
1826 return Verdict::Terminal(ChunkExecutionOutcome::Failed(terminal));
1827 }
1828 }
1829 }
1830 }
1831 }
1832 Verdict::Commit
1833}
1834
1835#[allow(
1837 clippy::too_many_lines,
1838 reason = "one phase keeps its listener, classification, and skip order visible"
1839)]
1840#[allow(
1841 clippy::too_many_arguments,
1842 reason = "the write boundary needs components, scope, buffer, state, outputs, and the transaction"
1843)]
1844async fn write_phase<I, O, E>(
1845 components: &Components<'_, I, O>,
1846 scope: AttemptScope<'_>,
1847 buffer: &mut ChunkBuffer<I, O>,
1848 state: &mut ExecutionState,
1849 outputs: &mut AttemptOutputs<O>,
1850 transaction: &mut dyn ChunkTransaction,
1851 emit: &mut E,
1852) -> Verdict
1853where
1854 I: Send + Sync,
1855 O: Send + Sync,
1856 E: FnMut(ChunkRuntimeEvent),
1857{
1858 if outputs.values.is_empty() {
1859 return Verdict::Commit;
1860 }
1861 if scope.stop.is_stop_requested() {
1862 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
1863 }
1864 let listener_context = scope.listener_context();
1865 let key = retry_key(
1866 components,
1867 buffer.checkpoint_digest,
1868 FaultPhase::Write,
1869 buffer.base_ordinal,
1870 );
1871
1872 let before = components
1873 .item_listeners
1874 .before_write(&outputs.values, listener_context)
1875 .await;
1876 if let Some(failure) = before.failure() {
1877 state.item_listener_failures.push(failure);
1878 return Verdict::Terminal(item_listener_outcome(failure.kind()));
1879 }
1880
1881 let write_context = match transaction.business_transaction() {
1882 Some(business) => WriteContext::enlisted(scope.stop, business),
1883 None => WriteContext::non_transactional(scope.stop),
1884 };
1885 match invoke_writer(components.writer, &outputs.values, write_context).await {
1886 Invoked::Completed(WriteOutcome::Written) => {
1887 let failures = components
1888 .item_listeners
1889 .after_write(before.entered(), &outputs.values, listener_context)
1890 .await;
1891 if let Some(first) = failures.first().copied() {
1892 state.item_listener_failures.extend(failures);
1893 return Verdict::Terminal(item_listener_outcome(first.kind()));
1894 }
1895 if let Some(outcome) = complete_retry(
1896 components,
1897 listener_context,
1898 &mut buffer.retry,
1899 state,
1900 key,
1901 RetryOutcome::Recovered,
1902 )
1903 .await
1904 {
1905 return Verdict::Terminal(outcome);
1906 }
1907 resolve_key(components, key).await;
1908 Verdict::Commit
1909 }
1910 Invoked::Completed(WriteOutcome::Stopped) => {
1911 Verdict::Terminal(ChunkExecutionOutcome::Stopped)
1912 }
1913 invoked => {
1914 let (error, panicked) = match invoked {
1915 Invoked::Failed(error) => (error, false),
1916 _ => (WriterError::new(), true),
1917 };
1918 let terminal = if panicked {
1919 ChunkFailure::WriterPanic
1920 } else {
1921 ChunkFailure::Writer
1922 };
1923 let located = if panicked {
1924 None
1925 } else {
1926 error
1927 .rolled_back_output()
1928 .filter(|index| *index < outputs.values.len())
1929 };
1930 let Some(fault) = descriptor(
1931 components,
1932 state,
1933 &buffer.skips,
1934 FaultPhase::Write,
1935 error.category(),
1936 ) else {
1937 return Verdict::Terminal(ChunkExecutionOutcome::Failed(ChunkFailure::Count));
1938 };
1939 let fault = with_reserved_ordinal(components, key, fault).await;
1940
1941 let failures = components
1942 .item_listeners
1943 .on_write_error(before.entered(), &outputs.values, fault, listener_context)
1944 .await;
1945 if let Some(first) = failures.first().copied() {
1946 state.item_listener_failures.extend(failures);
1947 return Verdict::Terminal(item_listener_outcome(first.kind()));
1948 }
1949
1950 let evidence = FaultEvidence::new(located.is_some(), located.is_some(), false);
1951 let decision = match classify(
1952 components,
1953 listener_context,
1954 &mut buffer.retry,
1955 state,
1956 key,
1957 fault,
1958 evidence,
1959 scope.sequence,
1960 emit,
1961 )
1962 .await
1963 {
1964 Ok(decision) => decision,
1965 Err(outcome) => return Verdict::Terminal(outcome),
1966 };
1967 match decision {
1968 FaultDecision::Retry { ordinal, delay } => Verdict::Retry(RetryRequest {
1969 key,
1970 phase: FaultPhase::Write,
1971 fault,
1972 ordinal,
1973 delay,
1974 }),
1975 FaultDecision::Skip { disposition } => {
1976 let Some(index) = located else {
1977 return Verdict::Terminal(ChunkExecutionOutcome::Failed(terminal));
1978 };
1979 resolve_key(components, key).await;
1980 let slot = outputs.slots[index];
1981 buffer.slots[slot].skipped = true;
1982 buffer.skips.push(PendingSkip {
1983 phase: FaultPhase::Write,
1984 fault,
1985 disposition,
1986 slot: Some(slot),
1987 output: Some(outputs.values.remove(index)),
1988 });
1989 outputs.slots.remove(index);
1990 Verdict::Replay
1991 }
1992 FaultDecision::Unknown => Verdict::Terminal(ChunkExecutionOutcome::Unknown),
1993 FaultDecision::Stop => Verdict::Terminal(ChunkExecutionOutcome::Stopped),
1994 _ => Verdict::Terminal(ChunkExecutionOutcome::Failed(terminal)),
1999 }
2000 }
2001 }
2002}
2003
2004async fn commit_attempt<I, O>(
2006 components: &Components<'_, I, O>,
2007 scope: AttemptScope<'_>,
2008 buffer: &mut ChunkBuffer<I, O>,
2009 state: &mut ExecutionState,
2010 transaction: &mut dyn ChunkTransaction,
2011 outputs: &AttemptOutputs<O>,
2012) -> AttemptResult
2013where
2014 I: Send + Sync,
2015 O: Send + Sync,
2016{
2017 let listener_context = scope.listener_context();
2018 for skip in &buffer.skips {
2019 let failures = match (skip.phase, skip.slot, skip.output.as_ref()) {
2020 (FaultPhase::Process, Some(index), _) => {
2021 components
2022 .item_listeners
2023 .on_skip_in_process(&buffer.slots[index].item, skip.fault, listener_context)
2024 .await
2025 }
2026 (FaultPhase::Write, _, Some(output)) => {
2027 components
2028 .item_listeners
2029 .on_skip_in_write(output, skip.fault, listener_context)
2030 .await
2031 }
2032 _ => {
2033 components
2034 .item_listeners
2035 .on_skip_in_read(skip.fault, listener_context)
2036 .await
2037 }
2038 };
2039 if let Some(first) = failures.first().copied() {
2040 state.item_listener_failures.extend(failures);
2041 let outcome = item_listener_outcome(first.kind());
2042 if transaction.rollback().await.is_err() {
2043 return AttemptResult::RollbackFailed(Some(outcome));
2044 }
2045 return AttemptResult::RolledBack(outcome);
2046 }
2047 }
2048
2049 let read = ChunkCount::new(buffer.slots.len() as u64);
2050 let processed = ChunkCount::new(outputs.values.len() as u64);
2051 let Ok(counts) = ChunkCounts::new(
2052 read,
2053 processed,
2054 processed,
2055 ChunkCount::new(outputs.filtered),
2056 ) else {
2057 let outcome = ChunkExecutionOutcome::Failed(ChunkFailure::Count);
2058 if transaction.rollback().await.is_err() {
2059 return AttemptResult::RollbackFailed(Some(outcome));
2060 }
2061 return AttemptResult::RolledBack(outcome);
2062 };
2063
2064 let Some(accepted) = accepted_fault_progress(&buffer.skips) else {
2065 let outcome = ChunkExecutionOutcome::Failed(ChunkFailure::Count);
2066 if transaction.rollback().await.is_err() {
2067 return AttemptResult::RollbackFailed(Some(outcome));
2068 }
2069 return AttemptResult::RolledBack(outcome);
2070 };
2071
2072 match transaction.commit(counts, accepted).await {
2073 Ok(receipt) => {
2074 let Ok(next_skips) = state.skip_counts.checked_add(accepted.skips()) else {
2075 return AttemptResult::RolledBack(ChunkExecutionOutcome::Failed(
2076 ChunkFailure::Count,
2077 ));
2078 };
2079 state.skip_counts = next_skips;
2080 state.no_rollback_count = state
2081 .no_rollback_count
2082 .saturating_add(accepted.no_rollbacks());
2083 if let Some(fault) = components.fault {
2087 let _ = fault.state().clear_resolved().await;
2088 }
2089 AttemptResult::Committed { counts, receipt }
2090 }
2091 Err(ChunkTransactionError::NotCommitted) => {
2092 let outcome = ChunkExecutionOutcome::Failed(ChunkFailure::TransactionCommit);
2093 if transaction.rollback().await.is_err() {
2094 return AttemptResult::RollbackFailed(Some(outcome));
2095 }
2096 AttemptResult::RolledBack(outcome)
2097 }
2098 Err(ChunkTransactionError::CommitOutcomeUnknown) => AttemptResult::Unknown,
2099 }
2100}
2101
2102#[allow(
2104 clippy::too_many_arguments,
2105 reason = "the retry scope needs components, scope, buffer, state, transaction, request, and events"
2106)]
2107async fn schedule_retry<I, O, E>(
2108 components: &Components<'_, I, O>,
2109 scope: AttemptScope<'_>,
2110 buffer: &mut ChunkBuffer<I, O>,
2111 state: &mut ExecutionState,
2112 transaction: &mut dyn ChunkTransaction,
2113 request: RetryRequest,
2114 emit: &mut E,
2115) -> AttemptResult
2116where
2117 I: Send + Sync,
2118 O: Send + Sync,
2119 E: FnMut(ChunkRuntimeEvent),
2120{
2121 let Some(fault_runtime) = components.fault else {
2122 return AttemptResult::RolledBack(ChunkExecutionOutcome::Failed(
2123 ChunkFailure::RetryReservation,
2124 ));
2125 };
2126 if transaction.rollback().await.is_err() {
2127 return AttemptResult::RollbackFailed(None);
2128 }
2129 if scope.stop.is_stop_requested() {
2130 return AttemptResult::RolledBack(ChunkExecutionOutcome::Stopped);
2131 }
2132
2133 let reservation = RetryReservation::new(
2134 request.key,
2135 request.phase,
2136 request.fault.category(),
2137 request.ordinal,
2138 );
2139 match fault_runtime.state().reserve(reservation).await {
2140 Ok(()) => {}
2141 Err(crate::FaultStateError::CapacityExhausted { .. }) => {
2142 return AttemptResult::RolledBack(ChunkExecutionOutcome::Failed(
2143 ChunkFailure::RetryStateExhausted,
2144 ));
2145 }
2146 Err(_) => {
2147 return AttemptResult::RolledBack(ChunkExecutionOutcome::Failed(
2148 ChunkFailure::RetryReservation,
2149 ));
2150 }
2151 }
2152 state.rollback_count = state.rollback_count.saturating_add(1);
2153 state.retry_counts = state.retry_counts.increment(request.phase);
2154 emit(ChunkRuntimeEvent::Fault(
2155 FaultRuntimeEvent::new(
2156 LifecycleEventKind::RetryReserved,
2157 scope.sequence,
2158 request.phase,
2159 )
2160 .with_summary(request.fault.summary())
2161 .with_ordinal(request.ordinal),
2162 ));
2163 emit(ChunkRuntimeEvent::Fault(
2164 FaultRuntimeEvent::new(
2165 LifecycleEventKind::FaultRollbackCommitted,
2166 scope.sequence,
2167 request.phase,
2168 )
2169 .with_summary(request.fault.summary()),
2170 ));
2171
2172 let listener_context = scope.listener_context();
2173 let before = components
2174 .item_listeners
2175 .before_retry(request.fault, listener_context)
2176 .await;
2177 if let Some(failure) = before.failure() {
2178 state.item_listener_failures.push(failure);
2179 return AttemptResult::RolledBack(item_listener_outcome(failure.kind()));
2180 }
2181 buffer.retry = Some(PendingRetry {
2182 key: request.key,
2183 fault: request.fault,
2184 entered: before.entered(),
2185 });
2186
2187 if scope.stop.is_stop_requested() {
2188 return AttemptResult::RolledBack(ChunkExecutionOutcome::Stopped);
2189 }
2190 emit(ChunkRuntimeEvent::Fault(
2191 FaultRuntimeEvent::new(
2192 LifecycleEventKind::RetryBackoffStarted,
2193 scope.sequence,
2194 request.phase,
2195 )
2196 .with_ordinal(request.ordinal)
2197 .with_backoff(request.delay),
2198 ));
2199 if fault_runtime
2200 .sleeper()
2201 .sleep(request.delay, scope.stop)
2202 .await
2203 == BackoffOutcome::Stopped
2204 {
2205 emit(ChunkRuntimeEvent::Fault(
2206 FaultRuntimeEvent::new(
2207 LifecycleEventKind::RetryBackoffCancelled,
2208 scope.sequence,
2209 request.phase,
2210 )
2211 .with_ordinal(request.ordinal)
2212 .with_backoff(request.delay),
2213 ));
2214 return AttemptResult::RolledBack(ChunkExecutionOutcome::Stopped);
2215 }
2216 if scope.stop.is_stop_requested() {
2217 return AttemptResult::RolledBack(ChunkExecutionOutcome::Stopped);
2218 }
2219 AttemptResult::Replay
2220}
2221
2222async fn complete_retry<I, O>(
2224 components: &Components<'_, I, O>,
2225 listener_context: ItemListenerContext<'_>,
2226 retry: &mut Option<PendingRetry>,
2227 state: &mut ExecutionState,
2228 key: RetryKey,
2229 outcome: RetryOutcome,
2230) -> Option<ChunkExecutionOutcome>
2231where
2232 I: Send + Sync,
2233 O: Send + Sync,
2234{
2235 let pending = retry.take_if(|pending| pending.key == key)?;
2236 let failures = components
2237 .item_listeners
2238 .after_retry(pending.entered, pending.fault, outcome, listener_context)
2239 .await;
2240 let first = failures.first().copied()?;
2241 state.item_listener_failures.extend(failures);
2242 Some(item_listener_outcome(first.kind()))
2243}
2244
2245#[allow(
2247 clippy::too_many_arguments,
2248 reason = "classification needs components, listeners, retry state, and the fault inputs"
2249)]
2250async fn classify<I, O, E>(
2251 components: &Components<'_, I, O>,
2252 listener_context: ItemListenerContext<'_>,
2253 retry: &mut Option<PendingRetry>,
2254 state: &mut ExecutionState,
2255 key: RetryKey,
2256 fault: FaultDescriptor,
2257 evidence: FaultEvidence,
2258 sequence: ChunkCount,
2259 emit: &mut E,
2260) -> Result<FaultDecision, ChunkExecutionOutcome>
2261where
2262 I: Send + Sync,
2263 O: Send + Sync,
2264 E: FnMut(ChunkRuntimeEvent),
2265{
2266 let entered = retry
2267 .as_ref()
2268 .filter(|pending| pending.key == key)
2269 .map(|pending| pending.entered);
2270 if entered.is_some()
2271 && let Some(outcome) = complete_retry(
2272 components,
2273 listener_context,
2274 retry,
2275 state,
2276 key,
2277 RetryOutcome::Failed,
2278 )
2279 .await
2280 {
2281 return Err(outcome);
2282 }
2283
2284 let Some(fault_runtime) = components.fault else {
2285 return Ok(FaultDecision::FailAndRollback);
2286 };
2287 let decision = fault_runtime.policy().decide(&fault, evidence);
2288
2289 if !decision.is_retry()
2290 && let Some(entered) = entered
2291 {
2292 emit(ChunkRuntimeEvent::Fault(
2293 FaultRuntimeEvent::new(LifecycleEventKind::RetryExhausted, sequence, fault.phase())
2294 .with_summary(fault.summary())
2295 .with_ordinal(fault.retry_ordinal()),
2296 ));
2297 let failures = components
2298 .item_listeners
2299 .on_retry_exhausted(entered, fault, listener_context)
2300 .await;
2301 if let Some(first) = failures.first().copied() {
2302 state.item_listener_failures.extend(failures);
2303 return Err(item_listener_outcome(first.kind()));
2304 }
2305 }
2306 Ok(decision)
2307}
2308
2309fn descriptor<I, O>(
2311 components: &Components<'_, I, O>,
2312 state: &mut ExecutionState,
2313 skips: &[PendingSkip<O>],
2314 phase: FaultPhase,
2315 category: FailureCategory,
2316) -> Option<FaultDescriptor> {
2317 let delivery_mode = components.fault.map_or(
2318 crate::ChunkDeliveryMode::AtLeastOnce,
2319 FaultRuntime::delivery_mode,
2320 );
2321 let committed = projected_skips(state.skip_counts, skips)?;
2322 state.next_failure_id = state.next_failure_id.saturating_add(1);
2323 let failure_id = FailureId::new(state.next_failure_id).ok()?;
2324 Some(FaultDescriptor::new(
2325 phase,
2326 FailureSummary::new(category, failure_id),
2327 RetryOrdinal::INITIAL,
2328 committed,
2329 true,
2330 delivery_mode,
2331 ))
2332}
2333
2334async fn with_reserved_ordinal<I, O>(
2336 components: &Components<'_, I, O>,
2337 key: RetryKey,
2338 fault: FaultDescriptor,
2339) -> FaultDescriptor {
2340 let Some(fault_runtime) = components.fault else {
2341 return fault;
2342 };
2343 let ordinal = fault_runtime
2344 .state()
2345 .reserved_ordinal(key)
2346 .await
2347 .ok()
2348 .flatten()
2349 .unwrap_or(RetryOrdinal::INITIAL);
2350 FaultDescriptor::new(
2351 fault.phase(),
2352 fault.summary(),
2353 ordinal,
2354 fault.committed_skips(),
2355 fault.is_transaction_open(),
2356 fault.delivery_mode(),
2357 )
2358}
2359
2360async fn resolve_key<I, O>(components: &Components<'_, I, O>, key: RetryKey) {
2362 if let Some(fault_runtime) = components.fault {
2363 let _ = fault_runtime.state().resolve(key).await;
2364 }
2365}
2366
2367fn retry_key<I, O>(
2368 components: &Components<'_, I, O>,
2369 checkpoint_digest: [u8; 32],
2370 phase: FaultPhase,
2371 ordinal: u64,
2372) -> RetryKey {
2373 RetryKey::derive(
2374 &components.definition_digest,
2375 components.step_name,
2376 phase,
2377 &checkpoint_digest,
2378 ordinal,
2379 )
2380}
2381
2382fn checkpoint_digest(checkpoint: &crate::Checkpoint) -> [u8; 32] {
2383 checkpoint.generation_digest()
2384}
2385
2386fn emit_committed_skips<I, O, E>(buffer: &ChunkBuffer<I, O>, sequence: ChunkCount, emit: &mut E)
2387where
2388 E: FnMut(ChunkRuntimeEvent),
2389{
2390 for skip in &buffer.skips {
2391 emit(ChunkRuntimeEvent::Fault(
2392 FaultRuntimeEvent::new(LifecycleEventKind::ItemSkipped, sequence, skip.phase)
2393 .with_summary(skip.fault.summary()),
2394 ));
2395 if skip.disposition == RollbackDisposition::CommitSafeSkip {
2396 emit(ChunkRuntimeEvent::Fault(
2397 FaultRuntimeEvent::new(
2398 LifecycleEventKind::FaultNoRollbackCommitted,
2399 sequence,
2400 skip.phase,
2401 )
2402 .with_summary(skip.fault.summary()),
2403 ));
2404 }
2405 }
2406}
2407
2408const fn item_listener_outcome(kind: ListenerFailureKind) -> ChunkExecutionOutcome {
2409 match kind {
2410 ListenerFailureKind::Error => ChunkExecutionOutcome::Failed(ChunkFailure::ItemListener),
2411 ListenerFailureKind::Panic => {
2412 ChunkExecutionOutcome::Failed(ChunkFailure::ItemListenerPanic)
2413 }
2414 }
2415}
2416
2417async fn finish_failed_attempt(
2418 listeners: &[Arc<dyn ChunkListener>],
2419 context: ChunkListenerContext<'_>,
2420 attempt_outcome: ChunkAttemptOutcome,
2421 outcome: ChunkExecutionOutcome,
2422 state: &mut ExecutionState,
2423) -> ChunkExecutionReport {
2424 let failures = run_after_listeners(listeners, context, attempt_outcome).await;
2425 if let Some(first) = failures.first().copied() {
2426 state.listener_failures.extend(failures);
2427 if outcome == ChunkExecutionOutcome::Unknown {
2428 return state.drain().report(outcome, None);
2429 }
2430 return state
2431 .drain()
2432 .report(listener_failure_outcome(first.kind()), Some(outcome));
2433 }
2434 state.drain().report(outcome, None)
2435}
2436
2437async fn run_before_listeners(
2438 listeners: &[Arc<dyn ChunkListener>],
2439 context: ChunkListenerContext<'_>,
2440) -> Option<ChunkListenerFailure> {
2441 for (index, listener) in listeners.iter().enumerate() {
2442 if let Err(kind) = invoke_before_listener(listener.as_ref(), context).await {
2443 return Some(ChunkListenerFailure::new(
2444 ChunkListenerPhase::BeforeChunk,
2445 index,
2446 kind,
2447 ));
2448 }
2449 }
2450 None
2451}
2452
2453async fn run_after_listeners(
2454 listeners: &[Arc<dyn ChunkListener>],
2455 context: ChunkListenerContext<'_>,
2456 outcome: ChunkAttemptOutcome,
2457) -> Vec<ChunkListenerFailure> {
2458 let mut failures = Vec::new();
2459 for (index, listener) in listeners.iter().enumerate().rev() {
2460 if let Err(kind) = invoke_after_listener(listener.as_ref(), context, outcome).await {
2461 failures.push(ChunkListenerFailure::new(
2462 ChunkListenerPhase::AfterChunk,
2463 index,
2464 kind,
2465 ));
2466 }
2467 }
2468 failures
2469}
2470
2471const fn listener_failure_outcome(kind: ChunkListenerFailureKind) -> ChunkExecutionOutcome {
2472 match kind {
2473 ChunkListenerFailureKind::Error => ChunkExecutionOutcome::Failed(ChunkFailure::Listener),
2474 ChunkListenerFailureKind::Panic => {
2475 ChunkExecutionOutcome::Failed(ChunkFailure::ListenerPanic)
2476 }
2477 }
2478}
2479
2480async fn invoke_before_listener(
2481 listener: &dyn ChunkListener,
2482 context: ChunkListenerContext<'_>,
2483) -> Result<(), ChunkListenerFailureKind> {
2484 let future = catch_unwind(AssertUnwindSafe(|| listener.before_chunk(context)))
2485 .map_err(|_| ChunkListenerFailureKind::Panic)?;
2486 match AssertUnwindSafe(future).catch_unwind().await {
2487 Ok(Ok(())) => Ok(()),
2488 Ok(Err(_)) => Err(ChunkListenerFailureKind::Error),
2489 Err(_) => Err(ChunkListenerFailureKind::Panic),
2490 }
2491}
2492
2493async fn invoke_after_listener(
2494 listener: &dyn ChunkListener,
2495 context: ChunkListenerContext<'_>,
2496 outcome: ChunkAttemptOutcome,
2497) -> Result<(), ChunkListenerFailureKind> {
2498 let future = catch_unwind(AssertUnwindSafe(|| listener.after_chunk(context, outcome)))
2499 .map_err(|_| ChunkListenerFailureKind::Panic)?;
2500 match AssertUnwindSafe(future).catch_unwind().await {
2501 Ok(Ok(())) => Ok(()),
2502 Ok(Err(_)) => Err(ChunkListenerFailureKind::Error),
2503 Err(_) => Err(ChunkListenerFailureKind::Panic),
2504 }
2505}
2506
2507struct ReaderInvocation<'a, I>(&'a mut dyn ItemReader<I>);
2508
2509impl<'a, I> ReaderInvocation<'a, I> {
2510 fn invoke(
2511 self,
2512 context: ReadContext<'a>,
2513 ) -> BoxFuture<'a, Result<ReadOutcome<I>, ReaderError>> {
2514 self.0.read(context)
2515 }
2516}
2517
2518async fn invoke_reader<'a, I>(
2519 reader: &'a mut dyn ItemReader<I>,
2520 context: ReadContext<'a>,
2521) -> Invoked<ReadOutcome<I>, ReaderError> {
2522 let invocation = ReaderInvocation(reader);
2523 let Ok(future) = catch_unwind(AssertUnwindSafe(move || invocation.invoke(context))) else {
2524 return Invoked::Panicked;
2525 };
2526 match AssertUnwindSafe(future).catch_unwind().await {
2527 Ok(Ok(outcome)) => Invoked::Completed(outcome),
2528 Ok(Err(error)) => Invoked::Failed(error),
2529 Err(_) => Invoked::Panicked,
2530 }
2531}
2532
2533async fn invoke_processor<I, O>(
2534 processor: &dyn ItemProcessor<I, O>,
2535 item: &I,
2536 context: ProcessContext<'_>,
2537) -> Invoked<ProcessOutcome<O>, ProcessorError> {
2538 let Ok(future) = catch_unwind(AssertUnwindSafe(|| processor.process(item, context))) else {
2539 return Invoked::Panicked;
2540 };
2541 match AssertUnwindSafe(future).catch_unwind().await {
2542 Ok(Ok(outcome)) => Invoked::Completed(outcome),
2543 Ok(Err(error)) => Invoked::Failed(error),
2544 Err(_) => Invoked::Panicked,
2545 }
2546}
2547
2548async fn invoke_writer<'a, O>(
2549 writer: &'a dyn ItemWriter<O>,
2550 items: &'a [O],
2551 context: WriteContext<'a>,
2552) -> Invoked<WriteOutcome, WriterError> {
2553 let Ok(future) = catch_unwind(AssertUnwindSafe(|| writer.write(items, context))) else {
2554 return Invoked::Panicked;
2555 };
2556 match AssertUnwindSafe(future).catch_unwind().await {
2557 Ok(Ok(outcome)) => Invoked::Completed(outcome),
2558 Ok(Err(error)) => Invoked::Failed(error),
2559 Err(_) => Invoked::Panicked,
2560 }
2561}
2562
2563async fn invoke_completion(
2564 completion: &dyn ChunkCompletion,
2565 context: ChunkCompletionContext<'_>,
2566) -> Result<ChunkCompletionOutcome, ChunkFailure> {
2567 let future = catch_unwind(AssertUnwindSafe(|| completion.after_commit(context)))
2568 .map_err(|_| ChunkFailure::CompletionPanic)?;
2569 match AssertUnwindSafe(future).catch_unwind().await {
2570 Ok(Ok(outcome)) => Ok(outcome),
2571 Ok(Err(_)) => Err(ChunkFailure::Completion),
2572 Err(_) => Err(ChunkFailure::CompletionPanic),
2573 }
2574}