1use std::collections::BTreeSet;
4use std::fmt;
5use std::panic::{AssertUnwindSafe, catch_unwind};
6use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9use futures_util::FutureExt;
10use sha2::{Digest, Sha256};
11use tokio::sync::Mutex as AsyncMutex;
12
13use crate::runtime::{lower_one_step, one_step_node};
14use crate::{
15 AdaptiveCompletionPolicy, BackoffOutcome, BoxFuture, BoxedStream, ChunkCommitReceipt,
16 ChunkCompletion, ChunkCompletionContext, ChunkCompletionOutcome, ChunkComponentRevisions,
17 ChunkCount, ChunkCounts, ChunkFaultProgress, ChunkSize, ChunkTransaction,
18 ChunkTransactionContext, ChunkTransactionError, ChunkTransactionManager, CompiledExecutionPlan,
19 CompletionPolicy, ComponentRevision, ComponentStateEnvelope, ComponentStreamIdentity,
20 DefinitionError, DefinitionIdentity, DefinitionRevision, ExecutionCorrelation, FailureCategory,
21 FailureId, FailureSummary, FaultDecision, FaultDescriptor, FaultEvidence, FaultPhase,
22 FaultProgress, FaultRuntime, InFlightPolicy, InheritedStepProgress, ItemListenerContext,
23 ItemListenerFailure, ItemListenerSet, ItemProcessor, ItemReader, ItemStream, ItemWriter,
24 JobExecutionListener, JobLauncher, JobName, JobParameters, LaunchError, LaunchReport,
25 LifecycleEventKind, ListenerFailureKind, ProcessContext, ProcessOutcome, ProcessorError,
26 ReadContext, ReadOutcome, ReaderError, RestartabilityDeclaration, RetryCounts, RetryKey,
27 RetryOrdinal, RetryOutcome, RetryReservation, RollbackDisposition, SkipCounts, StepComponents,
28 StepExecutionListener, StepName, StopToken, StreamCloseContext, StreamOpenContext,
29 StreamRuntimeOutcome, StreamStateContract, StreamUpdateContext, Tasklet, TaskletContext,
30 TaskletError, TaskletJob, TaskletOutcome, TaskletStep, WriteContext, WriteOutcome, WriterError,
31};
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44enum StreamOwner {
45 Manual,
47 Policy,
50}
51
52type StreamRegistration = (
54 ComponentStreamIdentity,
55 Arc<BoxedStream>,
56 StreamStateContract,
57 StreamOwner,
58);
59
60pub struct ChunkStep<I, O, R, P, W> {
62 name: StepName,
63 size: ChunkSize,
64 reader: R,
65 processor: P,
66 writer: W,
67 transactions: Arc<dyn ChunkTransactionManager>,
68 completion: Arc<dyn ChunkCompletion>,
69 completion_policy: Option<Arc<dyn CompletionPolicy>>,
70 listeners: Vec<Arc<dyn ChunkListener>>,
71 step_listeners: Vec<Arc<dyn StepExecutionListener>>,
72 item_listeners: ItemListenerSet<I, O>,
73 fault: Option<FaultRuntime>,
74 streams: Vec<StreamRegistration>,
75 in_flight_policy: InFlightPolicy,
76 definition_digest: [u8; 32],
77}
78
79impl<I, O, R, P, W> ChunkStep<I, O, R, P, W>
80where
81 R: ItemReader<I>,
82 P: ItemProcessor<I, O>,
83 W: ItemWriter<O>,
84{
85 #[must_use]
88 #[allow(clippy::too_many_arguments)]
89 pub fn new(
90 name: StepName,
91 size: ChunkSize,
92 reader: R,
93 processor: P,
94 writer: W,
95 transactions: Arc<dyn ChunkTransactionManager>,
96 completion: Arc<dyn ChunkCompletion>,
97 ) -> Self {
98 Self {
99 name,
100 size,
101 reader,
102 processor,
103 writer,
104 transactions,
105 completion,
106 completion_policy: None,
107 listeners: Vec::new(),
108 step_listeners: Vec::new(),
109 item_listeners: ItemListenerSet::new(),
110 fault: None,
111 streams: Vec::new(),
112 in_flight_policy: InFlightPolicy::FinishChunk,
113 definition_digest: [0; 32],
114 }
115 }
116
117 #[must_use]
127 pub fn with_completion(mut self, completion: Arc<dyn ChunkCompletion>) -> Self {
128 self.completion = completion;
129 self
130 }
131
132 #[must_use]
161 pub fn with_completion_policy(mut self, policy: Arc<dyn CompletionPolicy>) -> Self {
162 if self.completion_policy.is_some() {
163 self.streams
164 .retain(|(_, _, _, owner)| *owner != StreamOwner::Policy);
165 }
166 for (identity, stream, contract) in policy.stream_registrations() {
167 self.streams
168 .push((identity, stream, contract, StreamOwner::Policy));
169 }
170 self.completion_policy = Some(policy);
171 self
172 }
173
174 #[must_use]
183 pub fn with_adaptive_completion_policy(self, policy: Arc<AdaptiveCompletionPolicy>) -> Self {
184 self.with_completion_policy(policy)
185 }
186
187 #[must_use]
189 pub fn with_chunk_listener(mut self, listener: Arc<dyn ChunkListener>) -> Self {
190 self.listeners.push(listener);
191 self
192 }
193
194 #[must_use]
198 pub fn with_item_listeners(mut self, listeners: ItemListenerSet<I, O>) -> Self {
199 self.item_listeners = listeners;
200 self
201 }
202
203 #[must_use]
208 pub fn with_fault_runtime(mut self, fault: FaultRuntime) -> Self {
209 self.fault = Some(fault);
210 self
211 }
212
213 #[must_use]
215 pub fn with_listener(mut self, listener: Arc<dyn StepExecutionListener>) -> Self {
216 self.step_listeners.push(listener);
217 self
218 }
219
220 #[must_use]
238 pub fn with_item_stream(
239 mut self,
240 identity: ComponentStreamIdentity,
241 stream: impl ItemStream + 'static,
242 contract: StreamStateContract,
243 ) -> Self {
244 self.streams.push((
245 identity,
246 Arc::new(BoxedStream::new(stream)),
247 contract,
248 StreamOwner::Manual,
249 ));
250 self
251 }
252
253 #[must_use]
255 pub const fn name(&self) -> &StepName {
256 &self.name
257 }
258
259 #[must_use]
261 pub const fn size(&self) -> ChunkSize {
262 self.size
263 }
264
265 pub async fn execute(
272 &mut self,
273 correlation: &ExecutionCorrelation,
274 stop: &StopToken,
275 ) -> ChunkExecutionReport
276 where
277 I: Send + Sync,
278 O: Send + Sync,
279 {
280 execute_chunk_step(self, correlation, stop, None, |_| {}).await
281 }
282}
283
284fn validate_stream_registrations<I, O, R, P, W>(
303 step: &ChunkStep<I, O, R, P, W>,
304 components: &ChunkComponentRevisions,
305) -> Result<(), DefinitionError> {
306 let mut seen: BTreeSet<&ComponentStreamIdentity> = BTreeSet::new();
307 for (identity, _, contract, _owner) in &step.streams {
308 if !seen.insert(identity) {
309 return Err(DefinitionError::DuplicateRuntimeStream {
310 namespace: identity.clone(),
311 });
312 }
313 if components
314 .stream_revisions()
315 .all(|(declared, _)| declared != identity)
316 {
317 return Err(DefinitionError::RuntimeStreamNotDeclared {
318 namespace: identity.clone(),
319 });
320 }
321 if contract.restartability() == RestartabilityDeclaration::NotRestartable {
322 return Err(DefinitionError::NonRestartableStream {
323 namespace: identity.clone(),
324 });
325 }
326 }
327 for (declared, _) in components.stream_revisions() {
328 if !step
329 .streams
330 .iter()
331 .any(|(identity, _, _, _)| identity == declared)
332 {
333 return Err(DefinitionError::DeclaredStreamMissingRuntime {
334 namespace: declared.clone(),
335 });
336 }
337 }
338 Ok(())
339}
340
341pub fn completion_policy_revision(
371 policy: &dyn CompletionPolicy,
372) -> Result<ComponentRevision, DefinitionError> {
373 let fingerprint = catch_unwind(AssertUnwindSafe(|| policy.fingerprint()))
374 .map_err(|_| DefinitionError::CompletionPolicyFingerprintPanic)?;
375 let digest: [u8; 32] = Sha256::digest(fingerprint.as_bytes()).into();
376 let hex = digest.iter().fold(String::new(), |mut hex, byte| {
377 use std::fmt::Write as _;
378 let _ = write!(hex, "{byte:02x}");
379 hex
380 });
381 ComponentRevision::new(format!("completion:{hex}"))
382}
383
384pub(crate) fn completion_policy_component_revisions<I, O, R, P, W>(
411 step: &ChunkStep<I, O, R, P, W>,
412 components: &ChunkComponentRevisions,
413) -> Result<ChunkComponentRevisions, DefinitionError> {
414 let Some(policy) = step.completion_policy.as_ref() else {
415 return Ok(components.clone());
416 };
417 let revision = completion_policy_revision(policy.as_ref())?;
418 Ok(components.clone().with_completion_policy_revision(revision))
419}
420
421fn validate_completion_policy_revision<I, O, R, P, W>(
439 step: &ChunkStep<I, O, R, P, W>,
440 revisions: &ChunkComponentRevisions,
441) -> Result<(), DefinitionError> {
442 let expected = step
443 .completion_policy
444 .as_ref()
445 .map(|policy| completion_policy_revision(policy.as_ref()))
446 .transpose()?;
447 if expected.as_ref() == revisions.completion_policy_revision() {
448 Ok(())
449 } else {
450 Err(DefinitionError::CompletionPolicyRevisionMismatch)
451 }
452}
453
454impl<I, O, R, P, W> fmt::Debug for ChunkStep<I, O, R, P, W> {
455 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
456 formatter
457 .debug_struct("ChunkStep")
458 .field("name", &self.name)
459 .field("size", &self.size)
460 .field("chunk_listener_count", &self.listeners.len())
461 .field("step_listener_count", &self.step_listeners.len())
462 .field("stream_count", &self.streams.len())
463 .finish_non_exhaustive()
464 }
465}
466
467pub struct ChunkJob<I, O, R, P, W> {
469 name: JobName,
470 step_name: StepName,
471 plan: CompiledExecutionPlan,
472 tasklet: Arc<ChunkTasklet<I, O, R, P, W>>,
473 step_listeners: Vec<Arc<dyn StepExecutionListener>>,
474 listeners: Vec<Arc<dyn JobExecutionListener>>,
475}
476
477impl<I, O, R, P, W> ChunkJob<I, O, R, P, W> {
478 pub fn new(
487 name: JobName,
488 mut step: ChunkStep<I, O, R, P, W>,
489 revision: DefinitionRevision,
490 components: &ChunkComponentRevisions,
491 ) -> Result<Self, DefinitionError>
492 where
493 R: ItemReader<I> + Send + 'static,
494 P: ItemProcessor<I, O> + Send + 'static,
495 W: ItemWriter<O> + Send + 'static,
496 {
497 if let Some(fault) = step.fault.as_ref()
498 && fault.delivery_mode() != components.delivery_mode()
499 {
500 return Err(DefinitionError::DeliveryModeMismatch);
501 }
502 validate_stream_registrations(&step, components)?;
503 let components = completion_policy_component_revisions(&step, components)?;
504 let components = &components;
505 let step_name = step.name.clone();
506 let definition =
507 DefinitionIdentity::chunk(&name, &step_name, step.size, revision, components)?;
508 step.in_flight_policy = components.in_flight_policy();
509 step.definition_digest = *definition.manifest_digest();
510 let mut node = one_step_node(
511 &step_name,
512 StepComponents::Chunk {
513 size: step.size,
514 revisions: Box::new(components.clone()),
515 },
516 )?;
517 if let Some(fault) = step.fault.as_ref() {
518 node = node.with_fault_policy(fault.policy().clone());
519 }
520 let plan = lower_one_step(definition, node)?;
521 let step_listeners = step.step_listeners.clone();
522 Ok(Self {
523 name,
524 step_name,
525 plan,
526 tasklet: Arc::new(ChunkTasklet::new(step)),
527 step_listeners,
528 listeners: Vec::new(),
529 })
530 }
531
532 #[must_use]
537 pub const fn compiled_plan(&self) -> &CompiledExecutionPlan {
538 &self.plan
539 }
540
541 #[must_use]
543 pub const fn definition_identity(&self) -> &DefinitionIdentity {
544 self.plan.definition_identity()
545 }
546
547 #[must_use]
549 pub fn with_listener(mut self, listener: Arc<dyn JobExecutionListener>) -> Self {
550 self.listeners.push(listener);
551 self
552 }
553
554 #[must_use]
556 pub const fn name(&self) -> &JobName {
557 &self.name
558 }
559
560 #[must_use]
562 pub const fn step_name(&self) -> &StepName {
563 &self.step_name
564 }
565}
566
567impl<I, O, R, P, W> fmt::Debug for ChunkJob<I, O, R, P, W> {
568 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
569 formatter
570 .debug_struct("ChunkJob")
571 .field("name", &self.name)
572 .field("step_name", &self.step_name)
573 .field("definition", self.plan.definition_identity())
574 .field("listener_count", &self.listeners.len())
575 .field("step_listener_count", &self.step_listeners.len())
576 .finish_non_exhaustive()
577 }
578}
579
580impl crate::FlowJob {
581 pub fn with_chunk_step<I, O, R, P, W>(
608 mut self,
609 node_id: crate::NodeId,
610 mut step: ChunkStep<I, O, R, P, W>,
611 revisions: &ChunkComponentRevisions,
612 ) -> Result<Self, crate::FlowJobError>
613 where
614 I: Send + Sync + 'static,
615 O: Send + Sync + 'static,
616 R: ItemReader<I> + Send + 'static,
617 P: ItemProcessor<I, O> + Send + 'static,
618 W: ItemWriter<O> + Send + 'static,
619 {
620 let Some(crate::FlowNode::Step(compiled)) = self.compiled_plan().node(&node_id) else {
621 return Err(crate::FlowJobError::WrongNodeKind { node: node_id });
622 };
623 if validate_completion_policy_revision(&step, revisions).is_err() {
624 return Err(crate::FlowJobError::ComponentMismatch { node: node_id });
625 }
626 let expected = StepComponents::Chunk {
627 size: step.size,
628 revisions: Box::new(revisions.clone()),
629 };
630 if compiled.step_name() != step.name()
631 || compiled.components() != &expected
632 || compiled.fault_policy() != step.fault.as_ref().map(FaultRuntime::policy)
633 {
634 return Err(crate::FlowJobError::ComponentMismatch { node: node_id });
635 }
636 if validate_stream_registrations(&step, revisions).is_err() {
637 return Err(crate::FlowJobError::ComponentMismatch { node: node_id });
638 }
639 step.definition_digest = *self.compiled_plan().fingerprint();
640 step.in_flight_policy = revisions.in_flight_policy();
641 let listeners = step.step_listeners.clone();
642 let tasklet: Arc<dyn Tasklet> = Arc::new(ChunkTasklet::new(step));
643 let mut tasklet_step = TaskletStep::new(compiled.step_name().clone(), tasklet);
644 for listener in listeners {
645 tasklet_step = tasklet_step.with_listener(listener);
646 }
647 self.bind_chunk_tasklet(node_id, tasklet_step)?;
648 Ok(self)
649 }
650}
651
652struct ChunkTasklet<I, O, R, P, W> {
653 step: AsyncMutex<ChunkStep<I, O, R, P, W>>,
654 last_report: Mutex<Option<ChunkExecutionReport>>,
655}
656
657impl<I, O, R, P, W> ChunkTasklet<I, O, R, P, W>
658where
659 R: ItemReader<I>,
660 P: ItemProcessor<I, O>,
661 W: ItemWriter<O>,
662{
663 fn new(step: ChunkStep<I, O, R, P, W>) -> Self {
664 Self {
665 step: AsyncMutex::new(step),
666 last_report: Mutex::new(None),
667 }
668 }
669
670 fn take_last_report(&self) -> Option<ChunkExecutionReport> {
671 self.last_report
672 .lock()
673 .unwrap_or_else(std::sync::PoisonError::into_inner)
674 .take()
675 }
676
677 fn clear_last_report(&self) {
678 *self
679 .last_report
680 .lock()
681 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
682 }
683}
684
685impl<I, O, R, P, W> Tasklet for ChunkTasklet<I, O, R, P, W>
686where
687 I: Send + Sync + 'static,
688 O: Send + Sync + 'static,
689 R: ItemReader<I> + Send + 'static,
690 P: ItemProcessor<I, O> + Send + 'static,
691 W: ItemWriter<O> + Send + 'static,
692{
693 fn execute<'a>(
694 &'a self,
695 context: TaskletContext<'a>,
696 ) -> BoxFuture<'a, Result<TaskletOutcome, TaskletError>> {
697 Box::pin(async move {
698 let mut step = self.step.lock().await;
699 let transaction_context = ChunkTransactionContext::new(
700 context.job_execution_id(),
701 context.step_execution_id(),
702 );
703 let report = execute_chunk_step(
704 &mut step,
705 context.correlation(),
706 context.stop_token(),
707 Some(transaction_context),
708 |event| match event {
709 ChunkRuntimeEvent::Started(sequence) => {
710 context.emit_chunk_event(LifecycleEventKind::ChunkStarted, sequence);
711 }
712 ChunkRuntimeEvent::Committed(sequence) => {
713 context.emit_chunk_event(LifecycleEventKind::ChunkCommitted, sequence);
714 }
715 ChunkRuntimeEvent::RolledBack(sequence) => {
716 context.emit_chunk_event(LifecycleEventKind::ChunkRolledBack, sequence);
717 }
718 ChunkRuntimeEvent::Unknown(sequence) => {
719 context.emit_chunk_event(LifecycleEventKind::ChunkUnknown, sequence);
720 }
721 ChunkRuntimeEvent::Fault(fault) => context.emit_fault_event(&fault),
722 },
723 )
724 .await;
725 let outcome = match report.outcome() {
726 ChunkExecutionOutcome::Completed => Ok(TaskletOutcome::Completed),
727 ChunkExecutionOutcome::Stopped => Ok(TaskletOutcome::Stopped),
728 ChunkExecutionOutcome::Failed(_) => Err(TaskletError::new()),
729 ChunkExecutionOutcome::Unknown => Ok(TaskletOutcome::CommitOutcomeUnknown),
730 };
731 if report.terminal_rollback {
732 context.acknowledge_terminal_rollback();
733 }
734 *self
735 .last_report
736 .lock()
737 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(report);
738 outcome
739 })
740 }
741}
742
743#[derive(Clone, Debug, Eq, PartialEq)]
745pub struct ChunkLaunchReport {
746 launch: LaunchReport,
747 chunk: Option<ChunkExecutionReport>,
748}
749
750impl ChunkLaunchReport {
751 #[must_use]
753 pub const fn launch(&self) -> &LaunchReport {
754 &self.launch
755 }
756
757 #[must_use]
762 pub const fn chunk(&self) -> Option<&ChunkExecutionReport> {
763 self.chunk.as_ref()
764 }
765}
766
767impl JobLauncher<'_> {
768 pub async fn launch_chunk<I, O, R, P, W>(
778 &self,
779 job: &mut ChunkJob<I, O, R, P, W>,
780 parameters: &JobParameters,
781 stop: &StopToken,
782 ) -> Result<ChunkLaunchReport, LaunchError>
783 where
784 I: Send + Sync + 'static,
785 O: Send + Sync + 'static,
786 R: ItemReader<I> + Send + 'static,
787 P: ItemProcessor<I, O> + Send + 'static,
788 W: ItemWriter<O> + Send + 'static,
789 {
790 job.tasklet.clear_last_report();
791 let tasklet: Arc<dyn Tasklet> = job.tasklet.clone();
792 let mut tasklet_step = TaskletStep::new(job.step_name.clone(), tasklet);
793 for listener in &job.step_listeners {
794 tasklet_step = tasklet_step.with_listener(Arc::clone(listener));
795 }
796 let mut tasklet_job =
797 TaskletJob::from_lowered_plan(job.name.clone(), tasklet_step, job.plan.clone());
798 for listener in &job.listeners {
799 tasklet_job = tasklet_job.with_listener(Arc::clone(listener));
800 }
801
802 let launch = self.launch(&tasklet_job, parameters, stop).await?;
803 let chunk = job.tasklet.take_last_report();
804 Ok(ChunkLaunchReport { launch, chunk })
805 }
806}
807
808#[derive(Clone, Copy, Debug)]
810pub struct ChunkListenerContext<'a> {
811 sequence: ChunkCount,
812 committed_counts: ChunkCounts,
813 stop: &'a StopToken,
814}
815
816impl<'a> ChunkListenerContext<'a> {
817 const fn new(sequence: ChunkCount, committed_counts: ChunkCounts, stop: &'a StopToken) -> Self {
818 Self {
819 sequence,
820 committed_counts,
821 stop,
822 }
823 }
824
825 #[must_use]
827 pub const fn sequence(self) -> ChunkCount {
828 self.sequence
829 }
830
831 #[must_use]
833 pub const fn committed_counts(self) -> ChunkCounts {
834 self.committed_counts
835 }
836
837 #[must_use]
839 pub const fn stop_token(self) -> &'a StopToken {
840 self.stop
841 }
842}
843
844#[derive(Clone, Copy, Debug, Eq, PartialEq)]
846#[non_exhaustive]
847pub enum ChunkAttemptOutcome {
848 Committed,
850 RolledBack,
852 Stopped,
854 Unknown,
856}
857
858#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
860pub struct ChunkListenerError;
861
862impl ChunkListenerError {
863 #[must_use]
865 pub const fn new() -> Self {
866 Self
867 }
868}
869
870impl fmt::Display for ChunkListenerError {
871 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
872 formatter.write_str("chunk listener failed")
873 }
874}
875
876impl std::error::Error for ChunkListenerError {}
877
878pub trait ChunkListener: Send + Sync {
880 fn before_chunk<'a>(
882 &'a self,
883 context: ChunkListenerContext<'a>,
884 ) -> BoxFuture<'a, Result<(), ChunkListenerError>>;
885
886 fn after_chunk<'a>(
888 &'a self,
889 context: ChunkListenerContext<'a>,
890 outcome: ChunkAttemptOutcome,
891 ) -> BoxFuture<'a, Result<(), ChunkListenerError>>;
892}
893
894#[derive(Clone, Copy, Debug, Eq, PartialEq)]
896#[non_exhaustive]
897pub enum ChunkListenerFailureKind {
898 Error,
900 Panic,
902}
903
904#[derive(Clone, Copy, Debug, Eq, PartialEq)]
906#[non_exhaustive]
907pub enum ChunkListenerPhase {
908 BeforeChunk,
910 AfterChunk,
912}
913
914#[derive(Clone, Copy, Debug, Eq, PartialEq)]
916pub struct ChunkListenerFailure {
917 phase: ChunkListenerPhase,
918 registration_index: usize,
919 kind: ChunkListenerFailureKind,
920}
921
922impl ChunkListenerFailure {
923 const fn new(
924 phase: ChunkListenerPhase,
925 registration_index: usize,
926 kind: ChunkListenerFailureKind,
927 ) -> Self {
928 Self {
929 phase,
930 registration_index,
931 kind,
932 }
933 }
934
935 #[must_use]
937 pub const fn phase(self) -> ChunkListenerPhase {
938 self.phase
939 }
940
941 #[must_use]
943 pub const fn registration_index(self) -> usize {
944 self.registration_index
945 }
946
947 #[must_use]
949 pub const fn kind(self) -> ChunkListenerFailureKind {
950 self.kind
951 }
952}
953
954#[derive(Clone, Copy, Debug, Eq, PartialEq)]
956#[non_exhaustive]
957pub enum ChunkFailure {
958 Count,
960 Reader,
962 ReaderPanic,
964 Processor,
966 ProcessorPanic,
968 Writer,
970 WriterPanic,
972 TransactionBegin,
974 TransactionCommit,
976 TransactionRollback,
978 Completion,
980 CompletionPanic,
982 Listener,
984 ListenerPanic,
986 ItemListener,
988 ItemListenerPanic,
990 RetryReservation,
992 FaultState,
994 RetryStateExhausted,
996 UnsupportedCapability,
998 StreamOpen,
1001 StreamUpdate,
1004 StreamClose,
1006 CompletionPolicyPanic,
1009}
1010
1011#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1013#[non_exhaustive]
1014pub enum ChunkExecutionOutcome {
1015 Completed,
1017 Stopped,
1019 Failed(ChunkFailure),
1021 Unknown,
1023}
1024
1025#[derive(Clone, Debug, Eq, PartialEq)]
1027pub struct ChunkExecutionReport {
1028 outcome: ChunkExecutionOutcome,
1029 original_outcome: Option<ChunkExecutionOutcome>,
1030 committed_counts: ChunkCounts,
1031 committed_chunks: ChunkCount,
1032 rolled_back_chunks: ChunkCount,
1033 listener_failures: Vec<ChunkListenerFailure>,
1034 item_listener_failures: Vec<ItemListenerFailure>,
1035 skip_counts: SkipCounts,
1036 retry_counts: RetryCounts,
1037 rollback_count: u64,
1038 no_rollback_count: u64,
1039 terminal_rollback: bool,
1040 stream_close_failed: bool,
1041}
1042
1043impl ChunkExecutionReport {
1044 #[must_use]
1046 pub const fn outcome(&self) -> ChunkExecutionOutcome {
1047 self.outcome
1048 }
1049
1050 #[must_use]
1060 pub const fn stream_close_failed(&self) -> bool {
1061 self.stream_close_failed
1062 }
1063
1064 fn with_stream_close_failure(mut self) -> Self {
1067 if matches!(self.outcome, ChunkExecutionOutcome::Completed) {
1068 self.original_outcome = Some(self.outcome);
1069 self.outcome = ChunkExecutionOutcome::Failed(ChunkFailure::StreamClose);
1070 }
1071 self.stream_close_failed = true;
1072 self
1073 }
1074
1075 #[must_use]
1077 pub const fn original_outcome(&self) -> Option<ChunkExecutionOutcome> {
1078 self.original_outcome
1079 }
1080
1081 #[must_use]
1083 pub const fn committed_counts(&self) -> ChunkCounts {
1084 self.committed_counts
1085 }
1086
1087 #[must_use]
1089 pub const fn committed_chunks(&self) -> ChunkCount {
1090 self.committed_chunks
1091 }
1092
1093 #[must_use]
1095 pub const fn rolled_back_chunks(&self) -> ChunkCount {
1096 self.rolled_back_chunks
1097 }
1098
1099 #[must_use]
1101 pub fn listener_failures(&self) -> &[ChunkListenerFailure] {
1102 &self.listener_failures
1103 }
1104
1105 #[must_use]
1107 pub fn item_listener_failures(&self) -> &[ItemListenerFailure] {
1108 &self.item_listener_failures
1109 }
1110
1111 #[must_use]
1118 pub const fn skip_counts(&self) -> SkipCounts {
1119 self.skip_counts
1120 }
1121
1122 #[must_use]
1126 pub const fn retry_counts(&self) -> RetryCounts {
1127 self.retry_counts
1128 }
1129
1130 #[must_use]
1136 pub const fn rollback_count(&self) -> u64 {
1137 self.rollback_count
1138 }
1139
1140 #[must_use]
1143 pub const fn no_rollback_count(&self) -> u64 {
1144 self.no_rollback_count
1145 }
1146}
1147
1148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1149pub(crate) enum ChunkRuntimeEvent {
1150 Started(ChunkCount),
1151 Committed(ChunkCount),
1152 RolledBack(ChunkCount),
1153 Unknown(ChunkCount),
1154 Fault(FaultRuntimeEvent),
1155}
1156
1157#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1159pub(crate) struct FaultRuntimeEvent {
1160 pub(crate) kind: LifecycleEventKind,
1161 pub(crate) sequence: ChunkCount,
1162 pub(crate) phase: FaultPhase,
1163 pub(crate) summary: Option<FailureSummary>,
1164 pub(crate) ordinal: Option<RetryOrdinal>,
1165 pub(crate) backoff: Option<Duration>,
1166}
1167
1168impl FaultRuntimeEvent {
1169 const fn new(kind: LifecycleEventKind, sequence: ChunkCount, phase: FaultPhase) -> Self {
1170 Self {
1171 kind,
1172 sequence,
1173 phase,
1174 summary: None,
1175 ordinal: None,
1176 backoff: None,
1177 }
1178 }
1179
1180 const fn with_summary(mut self, summary: FailureSummary) -> Self {
1181 self.summary = Some(summary);
1182 self
1183 }
1184
1185 const fn with_ordinal(mut self, ordinal: RetryOrdinal) -> Self {
1186 self.ordinal = Some(ordinal);
1187 self
1188 }
1189
1190 const fn with_backoff(mut self, backoff: Duration) -> Self {
1191 self.backoff = Some(backoff);
1192 self
1193 }
1194}
1195
1196struct ExecutionState {
1197 committed_counts: ChunkCounts,
1198 committed_chunks: ChunkCount,
1199 rolled_back_chunks: ChunkCount,
1200 listener_failures: Vec<ChunkListenerFailure>,
1201 item_listener_failures: Vec<ItemListenerFailure>,
1202 skip_counts: SkipCounts,
1203 retry_counts: RetryCounts,
1204 rollback_count: u64,
1205 no_rollback_count: u64,
1206 terminal_rollback: bool,
1207 next_failure_id: u64,
1208}
1209
1210impl ExecutionState {
1211 fn new() -> Self {
1212 Self::inheriting(FaultProgress::NONE)
1213 }
1214
1215 fn inheriting(inherited: FaultProgress) -> Self {
1217 Self {
1218 committed_counts: ChunkCounts::default(),
1219 committed_chunks: ChunkCount::ZERO,
1220 rolled_back_chunks: ChunkCount::ZERO,
1221 listener_failures: Vec::new(),
1222 item_listener_failures: Vec::new(),
1223 skip_counts: inherited.skips(),
1224 retry_counts: inherited.retries(),
1225 rollback_count: 0,
1226 no_rollback_count: inherited.no_rollbacks(),
1227 terminal_rollback: false,
1228 next_failure_id: 0,
1229 }
1230 }
1231
1232 fn drain(&mut self) -> Self {
1234 let mut replacement = Self::new();
1235 replacement.skip_counts = self.skip_counts;
1236 replacement.retry_counts = self.retry_counts;
1237 replacement.rollback_count = self.rollback_count;
1238 replacement.no_rollback_count = self.no_rollback_count;
1239 replacement.terminal_rollback = self.terminal_rollback;
1240 replacement.next_failure_id = self.next_failure_id;
1241 std::mem::replace(self, replacement)
1242 }
1243
1244 fn report(
1245 self,
1246 outcome: ChunkExecutionOutcome,
1247 original_outcome: Option<ChunkExecutionOutcome>,
1248 ) -> ChunkExecutionReport {
1249 ChunkExecutionReport {
1250 outcome,
1251 original_outcome,
1252 committed_counts: self.committed_counts,
1253 committed_chunks: self.committed_chunks,
1254 rolled_back_chunks: self.rolled_back_chunks,
1255 listener_failures: self.listener_failures,
1256 item_listener_failures: self.item_listener_failures,
1257 skip_counts: self.skip_counts,
1258 retry_counts: self.retry_counts,
1259 rollback_count: self.rollback_count,
1260 no_rollback_count: self.no_rollback_count,
1261 terminal_rollback: self.terminal_rollback,
1262 stream_close_failed: false,
1263 }
1264 }
1265}
1266
1267struct ItemSlot<I> {
1269 item: I,
1270 ordinal: u64,
1271 skipped: bool,
1272}
1273
1274struct PendingSkip<O> {
1276 phase: FaultPhase,
1277 fault: FaultDescriptor,
1278 disposition: RollbackDisposition,
1279 slot: Option<usize>,
1280 output: Option<O>,
1281}
1282
1283struct PendingRetry {
1285 key: RetryKey,
1286 fault: FaultDescriptor,
1287 entered: usize,
1288}
1289
1290struct ChunkBuffer<I, O> {
1296 slots: Vec<ItemSlot<I>>,
1297 skips: Vec<PendingSkip<O>>,
1298 retry: Option<PendingRetry>,
1299 end_of_input: bool,
1300 base_ordinal: u64,
1301 read_ordinal: u64,
1302 checkpoint_digest: [u8; 32],
1303}
1304
1305impl<I, O> ChunkBuffer<I, O> {
1306 const fn new(base_ordinal: u64, checkpoint_digest: [u8; 32]) -> Self {
1307 Self {
1308 slots: Vec::new(),
1309 skips: Vec::new(),
1310 retry: None,
1311 end_of_input: false,
1312 base_ordinal,
1313 read_ordinal: base_ordinal,
1314 checkpoint_digest,
1315 }
1316 }
1317}
1318
1319fn accepted_fault_progress<O>(skips: &[PendingSkip<O>]) -> Option<ChunkFaultProgress> {
1321 let mut counts = SkipCounts::ZERO;
1322 let mut no_rollbacks = 0_u64;
1323 for skip in skips {
1324 counts = counts.checked_increment(skip.phase).ok()?;
1325 if skip.disposition == RollbackDisposition::CommitSafeSkip {
1326 no_rollbacks = no_rollbacks.checked_add(1)?;
1327 }
1328 }
1329 Some(ChunkFaultProgress::new(counts, no_rollbacks))
1330}
1331
1332fn projected_skips<O>(committed: SkipCounts, skips: &[PendingSkip<O>]) -> Option<SkipCounts> {
1334 skips.iter().try_fold(committed, |counts, skip| {
1335 counts.checked_increment(skip.phase).ok()
1336 })
1337}
1338
1339enum Verdict {
1341 Commit,
1343 Retry(RetryRequest),
1345 Replay,
1347 Terminal(ChunkExecutionOutcome),
1349}
1350
1351struct RetryRequest {
1353 key: RetryKey,
1354 phase: FaultPhase,
1355 fault: FaultDescriptor,
1356 ordinal: RetryOrdinal,
1357 delay: Duration,
1358}
1359
1360enum AttemptResult {
1362 Committed {
1364 counts: ChunkCounts,
1365 receipt: ChunkCommitReceipt,
1366 },
1367 Replay,
1369 RolledBack(ChunkExecutionOutcome),
1371 RollbackFailed(Option<ChunkExecutionOutcome>),
1373 Unknown,
1375}
1376
1377struct Components<'a, I, O, P, W> {
1379 processor: &'a P,
1380 writer: &'a W,
1381 item_listeners: &'a ItemListenerSet<I, O>,
1382 fault: Option<&'a FaultRuntime>,
1383 streams: &'a [StreamRegistration],
1384 step_name: &'a StepName,
1385 definition_digest: [u8; 32],
1386 size: ChunkSize,
1387 completion_policy: Option<&'a dyn CompletionPolicy>,
1388}
1389
1390#[derive(Clone, Copy)]
1392struct AttemptScope<'a> {
1393 correlation: &'a ExecutionCorrelation,
1394 stop: &'a StopToken,
1395 sequence: ChunkCount,
1396}
1397
1398impl<'a> AttemptScope<'a> {
1399 const fn listener_context(self) -> ItemListenerContext<'a> {
1400 ItemListenerContext::new(self.correlation, self.sequence, self.stop)
1401 }
1402}
1403
1404struct AttemptOutputs<O> {
1406 values: Vec<O>,
1407 slots: Vec<usize>,
1408 filtered: u64,
1409}
1410
1411impl<O> AttemptOutputs<O> {
1412 const fn new() -> Self {
1413 Self {
1414 values: Vec::new(),
1415 slots: Vec::new(),
1416 filtered: 0,
1417 }
1418 }
1419
1420 fn reset(&mut self) {
1421 self.values.clear();
1422 self.slots.clear();
1423 self.filtered = 0;
1424 }
1425}
1426
1427enum Invoked<T, E> {
1429 Completed(T),
1430 Failed(E),
1431 Panicked,
1432}
1433
1434#[allow(
1435 clippy::similar_names,
1436 clippy::too_many_lines,
1437 reason = "the chunk loop keeps the canonical attempt, commit, and stop order visible"
1438)]
1439pub(crate) async fn execute_chunk_step<I, O, R, P, W>(
1440 step: &mut ChunkStep<I, O, R, P, W>,
1441 correlation: &ExecutionCorrelation,
1442 stop: &StopToken,
1443 transaction_context: Option<ChunkTransactionContext>,
1444 emit: impl FnMut(ChunkRuntimeEvent),
1445) -> ChunkExecutionReport
1446where
1447 I: Send + Sync,
1448 O: Send + Sync,
1449 R: ItemReader<I>,
1450 P: ItemProcessor<I, O>,
1451 W: ItemWriter<O>,
1452{
1453 let ChunkStep {
1454 name,
1455 size,
1456 reader,
1457 processor,
1458 writer,
1459 transactions,
1460 completion,
1461 completion_policy,
1462 listeners,
1463 item_listeners,
1464 fault,
1465 streams,
1466 in_flight_policy,
1467 definition_digest,
1468 ..
1469 } = step;
1470 let components = Components {
1471 processor,
1472 writer,
1473 item_listeners,
1474 fault: fault.as_ref(),
1475 streams,
1476 step_name: name,
1477 definition_digest: *definition_digest,
1478 size: *size,
1479 completion_policy: completion_policy.as_deref(),
1480 };
1481
1482 let (inherited, opened) = match inherited_progress(
1483 transactions.as_ref(),
1484 fault.as_ref(),
1485 transaction_context,
1486 streams,
1487 stop,
1488 )
1489 .await
1490 {
1491 StreamOpenPhaseResult::Opened { progress, streams } => (progress, streams),
1492 StreamOpenPhaseResult::Failed {
1493 outcome,
1494 close_failed,
1495 } => {
1496 let report = ExecutionState::new().report(outcome, None);
1497 return if close_failed {
1498 report.with_stream_close_failure()
1499 } else {
1500 report
1501 };
1502 }
1503 };
1504 let base_ordinal = inherited.read_ordinal();
1505 let state = ExecutionState::inheriting(inherited.fault());
1506 let buffer = ChunkBuffer::new(base_ordinal, inherited.checkpoint_digest());
1507
1508 let report = run_chunk_loop(
1509 &components,
1510 reader,
1511 correlation,
1512 stop,
1513 listeners,
1514 transactions.as_ref(),
1515 transaction_context,
1516 *in_flight_policy,
1517 completion.as_ref(),
1518 base_ordinal,
1519 buffer,
1520 state,
1521 emit,
1522 )
1523 .await;
1524
1525 close_opened_streams(&opened, stop, report).await
1526}
1527
1528async fn close_opened_streams(
1532 opened: &[Arc<BoxedStream>],
1533 stop: &StopToken,
1534 report: ChunkExecutionReport,
1535) -> ChunkExecutionReport {
1536 if opened.is_empty() {
1537 return report;
1538 }
1539 let runtime_outcome = match report.outcome() {
1540 ChunkExecutionOutcome::Completed => StreamRuntimeOutcome::Committed,
1541 ChunkExecutionOutcome::Stopped => StreamRuntimeOutcome::Stopped,
1542 ChunkExecutionOutcome::Unknown => StreamRuntimeOutcome::Unknown,
1543 ChunkExecutionOutcome::Failed(_) => StreamRuntimeOutcome::Failed,
1544 };
1545 let context = StreamCloseContext::new(stop, runtime_outcome);
1546 let close_failed = close_streams(opened, context).await;
1547 if close_failed {
1548 report.with_stream_close_failure()
1549 } else {
1550 report
1551 }
1552}
1553
1554async fn close_streams(streams: &[Arc<BoxedStream>], context: StreamCloseContext<'_>) -> bool {
1557 let mut close_failed = false;
1558 for stream in streams.iter().rev() {
1559 if stream.close(context).await.is_err() {
1560 close_failed = true;
1561 }
1562 }
1563 close_failed
1564}
1565
1566#[allow(
1573 clippy::similar_names,
1574 clippy::too_many_arguments,
1575 clippy::too_many_lines,
1576 reason = "the chunk loop keeps the canonical attempt, commit, and stop order visible"
1577)]
1578async fn run_chunk_loop<I, O, E, P, W, R>(
1579 components: &Components<'_, I, O, P, W>,
1580 reader: &mut R,
1581 correlation: &ExecutionCorrelation,
1582 stop: &StopToken,
1583 listeners: &[Arc<dyn ChunkListener>],
1584 transactions: &dyn ChunkTransactionManager,
1585 transaction_context: Option<ChunkTransactionContext>,
1586 in_flight_policy: InFlightPolicy,
1587 completion: &dyn ChunkCompletion,
1588 base_ordinal: u64,
1589 mut buffer: ChunkBuffer<I, O>,
1590 mut state: ExecutionState,
1591 mut emit: E,
1592) -> ChunkExecutionReport
1593where
1594 I: Send + Sync,
1595 O: Send + Sync,
1596 E: FnMut(ChunkRuntimeEvent),
1597 P: ItemProcessor<I, O>,
1598 W: ItemWriter<O>,
1599 R: ItemReader<I>,
1600{
1601 let mut sequence = ChunkCount::ZERO;
1602 loop {
1603 if stop.is_stop_requested() {
1604 return state.report(ChunkExecutionOutcome::Stopped, None);
1605 }
1606 sequence = match sequence.checked_increment() {
1607 Ok(value) => value,
1608 Err(_) => {
1609 return state.report(ChunkExecutionOutcome::Failed(ChunkFailure::Count), None);
1610 }
1611 };
1612 let listener_context = ChunkListenerContext::new(sequence, state.committed_counts, stop);
1613
1614 if let Some(failure) = run_before_listeners(listeners, listener_context).await {
1615 let outcome = listener_failure_outcome(failure.kind());
1616 state.listener_failures.push(failure);
1617 return state.report(outcome, None);
1618 }
1619 if stop.is_stop_requested() {
1620 return state.report(ChunkExecutionOutcome::Stopped, None);
1621 }
1622
1623 let begun = match transaction_context {
1624 Some(context) => transactions.begin_for(context).await,
1625 None => transactions.begin().await,
1626 };
1627 let mut transaction = match begun {
1628 Ok(transaction) => transaction,
1629 Err(
1630 ChunkTransactionError::NotCommitted
1631 | ChunkTransactionError::ComponentStateUnsupported,
1632 ) => {
1633 return finish_failed_attempt(
1638 listeners,
1639 CompletionPolicyAttempt::not_begun(components.completion_policy),
1640 listener_context,
1641 ChunkAttemptOutcome::RolledBack,
1642 ChunkExecutionOutcome::Failed(ChunkFailure::TransactionBegin),
1643 &mut state,
1644 )
1645 .await;
1646 }
1647 Err(ChunkTransactionError::CommitOutcomeUnknown) => {
1648 return finish_failed_attempt(
1649 listeners,
1650 CompletionPolicyAttempt::not_begun(components.completion_policy),
1651 listener_context,
1652 ChunkAttemptOutcome::Unknown,
1653 ChunkExecutionOutcome::Unknown,
1654 &mut state,
1655 )
1656 .await;
1657 }
1658 };
1659 emit(ChunkRuntimeEvent::Started(sequence));
1660
1661 let masked_stop;
1666 let attempt_stop = match in_flight_policy {
1667 InFlightPolicy::FinishChunk => {
1668 let (_, token) = crate::StopSource::new();
1669 masked_stop = token;
1670 &masked_stop
1671 }
1672 _ => stop,
1676 };
1677 let scope = AttemptScope {
1678 correlation,
1679 stop: attempt_stop,
1680 sequence,
1681 };
1682
1683 let (result, policy_attempt) = run_attempt(
1684 components,
1685 reader,
1686 scope,
1687 &mut buffer,
1688 &mut state,
1689 transaction.as_mut(),
1690 &mut emit,
1691 )
1692 .await;
1693 drop(transaction);
1694
1695 match result {
1696 AttemptResult::Committed { counts, receipt } => {
1697 let Ok(next_counts) = state.committed_counts.checked_add(counts) else {
1698 return finish_failed_attempt(
1699 listeners,
1700 policy_attempt,
1701 listener_context,
1702 ChunkAttemptOutcome::Committed,
1703 ChunkExecutionOutcome::Failed(ChunkFailure::Count),
1704 &mut state,
1705 )
1706 .await;
1707 };
1708 let Ok(next_chunks) = state.committed_chunks.checked_increment() else {
1709 return finish_failed_attempt(
1710 listeners,
1711 policy_attempt,
1712 listener_context,
1713 ChunkAttemptOutcome::Committed,
1714 ChunkExecutionOutcome::Failed(ChunkFailure::Count),
1715 &mut state,
1716 )
1717 .await;
1718 };
1719 state.committed_counts = next_counts;
1720 state.committed_chunks = next_chunks;
1721 emit(ChunkRuntimeEvent::Committed(sequence));
1722 emit_committed_skips(&buffer, sequence, &mut emit);
1723
1724 let end_of_input = buffer.end_of_input;
1725 let checkpoint_digest = checkpoint_digest(receipt.checkpoint());
1726 let Some(next_ordinal) =
1727 base_ordinal.checked_add(state.committed_counts.read().get())
1728 else {
1729 return finish_failed_attempt(
1730 listeners,
1731 policy_attempt,
1732 listener_context,
1733 ChunkAttemptOutcome::Committed,
1734 ChunkExecutionOutcome::Failed(ChunkFailure::Count),
1735 &mut state,
1736 )
1737 .await;
1738 };
1739 buffer = ChunkBuffer::new(next_ordinal, checkpoint_digest);
1740
1741 let completion_context = ChunkCompletionContext::new(
1742 receipt.checkpoint(),
1743 receipt.execution_context(),
1744 counts,
1745 stop,
1746 );
1747 let terminal_outcome = match invoke_completion(completion, completion_context).await
1748 {
1749 Ok(ChunkCompletionOutcome::Acknowledged) => {
1750 if stop.is_stop_requested() {
1751 Some(ChunkExecutionOutcome::Stopped)
1752 } else if end_of_input {
1753 Some(ChunkExecutionOutcome::Completed)
1754 } else {
1755 None
1756 }
1757 }
1758 Ok(ChunkCompletionOutcome::StoppedAfterCommit) => {
1759 Some(ChunkExecutionOutcome::Stopped)
1760 }
1761 Err(failure) => Some(ChunkExecutionOutcome::Failed(failure)),
1762 };
1763
1764 let after_context =
1765 ChunkListenerContext::new(sequence, state.committed_counts, stop);
1766 let after_failures =
1767 run_after_listeners(listeners, after_context, ChunkAttemptOutcome::Committed)
1768 .await;
1769 let completion_policy_panicked = policy_attempt
1778 .finish(ChunkAttemptOutcome::Committed)
1779 .is_err();
1780 if let Some(first) = after_failures.first().copied() {
1781 state.listener_failures.extend(after_failures);
1782 let original = terminal_outcome.or(Some(ChunkExecutionOutcome::Completed));
1783 return state.report(listener_failure_outcome(first.kind()), original);
1784 }
1785 if completion_policy_panicked {
1786 let original = terminal_outcome.or(Some(ChunkExecutionOutcome::Completed));
1787 return state.report(
1788 ChunkExecutionOutcome::Failed(ChunkFailure::CompletionPolicyPanic),
1789 original,
1790 );
1791 }
1792 if let Some(outcome) = terminal_outcome {
1793 return state.report(outcome, None);
1794 }
1795 }
1796 AttemptResult::Replay => {
1797 if let Err(report) = record_rolled_back_attempt(
1798 listeners,
1799 policy_attempt,
1800 listener_context,
1801 &mut state,
1802 &mut emit,
1803 )
1804 .await
1805 {
1806 return report;
1807 }
1808 }
1809 AttemptResult::RolledBack(ChunkExecutionOutcome::Completed) => {
1810 emit(ChunkRuntimeEvent::RolledBack(sequence));
1813 let failures = run_after_listeners(
1814 listeners,
1815 listener_context,
1816 ChunkAttemptOutcome::RolledBack,
1817 )
1818 .await;
1819 let completion_policy_panicked = policy_attempt
1820 .finish(ChunkAttemptOutcome::RolledBack)
1821 .is_err();
1822 if let Some(first) = failures.first().copied() {
1823 state.listener_failures.extend(failures);
1824 return state
1825 .drain()
1826 .report(listener_failure_outcome(first.kind()), None);
1827 }
1828 if completion_policy_panicked {
1829 return state.drain().report(
1830 ChunkExecutionOutcome::Failed(ChunkFailure::CompletionPolicyPanic),
1831 None,
1832 );
1833 }
1834 return state.report(ChunkExecutionOutcome::Completed, None);
1835 }
1836 AttemptResult::RolledBack(outcome) => {
1837 state.rollback_count = state.rollback_count.saturating_add(1);
1838 state.terminal_rollback = true;
1839 state.rolled_back_chunks = match state.rolled_back_chunks.checked_increment() {
1840 Ok(count) => count,
1841 Err(_) => {
1842 return state.drain().report(
1843 ChunkExecutionOutcome::Failed(ChunkFailure::Count),
1844 Some(outcome),
1845 );
1846 }
1847 };
1848 emit(ChunkRuntimeEvent::RolledBack(sequence));
1849 let attempt_outcome = match outcome {
1850 ChunkExecutionOutcome::Stopped => ChunkAttemptOutcome::Stopped,
1851 _ => ChunkAttemptOutcome::RolledBack,
1852 };
1853 return finish_failed_attempt(
1854 listeners,
1855 policy_attempt,
1856 listener_context,
1857 attempt_outcome,
1858 outcome,
1859 &mut state,
1860 )
1861 .await;
1862 }
1863 AttemptResult::RollbackFailed(original) => {
1864 let completion_policy_panicked =
1869 policy_attempt.finish(ChunkAttemptOutcome::Unknown).is_err();
1870 if completion_policy_panicked {
1871 return state.drain().report(
1872 ChunkExecutionOutcome::Failed(ChunkFailure::CompletionPolicyPanic),
1873 Some(ChunkExecutionOutcome::Failed(
1874 ChunkFailure::TransactionRollback,
1875 )),
1876 );
1877 }
1878 return state.drain().report(
1879 ChunkExecutionOutcome::Failed(ChunkFailure::TransactionRollback),
1880 original,
1881 );
1882 }
1883 AttemptResult::Unknown => {
1884 emit(ChunkRuntimeEvent::Unknown(sequence));
1885 return finish_failed_attempt(
1886 listeners,
1887 policy_attempt,
1888 listener_context,
1889 ChunkAttemptOutcome::Unknown,
1890 ChunkExecutionOutcome::Unknown,
1891 &mut state,
1892 )
1893 .await;
1894 }
1895 }
1896 }
1897}
1898
1899enum StreamOpenPhaseResult {
1905 Opened {
1906 progress: InheritedStepProgress,
1907 streams: Vec<Arc<BoxedStream>>,
1908 },
1909 Failed {
1910 outcome: ChunkExecutionOutcome,
1911 close_failed: bool,
1912 },
1913}
1914
1915async fn inherited_progress(
1916 transactions: &dyn ChunkTransactionManager,
1917 fault: Option<&FaultRuntime>,
1918 context: Option<ChunkTransactionContext>,
1919 streams: &[StreamRegistration],
1920 stop: &StopToken,
1921) -> StreamOpenPhaseResult {
1922 let inherited_state = match context {
1923 None => Vec::new(),
1924 Some(_) if streams.is_empty() => Vec::new(),
1925 Some(context) => match transactions.inherited_component_state(context).await {
1926 Ok(state) => state,
1927 Err(_) => {
1928 return StreamOpenPhaseResult::Failed {
1929 outcome: ChunkExecutionOutcome::Failed(ChunkFailure::StreamOpen),
1930 close_failed: false,
1931 };
1932 }
1933 },
1934 };
1935
1936 let progress = match context {
1937 None => InheritedStepProgress::NONE,
1938 Some(context) => {
1939 if let Some(fault) = fault
1940 && fault.state().bind(context).await.is_err()
1941 {
1942 return StreamOpenPhaseResult::Failed {
1943 outcome: ChunkExecutionOutcome::Failed(ChunkFailure::FaultState),
1944 close_failed: false,
1945 };
1946 }
1947 match transactions.inherited_progress(context).await {
1948 Ok(inherited) => inherited,
1949 Err(ChunkTransactionError::CommitOutcomeUnknown) => {
1950 return StreamOpenPhaseResult::Failed {
1951 outcome: ChunkExecutionOutcome::Unknown,
1952 close_failed: false,
1953 };
1954 }
1955 Err(
1956 ChunkTransactionError::NotCommitted
1957 | ChunkTransactionError::ComponentStateUnsupported,
1958 ) => {
1959 return StreamOpenPhaseResult::Failed {
1960 outcome: ChunkExecutionOutcome::Failed(ChunkFailure::FaultState),
1961 close_failed: false,
1962 };
1963 }
1964 }
1965 }
1966 };
1967
1968 let mut opened: Vec<Arc<BoxedStream>> = Vec::with_capacity(streams.len());
1969 for (identity, stream, contract, _owner) in streams {
1970 let envelope = inherited_state
1971 .iter()
1972 .find(|envelope| envelope.namespace() == identity);
1973 let validated = match envelope.map(|envelope| contract.validate_for_open(envelope)) {
1979 None => None,
1980 Some(Ok(migrated)) => Some(migrated),
1981 Some(Err(_)) => {
1982 let close_context = StreamCloseContext::new(stop, StreamRuntimeOutcome::Failed);
1983 let close_failed = close_streams(&opened, close_context).await;
1984 return StreamOpenPhaseResult::Failed {
1985 outcome: ChunkExecutionOutcome::Failed(ChunkFailure::StreamOpen),
1986 close_failed,
1987 };
1988 }
1989 };
1990 let open_context = StreamOpenContext::new(validated.as_ref(), stop);
1991 if stream.open(open_context).await.is_ok() {
1992 opened.push(Arc::clone(stream));
1993 } else {
1994 let close_context = StreamCloseContext::new(stop, StreamRuntimeOutcome::Failed);
1995 let close_failed = close_streams(&opened, close_context).await;
1996 return StreamOpenPhaseResult::Failed {
1997 outcome: ChunkExecutionOutcome::Failed(ChunkFailure::StreamOpen),
1998 close_failed,
1999 };
2000 }
2001 }
2002 StreamOpenPhaseResult::Opened {
2003 progress,
2004 streams: opened,
2005 }
2006}
2007
2008async fn record_rolled_back_attempt<E>(
2010 listeners: &[Arc<dyn ChunkListener>],
2011 policy_attempt: CompletionPolicyAttempt<'_>,
2012 context: ChunkListenerContext<'_>,
2013 state: &mut ExecutionState,
2014 emit: &mut E,
2015) -> Result<(), ChunkExecutionReport>
2016where
2017 E: FnMut(ChunkRuntimeEvent),
2018{
2019 state.rolled_back_chunks = match state.rolled_back_chunks.checked_increment() {
2020 Ok(count) => count,
2021 Err(_) => {
2022 return Err(state
2023 .drain()
2024 .report(ChunkExecutionOutcome::Failed(ChunkFailure::Count), None));
2025 }
2026 };
2027 emit(ChunkRuntimeEvent::RolledBack(context.sequence()));
2028 let failures = run_after_listeners(listeners, context, ChunkAttemptOutcome::RolledBack).await;
2029 let completion_policy_panicked = policy_attempt
2030 .finish(ChunkAttemptOutcome::RolledBack)
2031 .is_err();
2032 if let Some(first) = failures.first().copied() {
2033 state.listener_failures.extend(failures);
2034 return Err(state
2035 .drain()
2036 .report(listener_failure_outcome(first.kind()), None));
2037 }
2038 if completion_policy_panicked {
2039 return Err(state.drain().report(
2040 ChunkExecutionOutcome::Failed(ChunkFailure::CompletionPolicyPanic),
2041 None,
2042 ));
2043 }
2044 Ok(())
2045}
2046
2047async fn run_attempt<'c, I, O, E, P, W, R>(
2048 components: &Components<'c, I, O, P, W>,
2049 reader: &mut R,
2050 scope: AttemptScope<'_>,
2051 buffer: &mut ChunkBuffer<I, O>,
2052 state: &mut ExecutionState,
2053 transaction: &mut dyn ChunkTransaction,
2054 emit: &mut E,
2055) -> (AttemptResult, CompletionPolicyAttempt<'c>)
2056where
2057 I: Send + Sync,
2058 O: Send + Sync,
2059 E: FnMut(ChunkRuntimeEvent),
2060 P: ItemProcessor<I, O>,
2061 W: ItemWriter<O>,
2062 R: ItemReader<I>,
2063{
2064 let mut outputs = AttemptOutputs::new();
2065 let mut component_state = Vec::new();
2066 let policy_attempt = CompletionPolicyAttempt::begin(components.completion_policy);
2072
2073 let verdict = 'body: {
2074 if policy_attempt.begin_panicked() {
2075 break 'body Verdict::Terminal(ChunkExecutionOutcome::Failed(
2076 ChunkFailure::CompletionPolicyPanic,
2077 ));
2078 }
2079 if let Some(fault) = components.fault
2080 && fault.policy().requires_commit_safe_skip()
2081 && transaction.business_transaction().is_none()
2082 {
2083 break 'body Verdict::Terminal(ChunkExecutionOutcome::Failed(
2084 ChunkFailure::UnsupportedCapability,
2085 ));
2086 }
2087
2088 match read_phase(components, reader, scope, buffer, state, emit).await {
2089 Verdict::Commit => {}
2090 other => break 'body other,
2091 }
2092
2093 if buffer.slots.is_empty() && buffer.end_of_input && buffer.skips.is_empty() {
2094 break 'body Verdict::Terminal(ChunkExecutionOutcome::Completed);
2095 }
2096
2097 match process_phase(components, scope, buffer, state, &mut outputs, emit).await {
2098 Verdict::Commit => {}
2099 other => break 'body other,
2100 }
2101
2102 write_phase(
2103 components,
2104 scope,
2105 buffer,
2106 state,
2107 &mut outputs,
2108 transaction,
2109 &mut component_state,
2110 emit,
2111 )
2112 .await
2113 };
2114
2115 let result = match verdict {
2116 Verdict::Commit => {
2117 commit_attempt(
2118 components,
2119 scope,
2120 buffer,
2121 state,
2122 transaction,
2123 &outputs,
2124 &component_state,
2125 )
2126 .await
2127 }
2128 Verdict::Retry(request) => {
2129 schedule_retry(components, scope, buffer, state, transaction, request, emit).await
2130 }
2131 Verdict::Replay => {
2132 if transaction.rollback().await.is_err() {
2133 AttemptResult::RollbackFailed(None)
2134 } else {
2135 AttemptResult::Replay
2136 }
2137 }
2138 Verdict::Terminal(ChunkExecutionOutcome::Unknown) => AttemptResult::Unknown,
2139 Verdict::Terminal(outcome) => {
2140 if transaction.rollback().await.is_err() {
2141 AttemptResult::RollbackFailed(Some(outcome))
2142 } else {
2143 AttemptResult::RolledBack(outcome)
2144 }
2145 }
2146 };
2147 (result, policy_attempt)
2148}
2149
2150#[allow(
2152 clippy::too_many_lines,
2153 reason = "one phase keeps its listener, classification, and skip order visible"
2154)]
2155async fn read_phase<I, O, E, P, W, R>(
2156 components: &Components<'_, I, O, P, W>,
2157 reader: &mut R,
2158 scope: AttemptScope<'_>,
2159 buffer: &mut ChunkBuffer<I, O>,
2160 state: &mut ExecutionState,
2161 emit: &mut E,
2162) -> Verdict
2163where
2164 I: Send + Sync,
2165 P: ItemProcessor<I, O>,
2166 W: ItemWriter<O>,
2167 R: ItemReader<I>,
2168 O: Send + Sync,
2169 E: FnMut(ChunkRuntimeEvent),
2170{
2171 let listener_context = scope.listener_context();
2172 loop {
2173 if buffer.end_of_input || buffer.slots.len() >= components.size.get() as usize {
2174 break;
2175 }
2176 if !buffer.slots.is_empty()
2184 && let Some(policy) = components.completion_policy
2185 {
2186 match invoke_completion_policy_is_complete(
2187 policy,
2188 ChunkCount::new(buffer.slots.len() as u64),
2189 ) {
2190 Ok(true) => break,
2191 Ok(false) => {}
2192 Err(()) => {
2193 return Verdict::Terminal(ChunkExecutionOutcome::Failed(
2194 ChunkFailure::CompletionPolicyPanic,
2195 ));
2196 }
2197 }
2198 }
2199 if scope.stop.is_stop_requested() {
2200 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
2201 }
2202 let ordinal = buffer.read_ordinal;
2203 let key = retry_key(
2204 components,
2205 buffer.checkpoint_digest,
2206 FaultPhase::Read,
2207 ordinal,
2208 );
2209
2210 let before = components
2211 .item_listeners
2212 .before_read(listener_context)
2213 .await;
2214 if let Some(failure) = before.failure() {
2215 state.item_listener_failures.push(failure);
2216 return Verdict::Terminal(item_listener_outcome(failure.kind()));
2217 }
2218
2219 match invoke_reader(reader, ReadContext::new(scope.stop)).await {
2220 Invoked::Completed(ReadOutcome::Item(item)) => {
2221 let failures = components
2222 .item_listeners
2223 .after_read(before.entered(), &item, listener_context)
2224 .await;
2225 if let Some(first) = failures.first().copied() {
2226 state.item_listener_failures.extend(failures);
2227 return Verdict::Terminal(item_listener_outcome(first.kind()));
2228 }
2229 if let Some(outcome) = complete_retry(
2230 components,
2231 listener_context,
2232 &mut buffer.retry,
2233 state,
2234 key,
2235 RetryOutcome::Recovered,
2236 )
2237 .await
2238 {
2239 return Verdict::Terminal(outcome);
2240 }
2241 resolve_key(components, key).await;
2242 buffer.slots.push(ItemSlot {
2243 item,
2244 ordinal,
2245 skipped: false,
2246 });
2247 buffer.read_ordinal = buffer.read_ordinal.saturating_add(1);
2248 }
2249 Invoked::Completed(ReadOutcome::EndOfInput) => buffer.end_of_input = true,
2250 Invoked::Completed(ReadOutcome::Stopped) => {
2251 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
2252 }
2253 invoked => {
2254 let (error, panicked) = match invoked {
2255 Invoked::Failed(error) => (error, false),
2256 _ => (ReaderError::new(), true),
2257 };
2258 let terminal = if panicked {
2259 ChunkFailure::ReaderPanic
2260 } else {
2261 ChunkFailure::Reader
2262 };
2263 let advanced = !panicked && error.has_checkpoint_advanced();
2264 let Some(fault) = descriptor(
2265 components,
2266 state,
2267 &buffer.skips,
2268 FaultPhase::Read,
2269 error.category(),
2270 ) else {
2271 return Verdict::Terminal(ChunkExecutionOutcome::Failed(ChunkFailure::Count));
2272 };
2273 let fault = with_reserved_ordinal(components, key, fault).await;
2274
2275 let failures = components
2276 .item_listeners
2277 .on_read_error(before.entered(), fault, listener_context)
2278 .await;
2279 if let Some(first) = failures.first().copied() {
2280 state.item_listener_failures.extend(failures);
2281 return Verdict::Terminal(item_listener_outcome(first.kind()));
2282 }
2283
2284 let evidence = FaultEvidence::new(advanced, true, advanced);
2285 let decision = match classify(
2286 components,
2287 listener_context,
2288 &mut buffer.retry,
2289 state,
2290 key,
2291 fault,
2292 evidence,
2293 scope.sequence,
2294 emit,
2295 )
2296 .await
2297 {
2298 Ok(decision) => decision,
2299 Err(outcome) => return Verdict::Terminal(outcome),
2300 };
2301 match decision {
2302 FaultDecision::Retry { ordinal, delay } => {
2303 return Verdict::Retry(RetryRequest {
2304 key,
2305 phase: FaultPhase::Read,
2306 fault,
2307 ordinal,
2308 delay,
2309 });
2310 }
2311 FaultDecision::Skip { disposition } => {
2312 resolve_key(components, key).await;
2313 buffer.read_ordinal = buffer.read_ordinal.saturating_add(1);
2314 buffer.skips.push(PendingSkip {
2315 phase: FaultPhase::Read,
2316 fault,
2317 disposition,
2318 slot: None,
2319 output: None,
2320 });
2321 if disposition == RollbackDisposition::Rollback {
2322 return Verdict::Replay;
2323 }
2324 }
2325 FaultDecision::Unknown => {
2326 return Verdict::Terminal(ChunkExecutionOutcome::Unknown);
2327 }
2328 FaultDecision::Stop => {
2329 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
2330 }
2331 _ => {
2337 return Verdict::Terminal(ChunkExecutionOutcome::Failed(terminal));
2338 }
2339 }
2340 }
2341 }
2342 }
2343 Verdict::Commit
2344}
2345
2346#[allow(
2348 clippy::too_many_lines,
2349 reason = "one phase keeps its listener, classification, and skip order visible"
2350)]
2351async fn process_phase<I, O, E, P, W>(
2352 components: &Components<'_, I, O, P, W>,
2353 scope: AttemptScope<'_>,
2354 buffer: &mut ChunkBuffer<I, O>,
2355 state: &mut ExecutionState,
2356 outputs: &mut AttemptOutputs<O>,
2357 emit: &mut E,
2358) -> Verdict
2359where
2360 I: Send + Sync,
2361 O: Send + Sync,
2362 E: FnMut(ChunkRuntimeEvent),
2363 P: ItemProcessor<I, O>,
2364 W: ItemWriter<O>,
2365{
2366 let listener_context = scope.listener_context();
2367 outputs.reset();
2368 for index in 0..buffer.slots.len() {
2369 if buffer.slots[index].skipped {
2370 continue;
2371 }
2372 if scope.stop.is_stop_requested() {
2373 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
2374 }
2375 let ordinal = buffer.slots[index].ordinal;
2376 let key = retry_key(
2377 components,
2378 buffer.checkpoint_digest,
2379 FaultPhase::Process,
2380 ordinal,
2381 );
2382
2383 let before = components
2384 .item_listeners
2385 .before_process(&buffer.slots[index].item, listener_context)
2386 .await;
2387 if let Some(failure) = before.failure() {
2388 state.item_listener_failures.push(failure);
2389 return Verdict::Terminal(item_listener_outcome(failure.kind()));
2390 }
2391
2392 let invoked = invoke_processor(
2393 components.processor,
2394 &buffer.slots[index].item,
2395 ProcessContext::new(scope.stop),
2396 )
2397 .await;
2398 match invoked {
2399 Invoked::Completed(ProcessOutcome::Item(output)) => {
2400 let failures = components
2401 .item_listeners
2402 .after_process(
2403 before.entered(),
2404 &buffer.slots[index].item,
2405 Some(&output),
2406 listener_context,
2407 )
2408 .await;
2409 if let Some(first) = failures.first().copied() {
2410 state.item_listener_failures.extend(failures);
2411 return Verdict::Terminal(item_listener_outcome(first.kind()));
2412 }
2413 if let Some(outcome) = complete_retry(
2414 components,
2415 listener_context,
2416 &mut buffer.retry,
2417 state,
2418 key,
2419 RetryOutcome::Recovered,
2420 )
2421 .await
2422 {
2423 return Verdict::Terminal(outcome);
2424 }
2425 resolve_key(components, key).await;
2426 outputs.values.push(output);
2427 outputs.slots.push(index);
2428 }
2429 Invoked::Completed(ProcessOutcome::Filtered) => {
2430 let failures = components
2431 .item_listeners
2432 .after_process(
2433 before.entered(),
2434 &buffer.slots[index].item,
2435 None,
2436 listener_context,
2437 )
2438 .await;
2439 if let Some(first) = failures.first().copied() {
2440 state.item_listener_failures.extend(failures);
2441 return Verdict::Terminal(item_listener_outcome(first.kind()));
2442 }
2443 if let Some(outcome) = complete_retry(
2444 components,
2445 listener_context,
2446 &mut buffer.retry,
2447 state,
2448 key,
2449 RetryOutcome::Recovered,
2450 )
2451 .await
2452 {
2453 return Verdict::Terminal(outcome);
2454 }
2455 resolve_key(components, key).await;
2456 outputs.filtered = outputs.filtered.saturating_add(1);
2457 }
2458 Invoked::Completed(ProcessOutcome::Stopped) => {
2459 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
2460 }
2461 invoked => {
2462 let (error, panicked) = match invoked {
2463 Invoked::Failed(error) => (error, false),
2464 _ => (ProcessorError::new(), true),
2465 };
2466 let terminal = if panicked {
2467 ChunkFailure::ProcessorPanic
2468 } else {
2469 ChunkFailure::Processor
2470 };
2471 let Some(fault) = descriptor(
2472 components,
2473 state,
2474 &buffer.skips,
2475 FaultPhase::Process,
2476 error.category(),
2477 ) else {
2478 return Verdict::Terminal(ChunkExecutionOutcome::Failed(ChunkFailure::Count));
2479 };
2480 let fault = with_reserved_ordinal(components, key, fault).await;
2481
2482 let failures = components
2483 .item_listeners
2484 .on_process_error(
2485 before.entered(),
2486 &buffer.slots[index].item,
2487 fault,
2488 listener_context,
2489 )
2490 .await;
2491 if let Some(first) = failures.first().copied() {
2492 state.item_listener_failures.extend(failures);
2493 return Verdict::Terminal(item_listener_outcome(first.kind()));
2494 }
2495
2496 let evidence = FaultEvidence::new(true, true, true);
2499 let decision = match classify(
2500 components,
2501 listener_context,
2502 &mut buffer.retry,
2503 state,
2504 key,
2505 fault,
2506 evidence,
2507 scope.sequence,
2508 emit,
2509 )
2510 .await
2511 {
2512 Ok(decision) => decision,
2513 Err(outcome) => return Verdict::Terminal(outcome),
2514 };
2515 match decision {
2516 FaultDecision::Retry { ordinal, delay } => {
2517 return Verdict::Retry(RetryRequest {
2518 key,
2519 phase: FaultPhase::Process,
2520 fault,
2521 ordinal,
2522 delay,
2523 });
2524 }
2525 FaultDecision::Skip { disposition } => {
2526 resolve_key(components, key).await;
2527 buffer.slots[index].skipped = true;
2528 buffer.skips.push(PendingSkip {
2529 phase: FaultPhase::Process,
2530 fault,
2531 disposition,
2532 slot: Some(index),
2533 output: None,
2534 });
2535 if disposition == RollbackDisposition::Rollback {
2536 return Verdict::Replay;
2537 }
2538 }
2539 FaultDecision::Unknown => {
2540 return Verdict::Terminal(ChunkExecutionOutcome::Unknown);
2541 }
2542 FaultDecision::Stop => {
2543 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
2544 }
2545 _ => {
2551 return Verdict::Terminal(ChunkExecutionOutcome::Failed(terminal));
2552 }
2553 }
2554 }
2555 }
2556 }
2557 Verdict::Commit
2558}
2559
2560#[allow(
2562 clippy::too_many_lines,
2563 reason = "one phase keeps its listener, classification, and skip order visible"
2564)]
2565#[allow(
2566 clippy::too_many_arguments,
2567 reason = "the write boundary needs components, scope, buffer, state, outputs, and the transaction"
2568)]
2569async fn write_phase<I, O, E, P, W>(
2570 components: &Components<'_, I, O, P, W>,
2571 scope: AttemptScope<'_>,
2572 buffer: &mut ChunkBuffer<I, O>,
2573 state: &mut ExecutionState,
2574 outputs: &mut AttemptOutputs<O>,
2575 transaction: &mut dyn ChunkTransaction,
2576 component_state: &mut Vec<ComponentStateEnvelope>,
2577 emit: &mut E,
2578) -> Verdict
2579where
2580 I: Send + Sync,
2581 O: Send + Sync,
2582 E: FnMut(ChunkRuntimeEvent),
2583 P: ItemProcessor<I, O>,
2584 W: ItemWriter<O>,
2585{
2586 if outputs.values.is_empty() {
2587 return update_streams(components, scope, component_state).await;
2588 }
2589 if scope.stop.is_stop_requested() {
2590 return Verdict::Terminal(ChunkExecutionOutcome::Stopped);
2591 }
2592 let listener_context = scope.listener_context();
2593 let key = retry_key(
2594 components,
2595 buffer.checkpoint_digest,
2596 FaultPhase::Write,
2597 buffer.base_ordinal,
2598 );
2599
2600 let before = components
2601 .item_listeners
2602 .before_write(&outputs.values, listener_context)
2603 .await;
2604 if let Some(failure) = before.failure() {
2605 state.item_listener_failures.push(failure);
2606 return Verdict::Terminal(item_listener_outcome(failure.kind()));
2607 }
2608
2609 let write_context = match transaction.business_transaction() {
2610 Some(business) => WriteContext::enlisted(scope.stop, business),
2611 None => WriteContext::non_transactional(scope.stop),
2612 };
2613 match invoke_writer(components.writer, &outputs.values, write_context).await {
2614 Invoked::Completed(WriteOutcome::Written) => {
2615 let failures = components
2616 .item_listeners
2617 .after_write(before.entered(), &outputs.values, listener_context)
2618 .await;
2619 if let Some(first) = failures.first().copied() {
2620 state.item_listener_failures.extend(failures);
2621 return Verdict::Terminal(item_listener_outcome(first.kind()));
2622 }
2623 if let Some(outcome) = complete_retry(
2624 components,
2625 listener_context,
2626 &mut buffer.retry,
2627 state,
2628 key,
2629 RetryOutcome::Recovered,
2630 )
2631 .await
2632 {
2633 return Verdict::Terminal(outcome);
2634 }
2635 resolve_key(components, key).await;
2636 update_streams(components, scope, component_state).await
2637 }
2638 Invoked::Completed(WriteOutcome::Stopped) => {
2639 Verdict::Terminal(ChunkExecutionOutcome::Stopped)
2640 }
2641 invoked => {
2642 let (error, panicked) = match invoked {
2643 Invoked::Failed(error) => (error, false),
2644 _ => (WriterError::new(), true),
2645 };
2646 let terminal = if panicked {
2647 ChunkFailure::WriterPanic
2648 } else {
2649 ChunkFailure::Writer
2650 };
2651 let located = if panicked {
2652 None
2653 } else {
2654 error
2655 .rolled_back_output()
2656 .filter(|index| *index < outputs.values.len())
2657 };
2658 let Some(fault) = descriptor(
2659 components,
2660 state,
2661 &buffer.skips,
2662 FaultPhase::Write,
2663 error.category(),
2664 ) else {
2665 return Verdict::Terminal(ChunkExecutionOutcome::Failed(ChunkFailure::Count));
2666 };
2667 let fault = with_reserved_ordinal(components, key, fault).await;
2668
2669 let failures = components
2670 .item_listeners
2671 .on_write_error(before.entered(), &outputs.values, fault, listener_context)
2672 .await;
2673 if let Some(first) = failures.first().copied() {
2674 state.item_listener_failures.extend(failures);
2675 return Verdict::Terminal(item_listener_outcome(first.kind()));
2676 }
2677
2678 let evidence = FaultEvidence::new(located.is_some(), located.is_some(), false);
2679 let decision = match classify(
2680 components,
2681 listener_context,
2682 &mut buffer.retry,
2683 state,
2684 key,
2685 fault,
2686 evidence,
2687 scope.sequence,
2688 emit,
2689 )
2690 .await
2691 {
2692 Ok(decision) => decision,
2693 Err(outcome) => return Verdict::Terminal(outcome),
2694 };
2695 match decision {
2696 FaultDecision::Retry { ordinal, delay } => Verdict::Retry(RetryRequest {
2697 key,
2698 phase: FaultPhase::Write,
2699 fault,
2700 ordinal,
2701 delay,
2702 }),
2703 FaultDecision::Skip { disposition } => {
2704 let Some(index) = located else {
2705 return Verdict::Terminal(ChunkExecutionOutcome::Failed(terminal));
2706 };
2707 resolve_key(components, key).await;
2708 let slot = outputs.slots[index];
2709 buffer.slots[slot].skipped = true;
2710 buffer.skips.push(PendingSkip {
2711 phase: FaultPhase::Write,
2712 fault,
2713 disposition,
2714 slot: Some(slot),
2715 output: Some(outputs.values.remove(index)),
2716 });
2717 outputs.slots.remove(index);
2718 Verdict::Replay
2719 }
2720 FaultDecision::Unknown => Verdict::Terminal(ChunkExecutionOutcome::Unknown),
2721 FaultDecision::Stop => Verdict::Terminal(ChunkExecutionOutcome::Stopped),
2722 _ => Verdict::Terminal(ChunkExecutionOutcome::Failed(terminal)),
2727 }
2728 }
2729 }
2730}
2731
2732async fn update_streams<I, O, P, W>(
2744 components: &Components<'_, I, O, P, W>,
2745 scope: AttemptScope<'_>,
2746 component_state: &mut Vec<ComponentStateEnvelope>,
2747) -> Verdict {
2748 component_state.clear();
2749 let context = StreamUpdateContext::new(scope.stop);
2750 for (identity, stream, _, _owner) in components.streams {
2751 match stream.update(context).await {
2752 Ok(envelope) if envelope.namespace() == identity => component_state.push(envelope),
2753 Ok(_) | Err(_) => {
2754 component_state.clear();
2755 return Verdict::Terminal(ChunkExecutionOutcome::Failed(
2756 ChunkFailure::StreamUpdate,
2757 ));
2758 }
2759 }
2760 }
2761 Verdict::Commit
2762}
2763
2764async fn commit_attempt<I, O, P, W>(
2766 components: &Components<'_, I, O, P, W>,
2767 scope: AttemptScope<'_>,
2768 buffer: &mut ChunkBuffer<I, O>,
2769 state: &mut ExecutionState,
2770 transaction: &mut dyn ChunkTransaction,
2771 outputs: &AttemptOutputs<O>,
2772 component_state: &[ComponentStateEnvelope],
2773) -> AttemptResult
2774where
2775 I: Send + Sync,
2776 O: Send + Sync,
2777 P: ItemProcessor<I, O>,
2778 W: ItemWriter<O>,
2779{
2780 let listener_context = scope.listener_context();
2781 for skip in &buffer.skips {
2782 let failures = match (skip.phase, skip.slot, skip.output.as_ref()) {
2783 (FaultPhase::Process, Some(index), _) => {
2784 components
2785 .item_listeners
2786 .on_skip_in_process(&buffer.slots[index].item, skip.fault, listener_context)
2787 .await
2788 }
2789 (FaultPhase::Write, _, Some(output)) => {
2790 components
2791 .item_listeners
2792 .on_skip_in_write(output, skip.fault, listener_context)
2793 .await
2794 }
2795 _ => {
2796 components
2797 .item_listeners
2798 .on_skip_in_read(skip.fault, listener_context)
2799 .await
2800 }
2801 };
2802 if let Some(first) = failures.first().copied() {
2803 state.item_listener_failures.extend(failures);
2804 let outcome = item_listener_outcome(first.kind());
2805 if transaction.rollback().await.is_err() {
2806 return AttemptResult::RollbackFailed(Some(outcome));
2807 }
2808 return AttemptResult::RolledBack(outcome);
2809 }
2810 }
2811
2812 let read = ChunkCount::new(buffer.slots.len() as u64);
2813 let processed = ChunkCount::new(outputs.values.len() as u64);
2814 let Ok(counts) = ChunkCounts::new(
2815 read,
2816 processed,
2817 processed,
2818 ChunkCount::new(outputs.filtered),
2819 ) else {
2820 let outcome = ChunkExecutionOutcome::Failed(ChunkFailure::Count);
2821 if transaction.rollback().await.is_err() {
2822 return AttemptResult::RollbackFailed(Some(outcome));
2823 }
2824 return AttemptResult::RolledBack(outcome);
2825 };
2826
2827 let Some(accepted) = accepted_fault_progress(&buffer.skips) else {
2828 let outcome = ChunkExecutionOutcome::Failed(ChunkFailure::Count);
2829 if transaction.rollback().await.is_err() {
2830 return AttemptResult::RollbackFailed(Some(outcome));
2831 }
2832 return AttemptResult::RolledBack(outcome);
2833 };
2834
2835 match transaction
2836 .commit_with_component_state(counts, accepted, component_state)
2837 .await
2838 {
2839 Ok(receipt) => {
2840 let Ok(next_skips) = state.skip_counts.checked_add(accepted.skips()) else {
2841 return AttemptResult::RolledBack(ChunkExecutionOutcome::Failed(
2842 ChunkFailure::Count,
2843 ));
2844 };
2845 state.skip_counts = next_skips;
2846 state.no_rollback_count = state
2847 .no_rollback_count
2848 .saturating_add(accepted.no_rollbacks());
2849 if let Some(fault) = components.fault {
2853 let _ = fault.state().clear_resolved().await;
2854 }
2855 AttemptResult::Committed { counts, receipt }
2856 }
2857 Err(
2858 ChunkTransactionError::NotCommitted | ChunkTransactionError::ComponentStateUnsupported,
2859 ) => {
2860 let outcome = ChunkExecutionOutcome::Failed(ChunkFailure::TransactionCommit);
2861 if transaction.rollback().await.is_err() {
2862 return AttemptResult::RollbackFailed(Some(outcome));
2863 }
2864 AttemptResult::RolledBack(outcome)
2865 }
2866 Err(ChunkTransactionError::CommitOutcomeUnknown) => AttemptResult::Unknown,
2867 }
2868}
2869
2870#[allow(
2872 clippy::too_many_arguments,
2873 reason = "the retry scope needs components, scope, buffer, state, transaction, request, and events"
2874)]
2875async fn schedule_retry<I, O, E, P, W>(
2876 components: &Components<'_, I, O, P, W>,
2877 scope: AttemptScope<'_>,
2878 buffer: &mut ChunkBuffer<I, O>,
2879 state: &mut ExecutionState,
2880 transaction: &mut dyn ChunkTransaction,
2881 request: RetryRequest,
2882 emit: &mut E,
2883) -> AttemptResult
2884where
2885 I: Send + Sync,
2886 O: Send + Sync,
2887 E: FnMut(ChunkRuntimeEvent),
2888 P: ItemProcessor<I, O>,
2889 W: ItemWriter<O>,
2890{
2891 let Some(fault_runtime) = components.fault else {
2892 return AttemptResult::RolledBack(ChunkExecutionOutcome::Failed(
2893 ChunkFailure::RetryReservation,
2894 ));
2895 };
2896 if transaction.rollback().await.is_err() {
2897 return AttemptResult::RollbackFailed(None);
2898 }
2899 if scope.stop.is_stop_requested() {
2900 return AttemptResult::RolledBack(ChunkExecutionOutcome::Stopped);
2901 }
2902
2903 let reservation = RetryReservation::new(
2904 request.key,
2905 request.phase,
2906 request.fault.category(),
2907 request.ordinal,
2908 );
2909 match fault_runtime.state().reserve(reservation).await {
2910 Ok(()) => {}
2911 Err(crate::FaultStateError::CapacityExhausted { .. }) => {
2912 return AttemptResult::RolledBack(ChunkExecutionOutcome::Failed(
2913 ChunkFailure::RetryStateExhausted,
2914 ));
2915 }
2916 Err(_) => {
2917 return AttemptResult::RolledBack(ChunkExecutionOutcome::Failed(
2918 ChunkFailure::RetryReservation,
2919 ));
2920 }
2921 }
2922 state.rollback_count = state.rollback_count.saturating_add(1);
2923 state.retry_counts = state.retry_counts.increment(request.phase);
2924 emit(ChunkRuntimeEvent::Fault(
2925 FaultRuntimeEvent::new(
2926 LifecycleEventKind::RetryReserved,
2927 scope.sequence,
2928 request.phase,
2929 )
2930 .with_summary(request.fault.summary())
2931 .with_ordinal(request.ordinal),
2932 ));
2933 emit(ChunkRuntimeEvent::Fault(
2934 FaultRuntimeEvent::new(
2935 LifecycleEventKind::FaultRollbackCommitted,
2936 scope.sequence,
2937 request.phase,
2938 )
2939 .with_summary(request.fault.summary()),
2940 ));
2941
2942 let listener_context = scope.listener_context();
2943 let before = components
2944 .item_listeners
2945 .before_retry(request.fault, listener_context)
2946 .await;
2947 if let Some(failure) = before.failure() {
2948 state.item_listener_failures.push(failure);
2949 return AttemptResult::RolledBack(item_listener_outcome(failure.kind()));
2950 }
2951 buffer.retry = Some(PendingRetry {
2952 key: request.key,
2953 fault: request.fault,
2954 entered: before.entered(),
2955 });
2956
2957 if scope.stop.is_stop_requested() {
2958 return AttemptResult::RolledBack(ChunkExecutionOutcome::Stopped);
2959 }
2960 emit(ChunkRuntimeEvent::Fault(
2961 FaultRuntimeEvent::new(
2962 LifecycleEventKind::RetryBackoffStarted,
2963 scope.sequence,
2964 request.phase,
2965 )
2966 .with_ordinal(request.ordinal)
2967 .with_backoff(request.delay),
2968 ));
2969 if fault_runtime
2970 .sleeper()
2971 .sleep(request.delay, scope.stop)
2972 .await
2973 == BackoffOutcome::Stopped
2974 {
2975 emit(ChunkRuntimeEvent::Fault(
2976 FaultRuntimeEvent::new(
2977 LifecycleEventKind::RetryBackoffCancelled,
2978 scope.sequence,
2979 request.phase,
2980 )
2981 .with_ordinal(request.ordinal)
2982 .with_backoff(request.delay),
2983 ));
2984 return AttemptResult::RolledBack(ChunkExecutionOutcome::Stopped);
2985 }
2986 if scope.stop.is_stop_requested() {
2987 return AttemptResult::RolledBack(ChunkExecutionOutcome::Stopped);
2988 }
2989 AttemptResult::Replay
2990}
2991
2992async fn complete_retry<I, O, P, W>(
2994 components: &Components<'_, I, O, P, W>,
2995 listener_context: ItemListenerContext<'_>,
2996 retry: &mut Option<PendingRetry>,
2997 state: &mut ExecutionState,
2998 key: RetryKey,
2999 outcome: RetryOutcome,
3000) -> Option<ChunkExecutionOutcome>
3001where
3002 I: Send + Sync,
3003 O: Send + Sync,
3004 P: ItemProcessor<I, O>,
3005 W: ItemWriter<O>,
3006{
3007 let pending = retry.take_if(|pending| pending.key == key)?;
3008 let failures = components
3009 .item_listeners
3010 .after_retry(pending.entered, pending.fault, outcome, listener_context)
3011 .await;
3012 let first = failures.first().copied()?;
3013 state.item_listener_failures.extend(failures);
3014 Some(item_listener_outcome(first.kind()))
3015}
3016
3017#[allow(
3019 clippy::too_many_arguments,
3020 reason = "classification needs components, listeners, retry state, and the fault inputs"
3021)]
3022async fn classify<I, O, E, P, W>(
3023 components: &Components<'_, I, O, P, W>,
3024 listener_context: ItemListenerContext<'_>,
3025 retry: &mut Option<PendingRetry>,
3026 state: &mut ExecutionState,
3027 key: RetryKey,
3028 fault: FaultDescriptor,
3029 evidence: FaultEvidence,
3030 sequence: ChunkCount,
3031 emit: &mut E,
3032) -> Result<FaultDecision, ChunkExecutionOutcome>
3033where
3034 I: Send + Sync,
3035 O: Send + Sync,
3036 E: FnMut(ChunkRuntimeEvent),
3037 P: ItemProcessor<I, O>,
3038 W: ItemWriter<O>,
3039{
3040 let entered = retry
3041 .as_ref()
3042 .filter(|pending| pending.key == key)
3043 .map(|pending| pending.entered);
3044 if entered.is_some()
3045 && let Some(outcome) = complete_retry(
3046 components,
3047 listener_context,
3048 retry,
3049 state,
3050 key,
3051 RetryOutcome::Failed,
3052 )
3053 .await
3054 {
3055 return Err(outcome);
3056 }
3057
3058 let Some(fault_runtime) = components.fault else {
3059 return Ok(FaultDecision::FailAndRollback);
3060 };
3061 let decision = fault_runtime.policy().decide(&fault, evidence);
3062
3063 if !decision.is_retry()
3064 && let Some(entered) = entered
3065 {
3066 emit(ChunkRuntimeEvent::Fault(
3067 FaultRuntimeEvent::new(LifecycleEventKind::RetryExhausted, sequence, fault.phase())
3068 .with_summary(fault.summary())
3069 .with_ordinal(fault.retry_ordinal()),
3070 ));
3071 let failures = components
3072 .item_listeners
3073 .on_retry_exhausted(entered, fault, listener_context)
3074 .await;
3075 if let Some(first) = failures.first().copied() {
3076 state.item_listener_failures.extend(failures);
3077 return Err(item_listener_outcome(first.kind()));
3078 }
3079 }
3080 Ok(decision)
3081}
3082
3083fn descriptor<I, O, P, W>(
3085 components: &Components<'_, I, O, P, W>,
3086 state: &mut ExecutionState,
3087 skips: &[PendingSkip<O>],
3088 phase: FaultPhase,
3089 category: FailureCategory,
3090) -> Option<FaultDescriptor>
3091where
3092 P: ItemProcessor<I, O>,
3093 W: ItemWriter<O>,
3094{
3095 let delivery_mode = components.fault.map_or(
3096 crate::ChunkDeliveryMode::AtLeastOnce,
3097 FaultRuntime::delivery_mode,
3098 );
3099 let committed = projected_skips(state.skip_counts, skips)?;
3100 state.next_failure_id = state.next_failure_id.saturating_add(1);
3101 let failure_id = FailureId::new(state.next_failure_id).ok()?;
3102 Some(FaultDescriptor::new(
3103 phase,
3104 FailureSummary::new(category, failure_id),
3105 RetryOrdinal::INITIAL,
3106 committed,
3107 true,
3108 delivery_mode,
3109 ))
3110}
3111
3112async fn with_reserved_ordinal<I, O, P, W>(
3114 components: &Components<'_, I, O, P, W>,
3115 key: RetryKey,
3116 fault: FaultDescriptor,
3117) -> FaultDescriptor
3118where
3119 P: ItemProcessor<I, O>,
3120 W: ItemWriter<O>,
3121{
3122 let Some(fault_runtime) = components.fault else {
3123 return fault;
3124 };
3125 let ordinal = fault_runtime
3126 .state()
3127 .reserved_ordinal(key)
3128 .await
3129 .ok()
3130 .flatten()
3131 .unwrap_or(RetryOrdinal::INITIAL);
3132 FaultDescriptor::new(
3133 fault.phase(),
3134 fault.summary(),
3135 ordinal,
3136 fault.committed_skips(),
3137 fault.is_transaction_open(),
3138 fault.delivery_mode(),
3139 )
3140}
3141
3142async fn resolve_key<I, O, P, W>(components: &Components<'_, I, O, P, W>, key: RetryKey)
3144where
3145 P: ItemProcessor<I, O>,
3146 W: ItemWriter<O>,
3147{
3148 if let Some(fault_runtime) = components.fault {
3149 let _ = fault_runtime.state().resolve(key).await;
3150 }
3151}
3152
3153fn retry_key<I, O, P, W>(
3154 components: &Components<'_, I, O, P, W>,
3155 checkpoint_digest: [u8; 32],
3156 phase: FaultPhase,
3157 ordinal: u64,
3158) -> RetryKey
3159where
3160 P: ItemProcessor<I, O>,
3161 W: ItemWriter<O>,
3162{
3163 RetryKey::derive(
3164 &components.definition_digest,
3165 components.step_name,
3166 phase,
3167 &checkpoint_digest,
3168 ordinal,
3169 )
3170}
3171
3172fn checkpoint_digest(checkpoint: &crate::Checkpoint) -> [u8; 32] {
3173 checkpoint.generation_digest()
3174}
3175
3176fn emit_committed_skips<I, O, E>(buffer: &ChunkBuffer<I, O>, sequence: ChunkCount, emit: &mut E)
3177where
3178 E: FnMut(ChunkRuntimeEvent),
3179{
3180 for skip in &buffer.skips {
3181 emit(ChunkRuntimeEvent::Fault(
3182 FaultRuntimeEvent::new(LifecycleEventKind::ItemSkipped, sequence, skip.phase)
3183 .with_summary(skip.fault.summary()),
3184 ));
3185 if skip.disposition == RollbackDisposition::CommitSafeSkip {
3186 emit(ChunkRuntimeEvent::Fault(
3187 FaultRuntimeEvent::new(
3188 LifecycleEventKind::FaultNoRollbackCommitted,
3189 sequence,
3190 skip.phase,
3191 )
3192 .with_summary(skip.fault.summary()),
3193 ));
3194 }
3195 }
3196}
3197
3198const fn item_listener_outcome(kind: ListenerFailureKind) -> ChunkExecutionOutcome {
3199 match kind {
3200 ListenerFailureKind::Error => ChunkExecutionOutcome::Failed(ChunkFailure::ItemListener),
3201 ListenerFailureKind::Panic => {
3202 ChunkExecutionOutcome::Failed(ChunkFailure::ItemListenerPanic)
3203 }
3204 }
3205}
3206
3207async fn finish_failed_attempt(
3208 listeners: &[Arc<dyn ChunkListener>],
3209 policy_attempt: CompletionPolicyAttempt<'_>,
3210 context: ChunkListenerContext<'_>,
3211 attempt_outcome: ChunkAttemptOutcome,
3212 outcome: ChunkExecutionOutcome,
3213 state: &mut ExecutionState,
3214) -> ChunkExecutionReport {
3215 let failures = run_after_listeners(listeners, context, attempt_outcome).await;
3216 let completion_policy_panicked = policy_attempt.finish(attempt_outcome).is_err();
3223 if let Some(first) = failures.first().copied() {
3224 state.listener_failures.extend(failures);
3225 if outcome == ChunkExecutionOutcome::Unknown {
3226 return state.drain().report(outcome, None);
3227 }
3228 return state
3229 .drain()
3230 .report(listener_failure_outcome(first.kind()), Some(outcome));
3231 }
3232 if completion_policy_panicked {
3233 if outcome == ChunkExecutionOutcome::Unknown {
3234 return state.drain().report(outcome, None);
3235 }
3236 return state.drain().report(
3237 ChunkExecutionOutcome::Failed(ChunkFailure::CompletionPolicyPanic),
3238 Some(outcome),
3239 );
3240 }
3241 state.drain().report(outcome, None)
3242}
3243
3244async fn run_before_listeners(
3245 listeners: &[Arc<dyn ChunkListener>],
3246 context: ChunkListenerContext<'_>,
3247) -> Option<ChunkListenerFailure> {
3248 for (index, listener) in listeners.iter().enumerate() {
3249 if let Err(kind) = invoke_before_listener(listener.as_ref(), context).await {
3250 return Some(ChunkListenerFailure::new(
3251 ChunkListenerPhase::BeforeChunk,
3252 index,
3253 kind,
3254 ));
3255 }
3256 }
3257 None
3258}
3259
3260async fn run_after_listeners(
3261 listeners: &[Arc<dyn ChunkListener>],
3262 context: ChunkListenerContext<'_>,
3263 outcome: ChunkAttemptOutcome,
3264) -> Vec<ChunkListenerFailure> {
3265 let mut failures = Vec::new();
3266 for (index, listener) in listeners.iter().enumerate().rev() {
3267 if let Err(kind) = invoke_after_listener(listener.as_ref(), context, outcome).await {
3268 failures.push(ChunkListenerFailure::new(
3269 ChunkListenerPhase::AfterChunk,
3270 index,
3271 kind,
3272 ));
3273 }
3274 }
3275 failures
3276}
3277
3278const fn listener_failure_outcome(kind: ChunkListenerFailureKind) -> ChunkExecutionOutcome {
3279 match kind {
3280 ChunkListenerFailureKind::Error => ChunkExecutionOutcome::Failed(ChunkFailure::Listener),
3281 ChunkListenerFailureKind::Panic => {
3282 ChunkExecutionOutcome::Failed(ChunkFailure::ListenerPanic)
3283 }
3284 }
3285}
3286
3287async fn invoke_before_listener(
3288 listener: &dyn ChunkListener,
3289 context: ChunkListenerContext<'_>,
3290) -> Result<(), ChunkListenerFailureKind> {
3291 let future = catch_unwind(AssertUnwindSafe(|| listener.before_chunk(context)))
3292 .map_err(|_| ChunkListenerFailureKind::Panic)?;
3293 match AssertUnwindSafe(future).catch_unwind().await {
3294 Ok(Ok(())) => Ok(()),
3295 Ok(Err(_)) => Err(ChunkListenerFailureKind::Error),
3296 Err(_) => Err(ChunkListenerFailureKind::Panic),
3297 }
3298}
3299
3300async fn invoke_after_listener(
3301 listener: &dyn ChunkListener,
3302 context: ChunkListenerContext<'_>,
3303 outcome: ChunkAttemptOutcome,
3304) -> Result<(), ChunkListenerFailureKind> {
3305 let future = catch_unwind(AssertUnwindSafe(|| listener.after_chunk(context, outcome)))
3306 .map_err(|_| ChunkListenerFailureKind::Panic)?;
3307 match AssertUnwindSafe(future).catch_unwind().await {
3308 Ok(Ok(())) => Ok(()),
3309 Ok(Err(_)) => Err(ChunkListenerFailureKind::Error),
3310 Err(_) => Err(ChunkListenerFailureKind::Panic),
3311 }
3312}
3313
3314async fn invoke_reader<'a, I, R>(
3331 reader: &'a mut R,
3332 context: ReadContext<'a>,
3333) -> Invoked<ReadOutcome<I>, ReaderError>
3334where
3335 R: ItemReader<I>,
3336{
3337 let future = async move { reader.read(context).await };
3338 match AssertUnwindSafe(future).catch_unwind().await {
3339 Ok(Ok(outcome)) => Invoked::Completed(outcome),
3340 Ok(Err(error)) => Invoked::Failed(error),
3341 Err(_) => Invoked::Panicked,
3342 }
3343}
3344
3345async fn invoke_processor<I, O, P>(
3346 processor: &P,
3347 item: &I,
3348 context: ProcessContext<'_>,
3349) -> Invoked<ProcessOutcome<O>, ProcessorError>
3350where
3351 P: ItemProcessor<I, O>,
3352{
3353 let future = async move { processor.process(item, context).await };
3354 match AssertUnwindSafe(future).catch_unwind().await {
3355 Ok(Ok(outcome)) => Invoked::Completed(outcome),
3356 Ok(Err(error)) => Invoked::Failed(error),
3357 Err(_) => Invoked::Panicked,
3358 }
3359}
3360
3361async fn invoke_writer<'a, O, W>(
3362 writer: &'a W,
3363 items: &'a [O],
3364 context: WriteContext<'a>,
3365) -> Invoked<WriteOutcome, WriterError>
3366where
3367 W: ItemWriter<O>,
3368{
3369 let future = async move { writer.write(items, context).await };
3370 match AssertUnwindSafe(future).catch_unwind().await {
3371 Ok(Ok(outcome)) => Invoked::Completed(outcome),
3372 Ok(Err(error)) => Invoked::Failed(error),
3373 Err(_) => Invoked::Panicked,
3374 }
3375}
3376
3377async fn invoke_completion(
3378 completion: &dyn ChunkCompletion,
3379 context: ChunkCompletionContext<'_>,
3380) -> Result<ChunkCompletionOutcome, ChunkFailure> {
3381 let future = catch_unwind(AssertUnwindSafe(|| completion.after_commit(context)))
3382 .map_err(|_| ChunkFailure::CompletionPanic)?;
3383 match AssertUnwindSafe(future).catch_unwind().await {
3384 Ok(Ok(outcome)) => Ok(outcome),
3385 Ok(Err(_)) => Err(ChunkFailure::Completion),
3386 Err(_) => Err(ChunkFailure::CompletionPanic),
3387 }
3388}
3389
3390fn invoke_completion_policy_begin(policy: &dyn CompletionPolicy) -> Result<(), ()> {
3396 catch_unwind(AssertUnwindSafe(|| policy.begin_chunk())).map_err(|_| ())
3397}
3398
3399fn invoke_completion_policy_is_complete(
3402 policy: &dyn CompletionPolicy,
3403 items_read: ChunkCount,
3404) -> Result<bool, ()> {
3405 catch_unwind(AssertUnwindSafe(|| policy.is_complete(items_read))).map_err(|_| ())
3406}
3407
3408fn invoke_completion_policy_end(
3414 policy: Option<&dyn CompletionPolicy>,
3415 outcome: ChunkAttemptOutcome,
3416) -> Result<(), ()> {
3417 let Some(policy) = policy else {
3418 return Ok(());
3419 };
3420 catch_unwind(AssertUnwindSafe(|| policy.end_chunk(outcome))).map_err(|_| ())
3421}
3422
3423struct CompletionPolicyAttempt<'a> {
3438 policy: Option<&'a dyn CompletionPolicy>,
3439 began: bool,
3440 finished: bool,
3441}
3442
3443impl<'a> CompletionPolicyAttempt<'a> {
3444 fn begin(policy: Option<&'a dyn CompletionPolicy>) -> Self {
3450 let began = match policy {
3451 Some(policy) => invoke_completion_policy_begin(policy).is_ok(),
3452 None => false,
3453 };
3454 Self {
3455 policy,
3456 began,
3457 finished: false,
3458 }
3459 }
3460
3461 const fn not_begun(policy: Option<&'a dyn CompletionPolicy>) -> Self {
3466 Self {
3467 policy,
3468 began: false,
3469 finished: false,
3470 }
3471 }
3472
3473 const fn begin_panicked(&self) -> bool {
3477 self.policy.is_some() && !self.began
3478 }
3479
3480 fn finish(mut self, outcome: ChunkAttemptOutcome) -> Result<(), ()> {
3487 self.finished = true;
3488 if !self.began {
3489 return Ok(());
3490 }
3491 invoke_completion_policy_end(self.policy, outcome)
3492 }
3493}
3494
3495impl Drop for CompletionPolicyAttempt<'_> {
3496 fn drop(&mut self) {
3497 if self.finished || !self.began {
3498 return;
3499 }
3500 let _ = invoke_completion_policy_end(self.policy, ChunkAttemptOutcome::Unknown);
3501 }
3502}