1use std::error::Error;
4use std::fmt;
5use std::num::{NonZeroU64, NonZeroUsize};
6use std::panic::{AssertUnwindSafe, catch_unwind};
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::time::Duration;
10
11use futures_util::FutureExt;
12use tokio::sync::{Notify, Semaphore};
13
14use crate::{
15 BatchStatus, BoxFuture, Clock, CompiledExecutionPlan, ComponentRevision, DefinitionError,
16 DefinitionIdentity, DefinitionRevision, ExecutionAttempt, ExecutionCorrelation, ExitStatus,
17 FailureCategory, FailureSummary, FlowSelectionError, FlowTarget, IdGenerator, JobExecution,
18 JobExecutionId, JobExecutionListener, JobInstance, JobInstanceKey, JobName, JobParameters,
19 JobRepository, LifecycleEvent, LifecycleEventKind, LifecycleEventSink, LifecycleTransition,
20 ListenerContext, ListenerFailure, ListenerFailureKind, ListenerPhase, NodeId, RepositoryError,
21 StepComponents, StepExecution, StepExecutionId, StepExecutionListener, StepName, StepNode,
22 TerminalKind,
23};
24
25pub trait Tasklet: Send + Sync {
54 fn execute<'a>(
56 &'a self,
57 context: TaskletContext<'a>,
58 ) -> BoxFuture<'a, Result<TaskletOutcome, TaskletError>>;
59}
60
61pub trait BlockingTasklet: Send + Sync + 'static {
66 fn execute(&self, context: BlockingTaskletContext) -> Result<TaskletOutcome, TaskletError>;
72}
73
74pub struct TaskletStep {
76 name: StepName,
77 tasklet: Arc<dyn Tasklet>,
78 listeners: Vec<Arc<dyn StepExecutionListener>>,
79}
80
81impl TaskletStep {
82 #[must_use]
84 pub fn new(name: StepName, tasklet: Arc<dyn Tasklet>) -> Self {
85 Self {
86 name,
87 tasklet,
88 listeners: Vec::new(),
89 }
90 }
91
92 #[must_use]
94 pub fn with_listener(mut self, listener: Arc<dyn StepExecutionListener>) -> Self {
95 self.listeners.push(listener);
96 self
97 }
98
99 #[must_use]
101 pub const fn name(&self) -> &StepName {
102 &self.name
103 }
104
105 pub(crate) fn tasklet(&self) -> &dyn Tasklet {
106 self.tasklet.as_ref()
107 }
108
109 pub(crate) fn listeners(&self) -> &[Arc<dyn StepExecutionListener>] {
110 &self.listeners
111 }
112}
113
114impl fmt::Debug for TaskletStep {
115 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116 formatter
117 .debug_struct("TaskletStep")
118 .field("name", &self.name)
119 .field("listener_count", &self.listeners.len())
120 .finish_non_exhaustive()
121 }
122}
123
124pub struct TaskletJob {
126 name: JobName,
127 step: TaskletStep,
128 plan: CompiledExecutionPlan,
129 listeners: Vec<Arc<dyn JobExecutionListener>>,
130}
131
132impl fmt::Debug for TaskletJob {
133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134 formatter
135 .debug_struct("TaskletJob")
136 .field("name", &self.name)
137 .field("step", &self.step)
138 .field("definition", self.plan.definition_identity())
139 .field("listener_count", &self.listeners.len())
140 .finish()
141 }
142}
143
144impl TaskletJob {
145 pub fn new(
156 name: JobName,
157 step: TaskletStep,
158 revision: DefinitionRevision,
159 component_revision: &ComponentRevision,
160 ) -> Result<Self, DefinitionError> {
161 let definition =
162 DefinitionIdentity::tasklet(&name, step.name(), revision, component_revision)?;
163 let plan = lower_one_step(
164 definition,
165 one_step_node(
166 step.name(),
167 StepComponents::Tasklet(component_revision.clone()),
168 )?,
169 )?;
170 Ok(Self {
171 name,
172 step,
173 plan,
174 listeners: Vec::new(),
175 })
176 }
177
178 pub(crate) fn from_lowered_plan(
179 name: JobName,
180 step: TaskletStep,
181 plan: CompiledExecutionPlan,
182 ) -> Self {
183 Self {
184 name,
185 step,
186 plan,
187 listeners: Vec::new(),
188 }
189 }
190
191 #[must_use]
193 pub fn with_listener(mut self, listener: Arc<dyn JobExecutionListener>) -> Self {
194 self.listeners.push(listener);
195 self
196 }
197
198 #[must_use]
200 pub const fn name(&self) -> &JobName {
201 &self.name
202 }
203
204 #[must_use]
206 pub const fn step(&self) -> &TaskletStep {
207 &self.step
208 }
209
210 #[must_use]
212 pub const fn definition_identity(&self) -> &DefinitionIdentity {
213 self.plan.definition_identity()
214 }
215
216 #[must_use]
222 pub const fn compiled_plan(&self) -> &CompiledExecutionPlan {
223 &self.plan
224 }
225}
226
227pub(crate) fn one_step_node(
232 step_name: &StepName,
233 components: StepComponents,
234) -> Result<StepNode, DefinitionError> {
235 Ok(StepNode::new(
236 NodeId::new(step_name.as_str())?,
237 step_name.clone(),
238 components,
239 ))
240}
241
242pub(crate) fn lower_one_step(
248 definition: DefinitionIdentity,
249 node: StepNode,
250) -> Result<CompiledExecutionPlan, DefinitionError> {
251 CompiledExecutionPlan::compatibility_one_step(definition, node)
252 .map_err(|_| DefinitionError::CompatibilityLowering)
253}
254
255#[derive(Clone, Copy)]
268pub struct TaskletContext<'a> {
269 parameters: &'a JobParameters,
270 job_execution_id: JobExecutionId,
271 step_execution_id: StepExecutionId,
272 stop: &'a StopToken,
273 correlation: &'a ExecutionCorrelation,
274 event_sink: Option<&'a dyn LifecycleEventSink>,
275 terminal_rollback: &'a AtomicBool,
276}
277
278impl<'a> TaskletContext<'a> {
279 pub(crate) const fn new_for_flow(
280 parameters: &'a JobParameters,
281 job_execution_id: JobExecutionId,
282 step_execution_id: StepExecutionId,
283 stop: &'a StopToken,
284 correlation: &'a ExecutionCorrelation,
285 terminal_rollback: &'a AtomicBool,
286 ) -> Self {
287 Self {
288 parameters,
289 job_execution_id,
290 step_execution_id,
291 stop,
292 correlation,
293 event_sink: None,
294 terminal_rollback,
295 }
296 }
297
298 #[must_use]
300 pub const fn parameters(&self) -> &'a JobParameters {
301 self.parameters
302 }
303
304 #[must_use]
306 pub const fn job_execution_id(self) -> JobExecutionId {
307 self.job_execution_id
308 }
309
310 #[must_use]
312 pub const fn step_execution_id(self) -> StepExecutionId {
313 self.step_execution_id
314 }
315
316 #[must_use]
318 pub const fn stop_token(&self) -> &'a StopToken {
319 self.stop
320 }
321
322 #[must_use]
324 pub const fn correlation(&self) -> &'a ExecutionCorrelation {
325 self.correlation
326 }
327
328 pub(crate) fn emit_chunk_event(&self, kind: LifecycleEventKind, sequence: crate::ChunkCount) {
329 let Some(sink) = self.event_sink else {
330 return;
331 };
332 let event = LifecycleEvent::chunk(kind, self.correlation.clone(), sequence);
333 let _ = catch_unwind(AssertUnwindSafe(|| sink.emit(&event)));
334 }
335
336 pub(crate) fn emit_fault_event(&self, fault: &crate::chunk_runtime::FaultRuntimeEvent) {
337 let Some(sink) = self.event_sink else {
338 return;
339 };
340 let mut event = LifecycleEvent::fault(
341 fault.kind,
342 self.correlation.clone(),
343 fault.sequence,
344 fault.phase,
345 );
346 if let Some(summary) = fault.summary {
347 event = event.with_failure(summary);
348 }
349 if let Some(ordinal) = fault.ordinal {
350 event = event.with_retry_ordinal(ordinal);
351 }
352 if let Some(backoff) = fault.backoff {
353 event = event.with_backoff(backoff);
354 }
355 let _ = catch_unwind(AssertUnwindSafe(|| sink.emit(&event)));
356 }
357
358 pub(crate) fn acknowledge_terminal_rollback(&self) {
359 self.terminal_rollback.store(true, Ordering::Release);
360 }
361
362 fn into_blocking(self) -> BlockingTaskletContext {
363 BlockingTaskletContext {
364 parameters: self.parameters.clone(),
365 job_execution_id: self.job_execution_id,
366 step_execution_id: self.step_execution_id,
367 stop: self.stop.clone(),
368 }
369 }
370}
371
372impl fmt::Debug for TaskletContext<'_> {
373 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
374 formatter
375 .debug_struct("TaskletContext")
376 .field("job_execution_id", &self.job_execution_id)
377 .field("step_execution_id", &self.step_execution_id)
378 .field("stop_requested", &self.stop.is_stop_requested())
379 .field("correlation", &self.correlation)
380 .field("event_sink", &self.event_sink.map(|_| "<attached>"))
381 .field(
382 "terminal_rollback",
383 &self.terminal_rollback.load(Ordering::Acquire),
384 )
385 .finish_non_exhaustive()
386 }
387}
388
389#[derive(Clone, Debug)]
391pub struct BlockingTaskletContext {
392 parameters: JobParameters,
393 job_execution_id: JobExecutionId,
394 step_execution_id: StepExecutionId,
395 stop: StopToken,
396}
397
398impl BlockingTaskletContext {
399 #[must_use]
401 pub const fn parameters(&self) -> &JobParameters {
402 &self.parameters
403 }
404
405 #[must_use]
407 pub const fn job_execution_id(&self) -> JobExecutionId {
408 self.job_execution_id
409 }
410
411 #[must_use]
413 pub const fn step_execution_id(&self) -> StepExecutionId {
414 self.step_execution_id
415 }
416
417 #[must_use]
423 pub const fn stop_token(&self) -> &StopToken {
424 &self.stop
425 }
426}
427
428#[derive(Clone, Debug, Eq, PartialEq)]
430#[non_exhaustive]
431pub enum TaskletOutcome {
432 Completed,
434 CompletedWith(ExitStatus),
439 Stopped,
441 StoppedAfterBlockingWork,
448 CommitOutcomeUnknown,
453}
454
455#[derive(Clone, Copy, Debug, Eq, PartialEq)]
457pub struct TaskletError {
458 kind: TaskletErrorKind,
459}
460
461#[derive(Clone, Copy, Debug, Eq, PartialEq)]
462enum TaskletErrorKind {
463 Component,
464 Panic,
465}
466
467impl TaskletError {
468 #[must_use]
470 pub const fn new() -> Self {
471 Self {
472 kind: TaskletErrorKind::Component,
473 }
474 }
475
476 #[must_use]
478 pub fn from_error(error: impl Error + Send + Sync + 'static) -> Self {
479 drop(error);
480 Self::new()
481 }
482
483 const fn panic() -> Self {
484 Self {
485 kind: TaskletErrorKind::Panic,
486 }
487 }
488}
489
490impl Default for TaskletError {
491 fn default() -> Self {
492 Self::new()
493 }
494}
495
496impl fmt::Display for TaskletError {
497 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
498 formatter.write_str("the tasklet failed")
499 }
500}
501
502impl Error for TaskletError {}
503
504#[derive(Debug)]
505struct StopState {
506 requested: AtomicBool,
507 notify: Notify,
508}
509
510#[derive(Clone, Debug)]
512pub struct StopSource {
513 state: Arc<StopState>,
514}
515
516#[derive(Clone, Debug)]
518pub struct StopToken {
519 state: Arc<StopState>,
520}
521
522impl StopSource {
523 #[must_use]
525 pub fn new() -> (Self, StopToken) {
526 let state = Arc::new(StopState {
527 requested: AtomicBool::new(false),
528 notify: Notify::new(),
529 });
530 (
531 Self {
532 state: Arc::clone(&state),
533 },
534 StopToken { state },
535 )
536 }
537
538 pub fn request_stop(&self) {
540 self.state.requested.store(true, Ordering::Release);
541 self.state.notify.notify_waiters();
542 }
543}
544
545impl StopToken {
546 #[must_use]
548 pub fn is_stop_requested(&self) -> bool {
549 self.state.requested.load(Ordering::Acquire)
550 }
551
552 pub async fn cancelled(&self) {
554 loop {
555 let notified = self.state.notify.notified();
556 if self.is_stop_requested() {
557 return;
558 }
559 notified.await;
560 }
561 }
562
563 pub(crate) fn request_stop(&self) {
564 self.state.requested.store(true, Ordering::Release);
565 self.state.notify.notify_waiters();
566 }
567}
568
569#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
571pub struct StopPollInterval(Duration);
572
573impl StopPollInterval {
574 pub const DEFAULT: Self = Self(Duration::from_secs(1));
576
577 pub fn new(value: Duration) -> Result<Self, LaunchError> {
583 if !(Duration::from_millis(100)..=Duration::from_mins(1)).contains(&value) {
584 return Err(LaunchError::InvalidStopPollInterval);
585 }
586 Ok(Self(value))
587 }
588
589 #[must_use]
591 pub const fn get(self) -> Duration {
592 self.0
593 }
594}
595
596impl Default for StopPollInterval {
597 fn default() -> Self {
598 Self::DEFAULT
599 }
600}
601
602#[derive(Clone, Copy, Debug, Eq, PartialEq)]
604#[non_exhaustive]
605pub enum StopTiming {
606 BeforeStart,
608 DuringExecution,
610 AfterBlockingWork,
613}
614
615#[derive(Clone, Copy, Debug, Eq, PartialEq)]
617#[non_exhaustive]
618pub enum TaskletFailure {
619 Error,
621 Panic,
623 ListenerError,
625 ListenerPanic,
627}
628
629#[derive(Clone, Copy, Debug, Eq, PartialEq)]
631#[non_exhaustive]
632pub enum TaskletExecutionOutcome {
633 Completed,
635 Failed(TaskletFailure),
637 Stopped(StopTiming),
639 Unknown,
641}
642
643#[derive(Clone, Debug, Eq, PartialEq)]
645pub struct LaunchReport {
646 instance: JobInstance,
647 job_execution: JobExecution,
648 step_execution: StepExecution,
649 outcome: TaskletExecutionOutcome,
650 original_outcome: Option<TaskletExecutionOutcome>,
651 original_failure: Option<FailureSummary>,
652 listener_failures: Vec<ListenerFailure>,
653}
654
655impl LaunchReport {
656 #[must_use]
658 pub const fn instance(&self) -> &JobInstance {
659 &self.instance
660 }
661
662 #[must_use]
664 pub const fn job_execution(&self) -> &JobExecution {
665 &self.job_execution
666 }
667
668 #[must_use]
670 pub const fn step_execution(&self) -> &StepExecution {
671 &self.step_execution
672 }
673
674 #[must_use]
676 pub const fn outcome(&self) -> TaskletExecutionOutcome {
677 self.outcome
678 }
679
680 #[must_use]
683 pub const fn original_outcome(&self) -> Option<TaskletExecutionOutcome> {
684 self.original_outcome
685 }
686
687 #[must_use]
690 pub const fn original_failure(&self) -> Option<FailureSummary> {
691 self.original_failure
692 }
693
694 #[must_use]
696 pub fn listener_failures(&self) -> &[ListenerFailure] {
697 &self.listener_failures
698 }
699}
700
701#[derive(Clone, Debug, Eq, PartialEq)]
703#[non_exhaustive]
704pub enum LaunchError {
705 Repository(RepositoryError),
707 Flow(FlowSelectionError),
709 UnsupportedPlan,
715 InvalidStopPollInterval,
717 ShuttingDown,
719}
720
721impl fmt::Display for LaunchError {
722 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
723 match self {
724 Self::Repository(error) => {
725 write!(formatter, "job repository operation failed: {error}")
726 }
727 Self::Flow(error) => write!(formatter, "compiled plan could not route: {error}"),
728 Self::UnsupportedPlan => {
729 formatter.write_str("this launcher executes one-step compatibility plans only")
730 }
731 Self::InvalidStopPollInterval => {
732 formatter.write_str("stop poll interval must be between 100 ms and 60 seconds")
733 }
734 Self::ShuttingDown => formatter.write_str("runtime intake is shutting down"),
735 }
736 }
737}
738
739impl Error for LaunchError {
740 fn source(&self) -> Option<&(dyn Error + 'static)> {
741 match self {
742 Self::Repository(error) => Some(error),
743 Self::Flow(error) => Some(error),
744 Self::UnsupportedPlan | Self::InvalidStopPollInterval | Self::ShuttingDown => None,
745 }
746 }
747}
748
749impl From<RepositoryError> for LaunchError {
750 fn from(error: RepositoryError) -> Self {
751 Self::Repository(error)
752 }
753}
754
755impl From<FlowSelectionError> for LaunchError {
756 fn from(error: FlowSelectionError) -> Self {
757 Self::Flow(error)
758 }
759}
760
761pub struct JobLauncher<'a> {
766 repository: &'a dyn JobRepository,
767 clock: &'a dyn Clock,
768 ids: &'a dyn IdGenerator,
769 event_sink: Option<&'a dyn LifecycleEventSink>,
770 execution_control: Option<(crate::OwnerToken, StopPollInterval)>,
771 shutdown_signal: Option<&'a crate::ShutdownSignal>,
772}
773
774impl<'a> JobLauncher<'a> {
775 #[must_use]
777 pub const fn new(
778 repository: &'a dyn JobRepository,
779 clock: &'a dyn Clock,
780 ids: &'a dyn IdGenerator,
781 ) -> Self {
782 Self {
783 repository,
784 clock,
785 ids,
786 event_sink: None,
787 execution_control: None,
788 shutdown_signal: None,
789 }
790 }
791
792 #[must_use]
794 pub const fn with_event_sink(mut self, event_sink: &'a dyn LifecycleEventSink) -> Self {
795 self.event_sink = Some(event_sink);
796 self
797 }
798
799 #[must_use]
801 pub const fn with_execution_control(
802 mut self,
803 owner: crate::OwnerToken,
804 interval: StopPollInterval,
805 ) -> Self {
806 self.execution_control = Some((owner, interval));
807 self
808 }
809
810 #[must_use]
812 pub const fn with_shutdown_signal(mut self, signal: &'a crate::ShutdownSignal) -> Self {
813 self.shutdown_signal = Some(signal);
814 self
815 }
816
817 #[allow(
831 clippy::too_many_lines,
832 reason = "the launch method keeps the listener nesting and commit order visible"
833 )]
834 pub async fn launch(
835 &self,
836 job: &TaskletJob,
837 parameters: &JobParameters,
838 stop: &StopToken,
839 ) -> Result<LaunchReport, LaunchError> {
840 self.ensure_accepting()?;
841 let key = JobInstanceKey::new(job.name.clone(), parameters);
842 let plan = job.compiled_plan();
843 let graph = self
844 .create_execution_graph(&key, job.step.name(), job.definition_identity())
845 .await?;
846 self.emit_event(LifecycleEventKind::LaunchAccepted, &graph.correlation, None);
847 self.emit_event(LifecycleEventKind::JobStarting, &graph.correlation, None);
848 self.emit_event(LifecycleEventKind::StepStarting, &graph.correlation, None);
849
850 self.poll_execution_control(graph.job_execution.id(), stop)
851 .await?;
852 self.observe_process_shutdown(stop);
853
854 if stop.is_stop_requested() {
855 let (job_execution, step_execution) = self
856 .stop_graph(
857 plan,
858 &graph.job_execution,
859 &graph.step_execution,
860 &graph.correlation,
861 )
862 .await?;
863 return Ok(LaunchReport {
864 instance: graph.instance,
865 job_execution,
866 step_execution,
867 outcome: TaskletExecutionOutcome::Stopped(StopTiming::BeforeStart),
868 original_outcome: None,
869 original_failure: None,
870 listener_failures: Vec::new(),
871 });
872 }
873
874 let context = ListenerContext::new(&graph.correlation, parameters, stop);
875 if let Some(failure) = self.run_before_job(&job.listeners, context).await? {
876 let outcome = listener_failure_outcome(failure.kind());
877 let step_execution = self
878 .finish_step(
879 &graph.step_execution,
880 outcome,
881 Some(failure.summary()),
882 None,
883 false,
884 &graph.correlation,
885 )
886 .await?;
887 let job_execution = self
888 .finish_job(
889 &graph.job_execution,
890 Self::terminal_status(plan, outcome)?,
891 Some(failure.summary()),
892 &graph.correlation,
893 )
894 .await?;
895 return Ok(LaunchReport {
896 instance: graph.instance,
897 job_execution,
898 step_execution,
899 outcome,
900 original_outcome: None,
901 original_failure: None,
902 listener_failures: vec![failure],
903 });
904 }
905
906 let started_job = self
907 .start_job(&graph.job_execution, &graph.correlation)
908 .await?;
909 self.poll_execution_control(started_job.id(), stop).await?;
910 self.observe_process_shutdown(stop);
911 if stop.is_stop_requested() {
912 let (job_execution, step_execution) = self
913 .stop_graph(
914 plan,
915 &started_job,
916 &graph.step_execution,
917 &graph.correlation,
918 )
919 .await?;
920 return Ok(LaunchReport {
921 instance: graph.instance,
922 job_execution,
923 step_execution,
924 outcome: TaskletExecutionOutcome::Stopped(StopTiming::BeforeStart),
925 original_outcome: None,
926 original_failure: None,
927 listener_failures: Vec::new(),
928 });
929 }
930
931 if let Some(failure) = self.run_before_step(&job.step.listeners, context).await? {
932 let outcome = listener_failure_outcome(failure.kind());
933 let step_execution = self
934 .finish_step(
935 &graph.step_execution,
936 outcome,
937 Some(failure.summary()),
938 None,
939 false,
940 &graph.correlation,
941 )
942 .await?;
943 let mut listener_failures = vec![failure];
944 let mut original_outcome = None;
945 let after_job_failures = self.run_after_job(&job.listeners, context, outcome).await?;
946 if !after_job_failures.is_empty() {
947 original_outcome = Some(outcome);
948 listener_failures.extend(after_job_failures);
949 }
950 let final_outcome = listener_failure_outcome(listener_failures[0].kind());
951 let job_execution = self
952 .finish_job(
953 &started_job,
954 Self::terminal_status(plan, final_outcome)?,
955 Some(listener_failures[0].summary()),
956 &graph.correlation,
957 )
958 .await?;
959 return Ok(LaunchReport {
960 instance: graph.instance,
961 job_execution,
962 step_execution,
963 outcome: final_outcome,
964 original_outcome,
965 original_failure: None,
966 listener_failures,
967 });
968 }
969
970 let started_step = self
971 .start_step(&graph.step_execution, &graph.correlation)
972 .await?;
973 let terminal_rollback = AtomicBool::new(false);
974 let tasklet_context = TaskletContext {
975 parameters,
976 job_execution_id: started_job.id(),
977 step_execution_id: started_step.id(),
978 stop,
979 correlation: &graph.correlation,
980 event_sink: self.event_sink,
981 terminal_rollback: &terminal_rollback,
982 };
983 let invocation = self
984 .invoke_with_execution_control(
985 started_job.id(),
986 job.step.tasklet.as_ref(),
987 tasklet_context,
988 stop,
989 )
990 .await?;
991 let mut custom_exit = None;
992 let provisional_outcome = match invocation {
993 Ok(TaskletOutcome::Completed) if !stop.is_stop_requested() => {
994 TaskletExecutionOutcome::Completed
995 }
996 Ok(TaskletOutcome::CompletedWith(exit_status)) if !stop.is_stop_requested() => {
997 custom_exit = Some(exit_status);
998 TaskletExecutionOutcome::Completed
999 }
1000 Ok(
1001 TaskletOutcome::Completed
1002 | TaskletOutcome::CompletedWith(_)
1003 | TaskletOutcome::Stopped,
1004 ) => TaskletExecutionOutcome::Stopped(StopTiming::DuringExecution),
1005 Ok(TaskletOutcome::StoppedAfterBlockingWork) => {
1006 TaskletExecutionOutcome::Stopped(StopTiming::AfterBlockingWork)
1007 }
1008 Ok(TaskletOutcome::CommitOutcomeUnknown) => TaskletExecutionOutcome::Unknown,
1009 Err(failure) => TaskletExecutionOutcome::Failed(failure),
1010 };
1011 let tasklet_failure = if matches!(
1012 provisional_outcome,
1013 TaskletExecutionOutcome::Failed(TaskletFailure::Error | TaskletFailure::Panic)
1014 ) {
1015 Some(self.next_failure_summary()?)
1016 } else {
1017 None
1018 };
1019
1020 let mut outcome = provisional_outcome;
1021 let mut original_outcome = None;
1022 let mut listener_failures = self
1023 .run_after_step(&job.step.listeners, context, outcome)
1024 .await?;
1025 if let Some(failure) = listener_failures.first()
1026 && outcome != TaskletExecutionOutcome::Unknown
1027 {
1028 original_outcome = Some(outcome);
1029 outcome = listener_failure_outcome(failure.kind());
1030 custom_exit = None;
1031 }
1032 let step_failure = listener_failures
1033 .first()
1034 .map(|failure| failure.summary())
1035 .or(tasklet_failure);
1036 let durable_step = self.reload_step(started_step.id()).await?;
1037 let step_execution = self
1038 .finish_step(
1039 &durable_step,
1040 outcome,
1041 step_failure,
1042 custom_exit.as_ref(),
1043 terminal_rollback.load(Ordering::Acquire),
1044 &graph.correlation,
1045 )
1046 .await?;
1047
1048 let after_job_failures = self.run_after_job(&job.listeners, context, outcome).await?;
1049 if !after_job_failures.is_empty() {
1050 if original_outcome.is_none() && outcome != TaskletExecutionOutcome::Unknown {
1051 original_outcome = Some(outcome);
1052 }
1053 if listener_failures.is_empty() && outcome != TaskletExecutionOutcome::Unknown {
1054 outcome = listener_failure_outcome(after_job_failures[0].kind());
1055 }
1056 listener_failures.extend(after_job_failures);
1057 }
1058 let job_failure = listener_failures
1059 .first()
1060 .map(|failure| failure.summary())
1061 .or(tasklet_failure);
1062 let job_execution = self
1063 .finish_job(
1064 &started_job,
1065 Self::terminal_status_for_exit(
1066 plan,
1067 outcome,
1068 step_execution.metadata().exit_status(),
1069 )?,
1070 job_failure,
1071 &graph.correlation,
1072 )
1073 .await?;
1074
1075 Ok(LaunchReport {
1076 instance: graph.instance,
1077 job_execution,
1078 step_execution,
1079 outcome,
1080 original_outcome,
1081 original_failure: original_outcome.and(tasklet_failure),
1082 listener_failures,
1083 })
1084 }
1085
1086 async fn reload_step(&self, id: StepExecutionId) -> Result<StepExecution, LaunchError> {
1087 let mut unit = self.repository.begin().await?;
1088 let step = unit
1089 .get_step_execution(id)
1090 .await?
1091 .ok_or(RepositoryError::StepExecutionNotFound { id })?;
1092 unit.rollback().await?;
1093 Ok(step)
1094 }
1095
1096 async fn poll_execution_control(
1097 &self,
1098 execution_id: JobExecutionId,
1099 stop: &StopToken,
1100 ) -> Result<(), LaunchError> {
1101 let Some((owner, _)) = self.execution_control else {
1102 return Ok(());
1103 };
1104 let mut unit = self.repository.begin().await?;
1105 let control = unit
1106 .observe_execution_control(execution_id, &owner, self.clock.now())
1107 .await?;
1108 unit.commit().await?;
1109 if !control.owner_matches() {
1110 return Err(RepositoryError::ExecutionOwned { id: execution_id }.into());
1111 }
1112 if control.stop_requested() {
1113 stop.request_stop();
1114 }
1115 Ok(())
1116 }
1117
1118 async fn invoke_with_execution_control(
1119 &self,
1120 execution_id: JobExecutionId,
1121 tasklet: &dyn Tasklet,
1122 context: TaskletContext<'_>,
1123 stop: &StopToken,
1124 ) -> Result<Result<TaskletOutcome, TaskletFailure>, LaunchError> {
1125 if self.execution_control.is_none() && self.shutdown_signal.is_none() {
1126 return Ok(invoke_tasklet(tasklet, context).await);
1127 }
1128 let invocation = invoke_tasklet(tasklet, context);
1129 tokio::pin!(invocation);
1130 let mut shutdown_observed = false;
1131 loop {
1132 tokio::select! {
1133 result = &mut invocation => return Ok(result),
1134 () = async {
1135 match self.execution_control {
1136 Some((_, interval)) => tokio::time::sleep(interval.get()).await,
1137 None => std::future::pending().await,
1138 }
1139 } => {
1140 self.poll_execution_control(execution_id, stop).await?;
1141 }
1142 () = async {
1143 match self.shutdown_signal {
1144 Some(signal) => signal.cancelled().await,
1145 None => std::future::pending().await,
1146 }
1147 }, if !shutdown_observed => {
1148 shutdown_observed = true;
1149 stop.request_stop();
1150 }
1151 }
1152 }
1153 }
1154
1155 fn ensure_accepting(&self) -> Result<(), LaunchError> {
1156 self.shutdown_signal.map_or(Ok(()), |signal| {
1157 signal
1158 .ensure_accepting()
1159 .map_err(|_| LaunchError::ShuttingDown)
1160 })
1161 }
1162
1163 fn observe_process_shutdown(&self, stop: &StopToken) {
1164 if self
1165 .shutdown_signal
1166 .is_some_and(crate::ShutdownSignal::is_shutdown_requested)
1167 {
1168 stop.request_stop();
1169 }
1170 }
1171
1172 async fn create_execution_graph(
1173 &self,
1174 key: &JobInstanceKey,
1175 step_name: &StepName,
1176 definition: &DefinitionIdentity,
1177 ) -> Result<CreatedExecutionGraph, LaunchError> {
1178 let mut unit = self.repository.begin().await?;
1179 let instance = unit
1180 .select_or_create_job_instance(key)
1181 .await?
1182 .instance()
1183 .clone();
1184 let job_execution = unit
1185 .create_job_execution_with_definition(instance.id(), definition)
1186 .await?;
1187 let job_execution = if let Some((owner, _)) = self.execution_control {
1188 unit.claim_execution_owner(
1189 job_execution.id(),
1190 job_execution.version(),
1191 &owner,
1192 self.clock.now(),
1193 )
1194 .await?
1195 } else {
1196 job_execution
1197 };
1198 let step_execution = unit
1199 .create_step_execution(job_execution.id(), step_name)
1200 .await?;
1201 let attempt_count = unit.job_executions(instance.id()).await?.len();
1202 let attempt = u64::try_from(attempt_count)
1203 .ok()
1204 .and_then(NonZeroU64::new)
1205 .map(ExecutionAttempt::new)
1206 .ok_or(RepositoryError::Unavailable)?;
1207 unit.commit().await?;
1208 let correlation = ExecutionCorrelation::new(
1209 key.job_name().clone(),
1210 instance.id(),
1211 job_execution.id(),
1212 attempt,
1213 step_name.clone(),
1214 step_execution.id(),
1215 attempt,
1216 );
1217 Ok(CreatedExecutionGraph {
1218 instance,
1219 job_execution,
1220 step_execution,
1221 correlation,
1222 })
1223 }
1224
1225 async fn start_job(
1226 &self,
1227 job: &JobExecution,
1228 correlation: &ExecutionCorrelation,
1229 ) -> Result<JobExecution, LaunchError> {
1230 let now = self.clock.now();
1231 let mut unit = self.repository.begin().await?;
1232 let started_job = unit
1233 .transition_job_execution(
1234 job.id(),
1235 job.version(),
1236 LifecycleTransition::new(BatchStatus::Started, now),
1237 )
1238 .await?;
1239 unit.commit().await?;
1240 self.emit_event(LifecycleEventKind::JobStarted, correlation, None);
1241 Ok(started_job)
1242 }
1243
1244 async fn start_step(
1245 &self,
1246 step: &StepExecution,
1247 correlation: &ExecutionCorrelation,
1248 ) -> Result<StepExecution, LaunchError> {
1249 let now = self.clock.now();
1250 let mut unit = self.repository.begin().await?;
1251 let started_step = unit
1252 .transition_step_execution(
1253 step.id(),
1254 step.version(),
1255 LifecycleTransition::new(BatchStatus::Started, now),
1256 )
1257 .await?;
1258 unit.commit().await?;
1259 self.emit_event(LifecycleEventKind::StepStarted, correlation, None);
1260 Ok(started_step)
1261 }
1262
1263 async fn finish_job(
1264 &self,
1265 job: &JobExecution,
1266 status: BatchStatus,
1267 failure: Option<FailureSummary>,
1268 correlation: &ExecutionCorrelation,
1269 ) -> Result<JobExecution, LaunchError> {
1270 let exit_status = status_exit_status(status);
1271 let now = self.clock.now();
1272 let mut unit = self.repository.begin().await?;
1273 let current = if self.execution_control.is_some() {
1274 unit.get_job_execution(job.id())
1275 .await?
1276 .ok_or(RepositoryError::JobExecutionNotFound { id: job.id() })?
1277 } else {
1278 job.clone()
1279 };
1280 let job = unit
1281 .enrich_job_exit_status(current.id(), current.version(), &exit_status)
1282 .await?;
1283 let transition = transition_for_outcome(status, now, failure)?;
1284 let job = unit
1285 .transition_job_execution(job.id(), job.version(), transition)
1286 .await?;
1287 unit.commit().await?;
1288 self.emit_event(job_event_kind(status), correlation, failure);
1289 Ok(job)
1290 }
1291
1292 async fn finish_step(
1293 &self,
1294 step: &StepExecution,
1295 outcome: TaskletExecutionOutcome,
1296 failure: Option<FailureSummary>,
1297 custom_exit: Option<&ExitStatus>,
1298 terminal_rollback: bool,
1299 correlation: &ExecutionCorrelation,
1300 ) -> Result<StepExecution, LaunchError> {
1301 let (status, default_exit) = final_status(outcome);
1302 let exit_status = custom_exit.unwrap_or(&default_exit);
1303 let now = self.clock.now();
1304 let mut unit = self.repository.begin().await?;
1305 let step = unit
1306 .enrich_step_exit_status(step.id(), step.version(), exit_status)
1307 .await?;
1308 let mut transition = transition_for_outcome(status, now, failure)?;
1309 if terminal_rollback {
1310 transition = transition.with_terminal_rollback();
1311 }
1312 let step = unit
1313 .transition_step_execution(step.id(), step.version(), transition)
1314 .await?;
1315 unit.commit().await?;
1316 self.emit_final_event(outcome, correlation, failure);
1317 Ok(step)
1318 }
1319
1320 async fn stop_graph(
1321 &self,
1322 plan: &CompiledExecutionPlan,
1323 job: &JobExecution,
1324 step: &StepExecution,
1325 correlation: &ExecutionCorrelation,
1326 ) -> Result<(JobExecution, StepExecution), LaunchError> {
1327 let stopping_job = self.mark_job_stopping(job, correlation).await?;
1328 let stopping_step = self.mark_step_stopping(step, correlation).await?;
1329 let outcome = TaskletExecutionOutcome::Stopped(StopTiming::BeforeStart);
1330 let step = self
1331 .finish_step(&stopping_step, outcome, None, None, false, correlation)
1332 .await?;
1333 let status = Self::terminal_status(plan, outcome)?;
1334 let job = self
1335 .finish_job(&stopping_job, status, None, correlation)
1336 .await?;
1337 Ok((job, step))
1338 }
1339
1340 async fn mark_job_stopping(
1341 &self,
1342 job: &JobExecution,
1343 correlation: &ExecutionCorrelation,
1344 ) -> Result<JobExecution, LaunchError> {
1345 let mut unit = self.repository.begin().await?;
1346 let current = if self.execution_control.is_some() {
1347 unit.get_job_execution(job.id())
1348 .await?
1349 .ok_or(RepositoryError::JobExecutionNotFound { id: job.id() })?
1350 } else {
1351 job.clone()
1352 };
1353 if current.metadata().status() == BatchStatus::Stopping {
1354 unit.rollback().await?;
1355 self.emit_event(LifecycleEventKind::JobStopping, correlation, None);
1356 return Ok(current);
1357 }
1358 let job = unit
1359 .transition_job_execution(
1360 current.id(),
1361 current.version(),
1362 LifecycleTransition::new(BatchStatus::Stopping, self.clock.now()),
1363 )
1364 .await?;
1365 unit.commit().await?;
1366 self.emit_event(LifecycleEventKind::JobStopping, correlation, None);
1367 Ok(job)
1368 }
1369
1370 async fn mark_step_stopping(
1371 &self,
1372 step: &StepExecution,
1373 correlation: &ExecutionCorrelation,
1374 ) -> Result<StepExecution, LaunchError> {
1375 let mut unit = self.repository.begin().await?;
1376 let step = unit
1377 .transition_step_execution(
1378 step.id(),
1379 step.version(),
1380 LifecycleTransition::new(BatchStatus::Stopping, self.clock.now()),
1381 )
1382 .await?;
1383 unit.commit().await?;
1384 self.emit_event(LifecycleEventKind::StepStopping, correlation, None);
1385 Ok(step)
1386 }
1387
1388 async fn run_before_job(
1389 &self,
1390 listeners: &[Arc<dyn JobExecutionListener>],
1391 context: ListenerContext<'_>,
1392 ) -> Result<Option<ListenerFailure>, LaunchError> {
1393 for (index, listener) in listeners.iter().enumerate() {
1394 if let Err(kind) = invoke_before_job(listener.as_ref(), context).await {
1395 return self
1396 .listener_failure(ListenerPhase::BeforeJob, index, kind, context)
1397 .map(Some);
1398 }
1399 }
1400 Ok(None)
1401 }
1402
1403 async fn run_before_step(
1404 &self,
1405 listeners: &[Arc<dyn StepExecutionListener>],
1406 context: ListenerContext<'_>,
1407 ) -> Result<Option<ListenerFailure>, LaunchError> {
1408 for (index, listener) in listeners.iter().enumerate() {
1409 if let Err(kind) = invoke_before_step(listener.as_ref(), context).await {
1410 return self
1411 .listener_failure(ListenerPhase::BeforeStep, index, kind, context)
1412 .map(Some);
1413 }
1414 }
1415 Ok(None)
1416 }
1417
1418 async fn run_after_job(
1419 &self,
1420 listeners: &[Arc<dyn JobExecutionListener>],
1421 context: ListenerContext<'_>,
1422 outcome: TaskletExecutionOutcome,
1423 ) -> Result<Vec<ListenerFailure>, LaunchError> {
1424 let mut failures = Vec::new();
1425 for (index, listener) in listeners.iter().enumerate().rev() {
1426 if let Err(kind) = invoke_after_job(listener.as_ref(), context, outcome).await {
1427 failures.push(self.listener_failure(
1428 ListenerPhase::AfterJob,
1429 index,
1430 kind,
1431 context,
1432 )?);
1433 }
1434 }
1435 Ok(failures)
1436 }
1437
1438 async fn run_after_step(
1439 &self,
1440 listeners: &[Arc<dyn StepExecutionListener>],
1441 context: ListenerContext<'_>,
1442 outcome: TaskletExecutionOutcome,
1443 ) -> Result<Vec<ListenerFailure>, LaunchError> {
1444 let mut failures = Vec::new();
1445 for (index, listener) in listeners.iter().enumerate().rev() {
1446 if let Err(kind) = invoke_after_step(listener.as_ref(), context, outcome).await {
1447 failures.push(self.listener_failure(
1448 ListenerPhase::AfterStep,
1449 index,
1450 kind,
1451 context,
1452 )?);
1453 }
1454 }
1455 Ok(failures)
1456 }
1457
1458 fn listener_failure(
1459 &self,
1460 phase: ListenerPhase,
1461 registration_index: usize,
1462 kind: ListenerFailureKind,
1463 context: ListenerContext<'_>,
1464 ) -> Result<ListenerFailure, LaunchError> {
1465 let summary = self.next_failure_summary()?;
1466 let event_kind = match phase {
1467 ListenerPhase::BeforeJob => LifecycleEventKind::JobBeforeListenerFailed,
1468 ListenerPhase::BeforeStep => LifecycleEventKind::StepBeforeListenerFailed,
1469 ListenerPhase::AfterStep => LifecycleEventKind::StepAfterListenerFailed,
1470 ListenerPhase::AfterJob => LifecycleEventKind::JobAfterListenerFailed,
1471 };
1472 self.emit_event(event_kind, context.correlation(), Some(summary));
1473 Ok(ListenerFailure::new(
1474 phase,
1475 registration_index,
1476 kind,
1477 summary,
1478 ))
1479 }
1480
1481 fn next_failure_summary(&self) -> Result<FailureSummary, LaunchError> {
1482 Ok(FailureSummary::new(
1483 FailureCategory::UserComponent,
1484 self.ids
1485 .next_failure_id()
1486 .map_err(RepositoryError::Identifier)?,
1487 ))
1488 }
1489
1490 fn emit_final_event(
1491 &self,
1492 outcome: TaskletExecutionOutcome,
1493 correlation: &ExecutionCorrelation,
1494 failure: Option<FailureSummary>,
1495 ) {
1496 let kind = match outcome {
1497 TaskletExecutionOutcome::Completed => LifecycleEventKind::StepCompleted,
1498 TaskletExecutionOutcome::Stopped(_) => LifecycleEventKind::StepStopped,
1499 TaskletExecutionOutcome::Failed(_) => LifecycleEventKind::StepFailed,
1500 TaskletExecutionOutcome::Unknown => LifecycleEventKind::StepUnknown,
1501 };
1502 self.emit_event(kind, correlation, failure);
1503 }
1504
1505 fn terminal_status(
1512 plan: &CompiledExecutionPlan,
1513 outcome: TaskletExecutionOutcome,
1514 ) -> Result<BatchStatus, LaunchError> {
1515 let (_, exit_status) = final_status(outcome);
1516 Self::terminal_status_for_exit(plan, outcome, &exit_status)
1517 }
1518
1519 fn terminal_status_for_exit(
1520 plan: &CompiledExecutionPlan,
1521 outcome: TaskletExecutionOutcome,
1522 exit_status: &ExitStatus,
1523 ) -> Result<BatchStatus, LaunchError> {
1524 if matches!(outcome, TaskletExecutionOutcome::Unknown) {
1525 return Ok(BatchStatus::Unknown);
1526 }
1527 if plan.manifest_format() == oxide_batch_core::MANIFEST_FORMAT_ONE_STEP
1528 && outcome == TaskletExecutionOutcome::Completed
1529 && exit_status.code().as_str() != "COMPLETED"
1530 {
1531 return Ok(BatchStatus::Completed);
1532 }
1533 match plan.select_target(plan.entry(), exit_status.code())? {
1534 FlowTarget::Terminal(TerminalKind::Complete) => Ok(BatchStatus::Completed),
1535 FlowTarget::Terminal(TerminalKind::Fail) => Ok(BatchStatus::Failed),
1536 FlowTarget::Terminal(TerminalKind::Stop) => Ok(BatchStatus::Stopped),
1537 _ => Err(LaunchError::UnsupportedPlan),
1541 }
1542 }
1543
1544 fn emit_event(
1545 &self,
1546 kind: LifecycleEventKind,
1547 correlation: &ExecutionCorrelation,
1548 failure: Option<FailureSummary>,
1549 ) {
1550 let Some(sink) = self.event_sink else {
1551 return;
1552 };
1553 let event = failure.map_or_else(
1554 || LifecycleEvent::new(kind, correlation.clone()),
1555 |summary| LifecycleEvent::failed(kind, correlation.clone(), summary),
1556 );
1557 let _ = catch_unwind(AssertUnwindSafe(|| sink.emit(&event)));
1558 }
1559}
1560
1561struct CreatedExecutionGraph {
1562 instance: JobInstance,
1563 job_execution: JobExecution,
1564 step_execution: StepExecution,
1565 correlation: ExecutionCorrelation,
1566}
1567
1568const fn job_event_kind(status: BatchStatus) -> LifecycleEventKind {
1569 match status {
1570 BatchStatus::Completed => LifecycleEventKind::JobCompleted,
1571 BatchStatus::Stopped => LifecycleEventKind::JobStopped,
1572 BatchStatus::Unknown => LifecycleEventKind::JobUnknown,
1573 _ => LifecycleEventKind::JobFailed,
1574 }
1575}
1576
1577fn status_exit_status(status: BatchStatus) -> ExitStatus {
1578 match status {
1579 BatchStatus::Completed => ExitStatus::completed(),
1580 BatchStatus::Stopped => ExitStatus::stopped(),
1581 BatchStatus::Unknown => ExitStatus::unknown(),
1582 _ => ExitStatus::failed(),
1583 }
1584}
1585
1586fn final_status(outcome: TaskletExecutionOutcome) -> (BatchStatus, ExitStatus) {
1587 match outcome {
1588 TaskletExecutionOutcome::Completed => (BatchStatus::Completed, ExitStatus::completed()),
1589 TaskletExecutionOutcome::Stopped(_) => (BatchStatus::Stopped, ExitStatus::stopped()),
1590 TaskletExecutionOutcome::Failed(_) => (BatchStatus::Failed, ExitStatus::failed()),
1591 TaskletExecutionOutcome::Unknown => (BatchStatus::Unknown, ExitStatus::unknown()),
1592 }
1593}
1594
1595fn transition_for_outcome(
1596 status: BatchStatus,
1597 transitioned_at: std::time::SystemTime,
1598 failure: Option<FailureSummary>,
1599) -> Result<LifecycleTransition, LaunchError> {
1600 if matches!(status, BatchStatus::Failed) {
1601 let summary = failure.ok_or(RepositoryError::Unavailable)?;
1602 Ok(LifecycleTransition::failed(transitioned_at, summary))
1603 } else {
1604 Ok(LifecycleTransition::new(status, transitioned_at))
1605 }
1606}
1607
1608const fn listener_failure_outcome(kind: ListenerFailureKind) -> TaskletExecutionOutcome {
1609 match kind {
1610 ListenerFailureKind::Error => {
1611 TaskletExecutionOutcome::Failed(TaskletFailure::ListenerError)
1612 }
1613 ListenerFailureKind::Panic => {
1614 TaskletExecutionOutcome::Failed(TaskletFailure::ListenerPanic)
1615 }
1616 }
1617}
1618
1619async fn invoke_before_job(
1620 listener: &dyn JobExecutionListener,
1621 context: ListenerContext<'_>,
1622) -> Result<(), ListenerFailureKind> {
1623 let future = catch_unwind(AssertUnwindSafe(|| listener.before_job(context)))
1624 .map_err(|_| ListenerFailureKind::Panic)?;
1625 match AssertUnwindSafe(future).catch_unwind().await {
1626 Ok(Ok(())) => Ok(()),
1627 Ok(Err(_)) => Err(ListenerFailureKind::Error),
1628 Err(_) => Err(ListenerFailureKind::Panic),
1629 }
1630}
1631
1632async fn invoke_after_job(
1633 listener: &dyn JobExecutionListener,
1634 context: ListenerContext<'_>,
1635 outcome: TaskletExecutionOutcome,
1636) -> Result<(), ListenerFailureKind> {
1637 let future = catch_unwind(AssertUnwindSafe(|| listener.after_job(context, outcome)))
1638 .map_err(|_| ListenerFailureKind::Panic)?;
1639 match AssertUnwindSafe(future).catch_unwind().await {
1640 Ok(Ok(())) => Ok(()),
1641 Ok(Err(_)) => Err(ListenerFailureKind::Error),
1642 Err(_) => Err(ListenerFailureKind::Panic),
1643 }
1644}
1645
1646pub(crate) async fn invoke_before_step(
1647 listener: &dyn StepExecutionListener,
1648 context: ListenerContext<'_>,
1649) -> Result<(), ListenerFailureKind> {
1650 let future = catch_unwind(AssertUnwindSafe(|| listener.before_step(context)))
1651 .map_err(|_| ListenerFailureKind::Panic)?;
1652 match AssertUnwindSafe(future).catch_unwind().await {
1653 Ok(Ok(())) => Ok(()),
1654 Ok(Err(_)) => Err(ListenerFailureKind::Error),
1655 Err(_) => Err(ListenerFailureKind::Panic),
1656 }
1657}
1658
1659pub(crate) async fn invoke_after_step(
1660 listener: &dyn StepExecutionListener,
1661 context: ListenerContext<'_>,
1662 outcome: TaskletExecutionOutcome,
1663) -> Result<(), ListenerFailureKind> {
1664 let future = catch_unwind(AssertUnwindSafe(|| listener.after_step(context, outcome)))
1665 .map_err(|_| ListenerFailureKind::Panic)?;
1666 match AssertUnwindSafe(future).catch_unwind().await {
1667 Ok(Ok(())) => Ok(()),
1668 Ok(Err(_)) => Err(ListenerFailureKind::Error),
1669 Err(_) => Err(ListenerFailureKind::Panic),
1670 }
1671}
1672
1673pub(crate) async fn invoke_tasklet(
1674 tasklet: &dyn Tasklet,
1675 context: TaskletContext<'_>,
1676) -> Result<TaskletOutcome, TaskletFailure> {
1677 let future = catch_unwind(AssertUnwindSafe(|| tasklet.execute(context)))
1678 .map_err(|_| TaskletFailure::Panic)?;
1679 match AssertUnwindSafe(future).catch_unwind().await {
1680 Ok(Ok(outcome)) => Ok(outcome),
1681 Ok(Err(error)) => match error.kind {
1682 TaskletErrorKind::Component => Err(TaskletFailure::Error),
1683 TaskletErrorKind::Panic => Err(TaskletFailure::Panic),
1684 },
1685 Err(_) => Err(TaskletFailure::Panic),
1686 }
1687}
1688
1689pub struct BlockingTaskletAdapter<T> {
1695 tasklet: Arc<T>,
1696 permits: Arc<Semaphore>,
1697}
1698
1699impl<T> BlockingTaskletAdapter<T>
1700where
1701 T: BlockingTasklet,
1702{
1703 #[must_use]
1705 pub fn new(tasklet: T, maximum_concurrency: NonZeroUsize) -> Self {
1706 Self {
1707 tasklet: Arc::new(tasklet),
1708 permits: Arc::new(Semaphore::new(maximum_concurrency.get())),
1709 }
1710 }
1711}
1712
1713impl<T> fmt::Debug for BlockingTaskletAdapter<T> {
1714 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1715 formatter
1716 .debug_struct("BlockingTaskletAdapter")
1717 .field("available_permits", &self.permits.available_permits())
1718 .finish_non_exhaustive()
1719 }
1720}
1721
1722impl<T> Tasklet for BlockingTaskletAdapter<T>
1723where
1724 T: BlockingTasklet,
1725{
1726 fn execute<'a>(
1727 &'a self,
1728 context: TaskletContext<'a>,
1729 ) -> BoxFuture<'a, Result<TaskletOutcome, TaskletError>> {
1730 Box::pin(async move {
1731 if context.stop.is_stop_requested() {
1732 return Ok(TaskletOutcome::Stopped);
1733 }
1734
1735 let permit = tokio::select! {
1736 result = Arc::clone(&self.permits).acquire_owned() => {
1737 match result {
1738 Ok(permit) => permit,
1739 Err(_) => return Err(TaskletError::new()),
1740 }
1741 }
1742 () = context.stop.cancelled() => return Ok(TaskletOutcome::Stopped),
1743 };
1744 if context.stop.is_stop_requested() {
1745 return Ok(TaskletOutcome::Stopped);
1746 }
1747
1748 let stop = context.stop.clone();
1749 let tasklet = Arc::clone(&self.tasklet);
1750 let owned_context = context.into_blocking();
1751 let joined = tokio::task::spawn_blocking(move || {
1752 let _permit = permit;
1753 if owned_context.stop_token().is_stop_requested() {
1754 (false, Ok(TaskletOutcome::Stopped))
1755 } else {
1756 (true, tasklet.execute(owned_context))
1757 }
1758 })
1759 .await;
1760
1761 let (started, result) = match joined {
1762 Ok(result) => result,
1763 Err(error) if error.is_panic() => return Err(TaskletError::panic()),
1764 Err(_) => return Err(TaskletError::new()),
1765 };
1766 let outcome = result?;
1767 if started && stop.is_stop_requested() {
1768 Ok(TaskletOutcome::StoppedAfterBlockingWork)
1769 } else {
1770 Ok(outcome)
1771 }
1772 })
1773 }
1774}