Skip to main content

obeli_sk_executor/
executor.rs

1use crate::worker::{FatalError, Worker, WorkerContext, WorkerError, WorkerResult, WorkerResultOk};
2use assert_matches::assert_matches;
3use chrono::{DateTime, Utc};
4use concepts::prefixed_ulid::{DeploymentId, RunId};
5use concepts::storage::{
6    AppendEventsToExecution, AppendRequest, AppendResponseToExecution, DbErrorGeneric,
7    DbErrorWrite, DbExecutor, DbPool, ExecutionLog, LockedExecution,
8};
9use concepts::time::{ClockFn, Sleep};
10use concepts::{
11    ComponentId, ComponentRetryConfig, FunctionMetadata, StrVariant, SupportedFunctionReturnValue,
12};
13use concepts::{ExecutionFailureKind, JoinSetId};
14use concepts::{ExecutionId, FunctionFqn, prefixed_ulid::ExecutorId};
15use concepts::{
16    FinishedExecutionError,
17    storage::{ExecutionRequest, Version},
18};
19use std::{
20    sync::{
21        Arc,
22        atomic::{AtomicBool, Ordering},
23    },
24    time::Duration,
25};
26use tokio::task::{AbortHandle, JoinHandle};
27use tracing::{Instrument, Level, Span, debug, error, info, info_span, instrument, trace, warn};
28
29#[derive(Debug, Clone)]
30pub struct ExecConfig {
31    pub lock_expiry: Duration,
32    pub tick_sleep: Duration,
33    pub batch_size: u32,
34    pub component_id: ComponentId,
35    pub task_limiter: Option<Arc<tokio::sync::Semaphore>>,
36    pub executor_id: ExecutorId,
37    pub retry_config: ComponentRetryConfig,
38    pub locking_strategy: LockingStrategy,
39}
40
41pub struct ExecTask {
42    worker: Arc<dyn Worker>,
43    pub config: ExecConfig,
44    clock_fn: Box<dyn ClockFn>, // Used for obtaining current time when the execution finishes.
45    db_pool: Arc<dyn DbPool>,
46    locking_strategy_holder: LockingStrategyHolder,
47}
48
49#[derive(derive_more::Debug, Default)]
50pub struct ExecutionProgress {
51    #[debug(skip)]
52    #[allow(dead_code)]
53    executions: Vec<(ExecutionId, JoinHandle<()>)>,
54}
55
56impl ExecutionProgress {
57    #[cfg(feature = "test")]
58    pub async fn wait_for_tasks(self) -> Vec<ExecutionId> {
59        let mut vec = Vec::new();
60        for (exe, join_handle) in self.executions {
61            vec.push(exe);
62            join_handle.await.unwrap();
63        }
64        vec
65    }
66}
67
68#[derive(derive_more::Debug)]
69pub struct ExecutorTaskHandle {
70    #[debug(skip)]
71    is_closing: Arc<AtomicBool>,
72    #[debug(skip)]
73    abort_handle: AbortHandle,
74    component_id: ComponentId,
75    executor_id: ExecutorId,
76}
77
78impl ExecutorTaskHandle {
79    #[instrument(level = Level::DEBUG, name = "executor.close", skip_all, fields(executor_id = %self.executor_id, component_id = %self.component_id))]
80    pub async fn close(&self) {
81        trace!("Gracefully closing");
82        // TODO: Close all the workers tasks as well.
83        // Simple solution:
84        // Attempt to unlock all in-progress executions. Activities will not be penalized for shutdown, although the number of retries would increase,
85        // some progress will be lost.
86        // More complex solution:
87        // Activities will be awaited, up to some deadline, then unlocked.
88        // Workflows that are awating should be signaled using `DeadlineTracker`.
89        self.is_closing.store(true, Ordering::Relaxed);
90        while !self.abort_handle.is_finished() {
91            tokio::time::sleep(Duration::from_millis(1)).await;
92        }
93        debug!("Gracefully closed");
94    }
95}
96
97impl Drop for ExecutorTaskHandle {
98    #[instrument(level = Level::DEBUG, name = "executor.drop", skip_all, fields(executor_id = %self.executor_id, component_id = %self.component_id))]
99    fn drop(&mut self) {
100        if self.abort_handle.is_finished() {
101            return;
102        }
103        warn!("Aborting the executor task");
104        self.abort_handle.abort();
105    }
106}
107
108#[cfg(feature = "test")]
109pub fn extract_exported_ffqns_noext_test(worker: &dyn Worker) -> Arc<[FunctionFqn]> {
110    extract_exported_ffqns_noext(worker)
111}
112
113fn extract_exported_ffqns_noext(worker: &dyn Worker) -> Arc<[FunctionFqn]> {
114    worker
115        .exported_functions_noext()
116        .iter()
117        .map(|FunctionMetadata { ffqn, .. }| ffqn.clone())
118        .collect::<Arc<_>>()
119}
120
121#[derive(Debug, Clone, Copy)]
122pub enum LockingStrategy {
123    ByFfqns,
124    ByComponentDigest,
125}
126impl LockingStrategy {
127    fn holder(&self, ffqns: Arc<[FunctionFqn]>) -> LockingStrategyHolder {
128        match self {
129            LockingStrategy::ByFfqns => LockingStrategyHolder::ByFfqns(ffqns),
130            LockingStrategy::ByComponentDigest => LockingStrategyHolder::ByComponentId,
131        }
132    }
133}
134
135enum LockingStrategyHolder {
136    ByFfqns(Arc<[FunctionFqn]>),
137    ByComponentId,
138}
139
140impl ExecTask {
141    #[cfg(feature = "test")]
142    pub fn new_test(
143        config: ExecConfig,
144        worker: Arc<dyn Worker>,
145        clock_fn: Box<dyn ClockFn>,
146        db_pool: Arc<dyn DbPool>,
147        ffqns: Arc<[FunctionFqn]>,
148    ) -> Self {
149        Self {
150            worker,
151            locking_strategy_holder: config.locking_strategy.holder(ffqns),
152            config,
153            clock_fn,
154            db_pool,
155        }
156    }
157
158    #[cfg(feature = "test")]
159    pub fn new_all_ffqns_test(
160        worker: Arc<dyn Worker>,
161        config: ExecConfig,
162        clock_fn: Box<dyn ClockFn>,
163        db_pool: Arc<dyn DbPool>,
164    ) -> Self {
165        let ffqns = extract_exported_ffqns_noext(worker.as_ref());
166        Self {
167            worker,
168            locking_strategy_holder: config.locking_strategy.holder(ffqns),
169            config,
170            clock_fn,
171            db_pool,
172        }
173    }
174
175    pub fn spawn_new(
176        deployment_id: DeploymentId,
177        worker: Arc<dyn Worker>,
178        config: ExecConfig,
179        clock_fn: Box<dyn ClockFn>,
180        db_pool: Arc<dyn DbPool>,
181        sleep: impl Sleep + 'static,
182    ) -> ExecutorTaskHandle {
183        let is_closing = Arc::new(AtomicBool::default());
184        let is_closing_inner = is_closing.clone();
185        let ffqns = extract_exported_ffqns_noext(worker.as_ref());
186        let component_id = config.component_id.clone();
187        let executor_id = config.executor_id;
188        let abort_handle = tokio::spawn(async move {
189            debug!(executor_id = %config.executor_id, component_id = %config.component_id, "Spawned executor");
190            let lock_strategy_holder = config.locking_strategy.holder(ffqns);
191            let task = ExecTask {
192                worker,
193                config,
194                db_pool,
195                locking_strategy_holder: lock_strategy_holder,
196                clock_fn: clock_fn.clone_box(),
197            };
198            let mut old_err = None;
199            while !is_closing_inner.load(Ordering::Relaxed) {
200                let res = task.db_pool.db_exec_conn().await;
201                let res = log_err_if_new(res, &mut old_err);
202                if let Ok(db_exec) = res {
203                    let _ = task.tick(db_exec.as_ref(), clock_fn.now(), RunId::generate(), deployment_id).await;
204                    db_exec
205                        .wait_for_pending_by_component_digest(clock_fn.now(), &task.config.component_id.input_digest, {
206                            let sleep = sleep.clone();
207                            Box::pin(async move { sleep.sleep(task.config.tick_sleep).await })})
208                        .await;
209                } else  {
210                    sleep.sleep(task.config.tick_sleep).await;
211                }
212            }
213        })
214        .abort_handle();
215        ExecutorTaskHandle {
216            is_closing,
217            abort_handle,
218            component_id,
219            executor_id,
220        }
221    }
222
223    fn acquire_task_permits(&self) -> Vec<Option<tokio::sync::OwnedSemaphorePermit>> {
224        if let Some(task_limiter) = &self.config.task_limiter {
225            let mut locks = Vec::new();
226            for _ in 0..self.config.batch_size {
227                if let Ok(permit) = task_limiter.clone().try_acquire_owned() {
228                    locks.push(Some(permit));
229                } else {
230                    break;
231                }
232            }
233            locks
234        } else {
235            let mut vec = Vec::with_capacity(self.config.batch_size as usize);
236            for _ in 0..self.config.batch_size {
237                vec.push(None);
238            }
239            vec
240        }
241    }
242
243    #[cfg(feature = "test")]
244    pub async fn tick_test(&self, executed_at: DateTime<Utc>, run_id: RunId) -> ExecutionProgress {
245        use concepts::prefixed_ulid::DEPLOYMENT_ID_DUMMY;
246
247        let db_exec = self.db_pool.db_exec_conn().await.unwrap();
248        self.tick(db_exec.as_ref(), executed_at, run_id, DEPLOYMENT_ID_DUMMY)
249            .await
250            .unwrap()
251    }
252
253    #[cfg(feature = "test")]
254    pub async fn tick_test_await(
255        &self,
256        executed_at: DateTime<Utc>,
257        run_id: RunId,
258    ) -> Vec<ExecutionId> {
259        use concepts::prefixed_ulid::DEPLOYMENT_ID_DUMMY;
260
261        let db_exec = self.db_pool.db_exec_conn().await.unwrap();
262        self.tick(db_exec.as_ref(), executed_at, run_id, DEPLOYMENT_ID_DUMMY)
263            .await
264            .unwrap()
265            .wait_for_tasks()
266            .await
267    }
268
269    #[instrument(level = Level::TRACE, name = "executor.tick" skip_all, fields(executor_id = %self.config.executor_id, component_id = %self.config.component_id))]
270    async fn tick(
271        &self,
272        db_exec: &dyn DbExecutor,
273        executed_at: DateTime<Utc>,
274        run_id: RunId,
275        deployment_id: DeploymentId,
276    ) -> Result<ExecutionProgress, DbErrorWrite> {
277        let locked_executions = {
278            let mut permits = self.acquire_task_permits();
279            if permits.is_empty() {
280                return Ok(ExecutionProgress::default());
281            }
282            let lock_expires_at = executed_at + self.config.lock_expiry;
283            let batch_size = u32::try_from(permits.len()).expect("ExecConfig.batch_size is u32");
284            let locked_executions = match &self.locking_strategy_holder {
285                LockingStrategyHolder::ByFfqns(ffqns) => {
286                    db_exec
287                        .lock_pending_by_ffqns(
288                            batch_size,
289                            executed_at, // fetch expiring before now
290                            ffqns.clone(),
291                            executed_at, // created at
292                            self.config.component_id.clone(),
293                            deployment_id,
294                            self.config.executor_id,
295                            lock_expires_at,
296                            run_id,
297                            self.config.retry_config,
298                        )
299                        .await?
300                }
301                LockingStrategyHolder::ByComponentId => {
302                    db_exec
303                        .lock_pending_by_component_digest(
304                            batch_size,
305                            executed_at, // pending_at_or_sooner
306                            &self.config.component_id,
307                            deployment_id,
308                            executed_at, // created at
309                            self.config.executor_id,
310                            lock_expires_at,
311                            run_id,
312                            self.config.retry_config,
313                        )
314                        .await?
315                }
316            };
317            // Drop permits if too many were allocated.
318            while permits.len() > locked_executions.len() {
319                permits.pop();
320            }
321            assert_eq!(permits.len(), locked_executions.len());
322            locked_executions.into_iter().zip(permits)
323        };
324
325        let mut executions = Vec::with_capacity(locked_executions.len());
326        for (locked_execution, permit) in locked_executions {
327            let execution_id = locked_execution.execution_id.clone();
328            let join_handle = {
329                let worker = self.worker.clone();
330                let db_pool = self.db_pool.clone();
331                let clock_fn = self.clock_fn.clone_box();
332                let worker_span = info_span!(parent: None, "worker",
333                    "otel.name" = format!("worker {}", locked_execution.ffqn),
334                    %execution_id, %run_id, ffqn = %locked_execution.ffqn, executor_id = %self.config.executor_id, component_id = %self.config.component_id);
335                locked_execution.metadata.enrich(&worker_span);
336                tokio::spawn({
337                    let worker_span2 = worker_span.clone();
338                    let retry_config = self.config.retry_config;
339                    async move {
340                        let _permit = permit;
341                        let res = Self::run_worker(
342                            worker,
343                            db_pool.as_ref(),
344                            clock_fn,
345                            locked_execution,
346                            retry_config,
347                            worker_span2,
348                        )
349                        .await;
350                        if let Err(db_error) = res {
351                            error!("Got db error `{db_error:?}`, expecting watcher to mark execution as timed out");
352                        }
353                    }
354                    .instrument(worker_span)
355                })
356            };
357            executions.push((execution_id, join_handle));
358        }
359        Ok(ExecutionProgress { executions })
360    }
361
362    async fn run_worker(
363        worker: Arc<dyn Worker>,
364        db_pool: &dyn DbPool,
365        clock_fn: Box<dyn ClockFn>,
366        locked_execution: LockedExecution,
367        retry_config: ComponentRetryConfig,
368        worker_span: Span,
369    ) -> Result<(), DbErrorWrite> {
370        debug!("Worker::run starting");
371        trace!(
372            version = %locked_execution.next_version,
373            params = ?locked_execution.params,
374            event_history = ?locked_execution.event_history,
375            "Worker::run starting"
376        );
377        let can_be_retried = ExecutionLog::can_be_retried_after(
378            locked_execution.intermittent_event_count + 1,
379            retry_config.max_retries,
380            retry_config.retry_exp_backoff,
381        );
382        let unlock_expiry_on_limit_reached =
383            ExecutionLog::compute_retry_duration_when_retrying_forever(
384                locked_execution.intermittent_event_count + 1,
385                retry_config.retry_exp_backoff,
386            );
387        let ctx = WorkerContext {
388            execution_id: locked_execution.execution_id.clone(),
389            metadata: locked_execution.metadata,
390            ffqn: locked_execution.ffqn,
391            params: locked_execution.params,
392            event_history: locked_execution.event_history,
393            responses: locked_execution.responses,
394            version: locked_execution.next_version,
395            can_be_retried: can_be_retried.is_some(),
396            locked_event: locked_execution.locked_event,
397            worker_span,
398        };
399        let worker_result = worker.run(ctx).await;
400        trace!(?worker_result, "Worker::run finished");
401        let result_obtained_at = clock_fn.now();
402        match Self::worker_result_to_execution_event(
403            locked_execution.execution_id,
404            worker_result,
405            result_obtained_at,
406            locked_execution.parent,
407            can_be_retried,
408            unlock_expiry_on_limit_reached,
409        )? {
410            Some(append) => {
411                trace!("Appending {append:?}");
412                let db_exec = db_pool.db_exec_conn().await?;
413                match append {
414                    AppendOrCancel::Cancel {
415                        execution_id,
416                        cancelled_at,
417                    } => db_exec
418                        .cancel_activity_with_retries(&execution_id, cancelled_at)
419                        .await
420                        .map(|_| ()),
421                    AppendOrCancel::Other(append) => append.append(db_exec.as_ref()).await,
422                }
423            }
424            None => Ok(()),
425        }
426    }
427
428    /// Map the `WorkerError` to an temporary or a permanent failure.
429    fn worker_result_to_execution_event(
430        execution_id: ExecutionId,
431        worker_result: WorkerResult,
432        result_obtained_at: DateTime<Utc>,
433        parent: Option<(ExecutionId, JoinSetId)>,
434        can_be_retried: Option<Duration>,
435        unlock_expiry_on_limit_reached: Duration,
436    ) -> Result<Option<AppendOrCancel>, DbErrorWrite> {
437        Ok(match worker_result {
438            WorkerResult::Ok(WorkerResultOk::Finished {
439                retval: result,
440                version,
441                http_client_traces,
442            }) => {
443                info!("Execution finished: {result}");
444                let child_finished =
445                    parent.map(
446                        |(parent_execution_id, parent_join_set)| ChildFinishedResponse {
447                            parent_execution_id,
448                            parent_join_set,
449                            result: result.clone(),
450                        },
451                    );
452                let primary_event = AppendRequest {
453                    created_at: result_obtained_at,
454                    event: ExecutionRequest::Finished {
455                        result,
456                        http_client_traces,
457                    },
458                };
459
460                Some(AppendOrCancel::Other(Append {
461                    created_at: result_obtained_at,
462                    primary_event,
463                    execution_id,
464                    version,
465                    child_finished,
466                }))
467            }
468            WorkerResult::Ok(WorkerResultOk::DbUpdatedByWorkerOrWatcher) => None,
469            WorkerResult::Err(err) => {
470                let reason_generic = err.to_string(); // Override with err's reason if no information is lost.
471
472                let (primary_event, child_finished, version) = match err {
473                    WorkerError::TemporaryTimeout {
474                        http_client_traces,
475                        version,
476                    } => {
477                        if let Some(duration) = can_be_retried {
478                            let backoff_expires_at = result_obtained_at + duration;
479                            info!(
480                                "Temporary timeout, retrying after {duration:?} at {backoff_expires_at}"
481                            );
482                            (
483                                ExecutionRequest::TemporarilyTimedOut {
484                                    backoff_expires_at,
485                                    http_client_traces,
486                                },
487                                None,
488                                version,
489                            )
490                        } else {
491                            info!("Execution timed out");
492                            let result = SupportedFunctionReturnValue::ExecutionError(
493                                FinishedExecutionError {
494                                    kind: ExecutionFailureKind::TimedOut,
495                                    reason: None,
496                                    detail: None,
497                                },
498                            );
499                            let child_finished =
500                                parent.map(|(parent_execution_id, parent_join_set)| {
501                                    ChildFinishedResponse {
502                                        parent_execution_id,
503                                        parent_join_set,
504                                        result: result.clone(),
505                                    }
506                                });
507                            (
508                                ExecutionRequest::Finished {
509                                    result,
510                                    http_client_traces,
511                                },
512                                child_finished,
513                                version,
514                            )
515                        }
516                    }
517                    WorkerError::DbError(db_error) => {
518                        return Err(db_error);
519                    }
520                    WorkerError::ActivityTrap {
521                        reason: _, // reason_generic contains trap_kind + reason
522                        trap_kind,
523                        detail,
524                        version,
525                        http_client_traces,
526                    } => {
527                        if let Some(duration) = can_be_retried {
528                            let expires_at = result_obtained_at + duration;
529                            debug!(
530                                "Retrying activity with `{trap_kind}` execution after {duration:?} at {expires_at}"
531                            );
532                            (
533                                ExecutionRequest::TemporarilyFailed {
534                                    reason: StrVariant::from(reason_generic),
535                                    backoff_expires_at: expires_at,
536                                    detail,
537                                    http_client_traces,
538                                },
539                                None,
540                                version,
541                            )
542                        } else {
543                            info!(
544                                "Activity with `{trap_kind}` marked as permanent failure - {reason_generic}"
545                            );
546                            let result = SupportedFunctionReturnValue::ExecutionError(
547                                FinishedExecutionError {
548                                    reason: Some(reason_generic),
549                                    kind: ExecutionFailureKind::Uncategorized,
550                                    detail,
551                                },
552                            );
553                            let child_finished =
554                                parent.map(|(parent_execution_id, parent_join_set)| {
555                                    ChildFinishedResponse {
556                                        parent_execution_id,
557                                        parent_join_set,
558                                        result: result.clone(),
559                                    }
560                                });
561                            (
562                                ExecutionRequest::Finished {
563                                    result,
564                                    http_client_traces,
565                                },
566                                child_finished,
567                                version,
568                            )
569                        }
570                    }
571                    WorkerError::ActivityPreopenedDirError {
572                        reason,
573                        detail,
574                        version,
575                    } => {
576                        let http_client_traces = None;
577                        if let Some(duration) = can_be_retried {
578                            let expires_at = result_obtained_at + duration;
579                            debug!(
580                                "Retrying activity with ActivityPreopenedDirError `{reason}` execution after {duration:?} at {expires_at}"
581                            );
582                            (
583                                ExecutionRequest::TemporarilyFailed {
584                                    reason: StrVariant::from(reason_generic),
585                                    backoff_expires_at: expires_at,
586                                    detail: Some(detail),
587                                    http_client_traces,
588                                },
589                                None,
590                                version,
591                            )
592                        } else {
593                            info!(
594                                "Activity with ActivityPreopenedDirError `{reason}` marked as permanent failure - {reason_generic}"
595                            );
596                            let result = SupportedFunctionReturnValue::ExecutionError(
597                                FinishedExecutionError {
598                                    reason: Some(reason_generic),
599                                    kind: ExecutionFailureKind::Uncategorized,
600                                    detail: Some(detail),
601                                },
602                            );
603                            let child_finished =
604                                parent.map(|(parent_execution_id, parent_join_set)| {
605                                    ChildFinishedResponse {
606                                        parent_execution_id,
607                                        parent_join_set,
608                                        result: result.clone(),
609                                    }
610                                });
611                            (
612                                ExecutionRequest::Finished {
613                                    result,
614                                    http_client_traces,
615                                },
616                                child_finished,
617                                version,
618                            )
619                        }
620                    }
621                    WorkerError::ActivityReturnedError {
622                        detail,
623                        version,
624                        http_client_traces,
625                    } => {
626                        let duration = can_be_retried.expect(
627                            "ActivityReturnedError must not be returned when retries are exhausted",
628                        );
629                        let expires_at = result_obtained_at + duration;
630                        debug!("Retrying ActivityReturnedError after {duration:?} at {expires_at}");
631                        (
632                            ExecutionRequest::TemporarilyFailed {
633                                backoff_expires_at: expires_at,
634                                reason: StrVariant::Static("activity returned error"), // is same as the variant's display message.
635                                detail, // contains the backtrace
636                                http_client_traces,
637                            },
638                            None,
639                            version,
640                        )
641                    }
642                    WorkerError::LimitReached {
643                        reason,
644                        version: new_version,
645                    } => {
646                        let expires_at = result_obtained_at + unlock_expiry_on_limit_reached;
647                        warn!(
648                            "Limit reached: {reason}, unlocking after {unlock_expiry_on_limit_reached:?} at {expires_at}"
649                        );
650                        (
651                            ExecutionRequest::Unlocked {
652                                backoff_expires_at: expires_at,
653                                reason: StrVariant::from(reason),
654                            },
655                            None,
656                            new_version,
657                        )
658                    }
659                    WorkerError::FatalError(FatalError::Cancelled, _version) => {
660                        // use more optimized `cancel` to avoid polluting the logs with errors on a race.
661                        return Ok(Some(AppendOrCancel::Cancel {
662                            execution_id,
663                            cancelled_at: result_obtained_at,
664                        }));
665                    }
666                    WorkerError::FatalError(fatal_error, version) => {
667                        warn!("Fatal worker error - {fatal_error:?}");
668                        let result = SupportedFunctionReturnValue::ExecutionError(
669                            FinishedExecutionError::from(fatal_error),
670                        );
671                        let child_finished =
672                            parent.map(|(parent_execution_id, parent_join_set)| {
673                                ChildFinishedResponse {
674                                    parent_execution_id,
675                                    parent_join_set,
676                                    result: result.clone(),
677                                }
678                            });
679                        (
680                            ExecutionRequest::Finished {
681                                result,
682                                http_client_traces: None,
683                            },
684                            child_finished,
685                            version,
686                        )
687                    }
688                };
689                Some(AppendOrCancel::Other(Append {
690                    created_at: result_obtained_at,
691                    primary_event: AppendRequest {
692                        created_at: result_obtained_at,
693                        event: primary_event,
694                    },
695                    execution_id,
696                    version,
697                    child_finished,
698                }))
699            }
700        })
701    }
702}
703
704#[derive(Debug, Clone)]
705pub(crate) struct ChildFinishedResponse {
706    pub(crate) parent_execution_id: ExecutionId,
707    pub(crate) parent_join_set: JoinSetId,
708    pub(crate) result: SupportedFunctionReturnValue,
709}
710
711#[derive(Debug, Clone)]
712#[expect(clippy::large_enum_variant)]
713pub(crate) enum AppendOrCancel {
714    Cancel {
715        execution_id: ExecutionId,
716        cancelled_at: DateTime<Utc>,
717    },
718    Other(Append),
719}
720
721#[derive(Debug, Clone)]
722pub(crate) struct Append {
723    pub(crate) created_at: DateTime<Utc>,
724    pub(crate) primary_event: AppendRequest,
725    pub(crate) execution_id: ExecutionId,
726    pub(crate) version: Version,
727    pub(crate) child_finished: Option<ChildFinishedResponse>,
728}
729
730impl Append {
731    pub(crate) async fn append(self, db_exec: &dyn DbExecutor) -> Result<(), DbErrorWrite> {
732        if let Some(child_finished) = self.child_finished {
733            assert_matches!(
734                &self.primary_event,
735                AppendRequest {
736                    event: ExecutionRequest::Finished { .. },
737                    ..
738                }
739            );
740            let child_execution_id = assert_matches!(self.execution_id.clone(), ExecutionId::Derived(derived) => derived);
741            let events = AppendEventsToExecution {
742                execution_id: self.execution_id,
743                version: self.version.clone(),
744                batch: vec![self.primary_event],
745            };
746            let response = AppendResponseToExecution {
747                parent_execution_id: child_finished.parent_execution_id,
748                created_at: self.created_at,
749                join_set_id: child_finished.parent_join_set,
750                child_execution_id,
751                finished_version: self.version, // Since self.primary_event is a finished event, the version will remain the same.
752                result: child_finished.result,
753            };
754
755            db_exec
756                .append_batch_respond_to_parent(events, response, self.created_at)
757                .await?;
758        } else {
759            db_exec
760                .append(self.execution_id, self.version, self.primary_event)
761                .await?;
762        }
763        Ok(())
764    }
765}
766
767fn log_err_if_new<T>(
768    res: Result<T, DbErrorGeneric>,
769    old_err: &mut Option<DbErrorGeneric>,
770) -> Result<T, ()> {
771    match (res, &old_err) {
772        (Ok(ok), _) => {
773            *old_err = None;
774            Ok(ok)
775        }
776        (Err(err), Some(old)) if err == *old => Err(()),
777        (Err(err), _) => {
778            warn!("Tick failed: {err:?}");
779            *old_err = Some(err);
780            Err(())
781        }
782    }
783}
784
785#[cfg(any(test, feature = "test"))]
786pub mod simple_worker {
787    use crate::worker::{Worker, WorkerContext, WorkerResult};
788    use async_trait::async_trait;
789    use concepts::{
790        FunctionFqn, FunctionMetadata, ParameterTypes, RETURN_TYPE_DUMMY,
791        storage::{HistoryEvent, Version},
792    };
793    use indexmap::IndexMap;
794    use std::sync::Arc;
795    use tracing::trace;
796
797    pub(crate) const FFQN_SOME: FunctionFqn = FunctionFqn::new_static("ns:pkg/ifc", "fn");
798    pub type SimpleWorkerResultMap =
799        Arc<std::sync::Mutex<IndexMap<Version, (Vec<HistoryEvent>, WorkerResult)>>>;
800
801    #[derive(Clone, Debug)]
802    pub struct SimpleWorker {
803        pub worker_results_rev: SimpleWorkerResultMap,
804        pub ffqn: FunctionFqn,
805        exported: [FunctionMetadata; 1],
806    }
807
808    impl SimpleWorker {
809        #[must_use]
810        pub fn with_single_result(res: WorkerResult) -> Self {
811            Self::with_worker_results_rev(Arc::new(std::sync::Mutex::new(IndexMap::from([(
812                Version::new(2),
813                (vec![], res),
814            )]))))
815        }
816
817        #[must_use]
818        pub fn with_ffqn(self, ffqn: FunctionFqn) -> Self {
819            Self {
820                worker_results_rev: self.worker_results_rev,
821                exported: [FunctionMetadata {
822                    ffqn: ffqn.clone(),
823                    parameter_types: ParameterTypes::default(),
824                    return_type: RETURN_TYPE_DUMMY,
825                    extension: None,
826                    submittable: true,
827                }],
828                ffqn,
829            }
830        }
831
832        #[must_use]
833        pub fn with_worker_results_rev(worker_results_rev: SimpleWorkerResultMap) -> Self {
834            Self {
835                worker_results_rev,
836                ffqn: FFQN_SOME,
837                exported: [FunctionMetadata {
838                    ffqn: FFQN_SOME,
839                    parameter_types: ParameterTypes::default(),
840                    return_type: RETURN_TYPE_DUMMY,
841                    extension: None,
842                    submittable: true,
843                }],
844            }
845        }
846    }
847
848    #[async_trait]
849    impl Worker for SimpleWorker {
850        async fn run(&self, ctx: WorkerContext) -> WorkerResult {
851            let (expected_version, (expected_eh, worker_result)) =
852                self.worker_results_rev.lock().unwrap().pop().unwrap();
853            trace!(%expected_version, version = %ctx.version, ?expected_eh, eh = ?ctx.event_history, "Running SimpleWorker");
854            assert_eq!(expected_version, ctx.version);
855            assert_eq!(
856                expected_eh,
857                ctx.event_history
858                    .iter()
859                    .map(|(event, _version)| event.clone())
860                    .collect::<Vec<_>>()
861            );
862            worker_result
863        }
864
865        fn exported_functions_noext(&self) -> &[FunctionMetadata] {
866            &self.exported
867        }
868    }
869}
870
871#[cfg(test)]
872mod tests {
873    use self::simple_worker::SimpleWorker;
874    use super::*;
875    use crate::{expired_timers_watcher, worker::WorkerResult};
876    use assert_matches::assert_matches;
877    use async_trait::async_trait;
878    use concepts::prefixed_ulid::DEPLOYMENT_ID_DUMMY;
879    use concepts::storage::{
880        CreateRequest, DbConnectionTest, JoinSetRequest, JoinSetResponse, JoinSetResponseEvent,
881    };
882    use concepts::storage::{DbPoolCloseable, LockedBy};
883    use concepts::storage::{
884        ExecutionEvent, ExecutionRequest, HistoryEvent, PendingState, PendingStatePendingAt,
885    };
886    use concepts::time::{ConstClock, Now};
887    use concepts::{
888        FunctionMetadata, JoinSetKind, ParameterTypes, Params, RETURN_TYPE_DUMMY,
889        SUPPORTED_RETURN_VALUE_OK_EMPTY, StrVariant, SupportedFunctionReturnValue, TrapKind,
890    };
891    use db_tests::Database;
892    use indexmap::IndexMap;
893    use rstest::rstest;
894    use simple_worker::FFQN_SOME;
895    use std::{fmt::Debug, future::Future, ops::Deref, sync::Arc};
896    use test_db_macro::expand_enum_database;
897    use test_utils::set_up;
898    use test_utils::sim_clock::SimClock;
899
900    pub(crate) const FFQN_CHILD: FunctionFqn = FunctionFqn::new_static("ns:pkg/ifc", "fn-child");
901
902    async fn tick_fn<W: Worker + Debug>(
903        config: ExecConfig,
904        clock_fn: Box<dyn ClockFn>,
905        db_pool: Arc<dyn DbPool>,
906        worker: Arc<W>,
907        executed_at: DateTime<Utc>,
908    ) -> Vec<ExecutionId> {
909        trace!("Ticking with {worker:?}");
910        let ffqns = super::extract_exported_ffqns_noext(worker.as_ref());
911        let executor = ExecTask::new_test(config, worker, clock_fn, db_pool, ffqns);
912        executor
913            .tick_test_await(executed_at, RunId::generate())
914            .await
915    }
916
917    #[expand_enum_database]
918    #[rstest]
919    #[tokio::test]
920    async fn execute_simple_lifecycle_tick_based(
921        database: Database,
922        #[values(LockingStrategy::ByFfqns, LockingStrategy::ByComponentDigest)]
923        locking_strategy: LockingStrategy,
924    ) {
925        set_up();
926        let created_at = Now.now();
927        let (_guard, db_pool, db_close) = database.set_up().await;
928        let db_connection = db_pool.connection_test().await.unwrap();
929        execute_simple_lifecycle_tick_based_inner(
930            db_connection.as_ref(),
931            db_pool.clone(),
932            Box::new(ConstClock(created_at)),
933            locking_strategy,
934        )
935        .await;
936        drop(db_connection);
937        db_close.close().await;
938    }
939
940    async fn execute_simple_lifecycle_tick_based_inner(
941        db_connection: &dyn DbConnectionTest,
942        db_pool: Arc<dyn DbPool>,
943        clock_fn: Box<dyn ClockFn>,
944        locking_strategy: LockingStrategy,
945    ) {
946        let created_at = clock_fn.now();
947        let exec_config = ExecConfig {
948            batch_size: 1,
949            lock_expiry: Duration::from_secs(1),
950            tick_sleep: Duration::from_millis(100),
951            component_id: ComponentId::dummy_activity(),
952            task_limiter: None,
953            executor_id: ExecutorId::generate(),
954            retry_config: ComponentRetryConfig::ZERO,
955            locking_strategy,
956        };
957
958        let execution_log = create_and_tick(
959            CreateAndTickConfig {
960                execution_id: ExecutionId::generate(),
961                created_at,
962                executed_at: created_at,
963            },
964            clock_fn,
965            db_connection,
966            db_pool,
967            exec_config,
968            Arc::new(SimpleWorker::with_single_result(WorkerResult::Ok(
969                WorkerResultOk::Finished {
970                    retval: SUPPORTED_RETURN_VALUE_OK_EMPTY,
971                    version: Version::new(2),
972                    http_client_traces: None,
973                },
974            ))),
975            tick_fn,
976        )
977        .await;
978        assert_matches!(
979            execution_log.events.get(2).unwrap(),
980            ExecutionEvent {
981                event: ExecutionRequest::Finished {
982                    result: SupportedFunctionReturnValue::Ok { ok: None },
983                    http_client_traces: None
984                },
985                created_at: _,
986                backtrace_id: None,
987                version: Version(2),
988            }
989        );
990    }
991
992    #[rstest]
993    #[tokio::test]
994    async fn execute_simple_lifecycle_task_based_mem(
995        #[values(LockingStrategy::ByFfqns, LockingStrategy::ByComponentDigest)]
996        locking_strategy: LockingStrategy,
997    ) {
998        set_up();
999        let created_at = Now.now();
1000        let clock_fn = Box::new(ConstClock(created_at));
1001        let (_guard, db_pool, db_close) = Database::Memory.set_up().await;
1002        let exec_config = ExecConfig {
1003            batch_size: 1,
1004            lock_expiry: Duration::from_secs(1),
1005            tick_sleep: Duration::ZERO,
1006            component_id: ComponentId::dummy_activity(),
1007            task_limiter: None,
1008            executor_id: ExecutorId::generate(),
1009            retry_config: ComponentRetryConfig::ZERO,
1010            locking_strategy,
1011        };
1012
1013        let worker = Arc::new(SimpleWorker::with_single_result(WorkerResult::Ok(
1014            WorkerResultOk::Finished {
1015                retval: SUPPORTED_RETURN_VALUE_OK_EMPTY,
1016                version: Version::new(2),
1017                http_client_traces: None,
1018            },
1019        )));
1020        let db_connection = db_pool.connection_test().await.unwrap();
1021
1022        let execution_log = create_and_tick(
1023            CreateAndTickConfig {
1024                execution_id: ExecutionId::generate(),
1025                created_at,
1026                executed_at: created_at,
1027            },
1028            clock_fn,
1029            db_connection.as_ref(),
1030            db_pool,
1031            exec_config,
1032            worker,
1033            tick_fn,
1034        )
1035        .await;
1036        assert_matches!(
1037            execution_log.events.get(2).unwrap(),
1038            ExecutionEvent {
1039                event: ExecutionRequest::Finished {
1040                    result: SupportedFunctionReturnValue::Ok { ok: None },
1041                    http_client_traces: None
1042                },
1043                created_at: _,
1044                backtrace_id: None,
1045                version: Version(2),
1046            }
1047        );
1048        db_close.close().await;
1049    }
1050
1051    struct CreateAndTickConfig {
1052        execution_id: ExecutionId,
1053        created_at: DateTime<Utc>,
1054        executed_at: DateTime<Utc>,
1055    }
1056
1057    async fn create_and_tick<
1058        W: Worker,
1059        T: FnMut(ExecConfig, Box<dyn ClockFn>, Arc<dyn DbPool>, Arc<W>, DateTime<Utc>) -> F,
1060        F: Future<Output = Vec<ExecutionId>>,
1061    >(
1062        config: CreateAndTickConfig,
1063        clock_fn: Box<dyn ClockFn>,
1064        db_connection: &dyn DbConnectionTest,
1065        db_pool: Arc<dyn DbPool>,
1066        exec_config: ExecConfig,
1067        worker: Arc<W>,
1068        mut tick: T,
1069    ) -> ExecutionLog {
1070        // Create an execution
1071        db_connection
1072            .create(CreateRequest {
1073                created_at: config.created_at,
1074                execution_id: config.execution_id.clone(),
1075                ffqn: FFQN_SOME,
1076                params: Params::empty(),
1077                parent: None,
1078                metadata: concepts::ExecutionMetadata::empty(),
1079                scheduled_at: config.created_at,
1080                component_id: ComponentId::dummy_activity(),
1081                deployment_id: DEPLOYMENT_ID_DUMMY,
1082                scheduled_by: None,
1083            })
1084            .await
1085            .unwrap();
1086        // execute!
1087        tick(exec_config, clock_fn, db_pool, worker, config.executed_at).await;
1088        let execution_log = db_connection.get(&config.execution_id).await.unwrap();
1089        debug!("Execution history after tick: {execution_log:?}");
1090        // check that DB contains Created and Locked events.
1091        let actually_created_at = assert_matches!(
1092            execution_log.events.first().unwrap(),
1093            ExecutionEvent {
1094                event: ExecutionRequest::Created { .. },
1095                created_at: actually_created_at,
1096                backtrace_id: None,
1097                version: Version(0),
1098            }
1099            => *actually_created_at
1100        );
1101        assert_eq!(config.created_at, actually_created_at);
1102        let locked_at = assert_matches!(
1103            execution_log.events.get(1).unwrap(),
1104            ExecutionEvent {
1105                event: ExecutionRequest::Locked { .. },
1106                created_at: locked_at,
1107                backtrace_id: None,
1108                version: Version(1),
1109            } if config.created_at <= *locked_at
1110            => *locked_at
1111        );
1112        assert_matches!(execution_log.events.get(2).unwrap(), ExecutionEvent {
1113            event: _,
1114            created_at: executed_at,
1115            backtrace_id: None,
1116            version: Version(2),
1117        } if *executed_at >= locked_at);
1118        execution_log
1119    }
1120
1121    #[rstest]
1122    #[tokio::test]
1123    async fn activity_trap_should_trigger_an_execution_retry(
1124        #[values(LockingStrategy::ByFfqns, LockingStrategy::ByComponentDigest)]
1125        locking_strategy: LockingStrategy,
1126    ) {
1127        set_up();
1128        let sim_clock = SimClock::default();
1129        let (_guard, db_pool, db_close) = Database::Memory.set_up().await;
1130        let retry_exp_backoff = Duration::from_millis(100);
1131        let retry_config = ComponentRetryConfig {
1132            max_retries: Some(1),
1133            retry_exp_backoff,
1134        };
1135        let exec_config = ExecConfig {
1136            batch_size: 1,
1137            lock_expiry: Duration::from_secs(1),
1138            tick_sleep: Duration::ZERO,
1139            component_id: ComponentId::dummy_activity(),
1140            task_limiter: None,
1141            executor_id: ExecutorId::generate(),
1142            retry_config,
1143            locking_strategy,
1144        };
1145        let expected_reason = "error reason";
1146        let expected_detail = "error detail";
1147        let worker = Arc::new(SimpleWorker::with_single_result(WorkerResult::Err(
1148            WorkerError::ActivityTrap {
1149                reason: expected_reason.to_string(),
1150                trap_kind: concepts::TrapKind::Trap,
1151                detail: Some(expected_detail.to_string()),
1152                version: Version::new(2),
1153                http_client_traces: None,
1154            },
1155        )));
1156        debug!(now = %sim_clock.now(), "Creating an execution that should fail");
1157        let db_connection = db_pool.connection_test().await.unwrap();
1158        let execution_log = create_and_tick(
1159            CreateAndTickConfig {
1160                execution_id: ExecutionId::generate(),
1161                created_at: sim_clock.now(),
1162                executed_at: sim_clock.now(),
1163            },
1164            sim_clock.clone_box(),
1165            db_connection.as_ref(),
1166            db_pool.clone(),
1167            exec_config.clone(),
1168            worker,
1169            tick_fn,
1170        )
1171        .await;
1172        assert_eq!(3, execution_log.events.len());
1173        {
1174            let (reason, detail, at, expires_at) = assert_matches!(
1175                &execution_log.events.get(2).unwrap(),
1176                ExecutionEvent {
1177                    event: ExecutionRequest::TemporarilyFailed {
1178                        reason,
1179                        detail,
1180                        backoff_expires_at,
1181                        http_client_traces: None,
1182                    },
1183                    created_at: at,
1184                    backtrace_id: None,
1185                    version: Version(2),
1186                }
1187                => (reason, detail, *at, *backoff_expires_at)
1188            );
1189            assert_eq!(format!("activity trap: {expected_reason}"), reason.deref());
1190            assert_eq!(Some(expected_detail), detail.as_deref());
1191            assert_eq!(at, sim_clock.now());
1192            assert_eq!(sim_clock.now() + retry_config.retry_exp_backoff, expires_at);
1193        }
1194        let worker = Arc::new(SimpleWorker::with_worker_results_rev(Arc::new(
1195            std::sync::Mutex::new(IndexMap::from([(
1196                Version::new(4),
1197                (
1198                    vec![],
1199                    WorkerResult::Ok(WorkerResultOk::Finished {
1200                        retval: SUPPORTED_RETURN_VALUE_OK_EMPTY,
1201                        version: Version::new(4),
1202                        http_client_traces: None,
1203                    }),
1204                ),
1205            )])),
1206        )));
1207        // noop until `retry_exp_backoff` expires
1208        assert!(
1209            tick_fn(
1210                exec_config.clone(),
1211                sim_clock.clone_box(),
1212                db_pool.clone(),
1213                worker.clone(),
1214                sim_clock.now(),
1215            )
1216            .await
1217            .is_empty()
1218        );
1219        // tick again to finish the execution
1220        sim_clock.move_time_forward(retry_config.retry_exp_backoff);
1221        tick_fn(
1222            exec_config,
1223            sim_clock.clone_box(),
1224            db_pool.clone(),
1225            worker,
1226            sim_clock.now(),
1227        )
1228        .await;
1229        let execution_log = {
1230            let db_connection = db_pool.connection_test().await.unwrap();
1231            db_connection
1232                .get(&execution_log.execution_id)
1233                .await
1234                .unwrap()
1235        };
1236        debug!(now = %sim_clock.now(), "Execution history after second tick: {execution_log:?}");
1237        assert_matches!(
1238            execution_log.events.get(3).unwrap(),
1239            ExecutionEvent {
1240                event: ExecutionRequest::Locked { .. },
1241                created_at: at,
1242                backtrace_id: None,
1243                version: Version(3),
1244            } if *at == sim_clock.now()
1245        );
1246        assert_matches!(
1247            execution_log.events.get(4).unwrap(),
1248            ExecutionEvent {
1249                event: ExecutionRequest::Finished {
1250                    result: SupportedFunctionReturnValue::Ok{ok:None},
1251                    http_client_traces: None
1252                },
1253                created_at: finished_at,
1254                backtrace_id: None,
1255                version: Version(4),
1256            } if *finished_at == sim_clock.now()
1257        );
1258        db_close.close().await;
1259    }
1260
1261    #[rstest]
1262    #[tokio::test]
1263    async fn activity_trap_should_not_be_retried_if_no_retries_are_set(
1264        #[values(LockingStrategy::ByFfqns, LockingStrategy::ByComponentDigest)]
1265        locking_strategy: LockingStrategy,
1266    ) {
1267        set_up();
1268        let created_at = Now.now();
1269        let clock_fn = Box::new(ConstClock(created_at));
1270        let (_guard, db_pool, db_close) = Database::Memory.set_up().await;
1271        let exec_config = ExecConfig {
1272            batch_size: 1,
1273            lock_expiry: Duration::from_secs(1),
1274            tick_sleep: Duration::ZERO,
1275            component_id: ComponentId::dummy_activity(),
1276            task_limiter: None,
1277            executor_id: ExecutorId::generate(),
1278            retry_config: ComponentRetryConfig::ZERO,
1279            locking_strategy,
1280        };
1281
1282        let reason = "error reason";
1283        let expected_reason = format!("activity trap: {reason}");
1284        let expected_detail = "error detail";
1285        let worker = Arc::new(SimpleWorker::with_single_result(WorkerResult::Err(
1286            WorkerError::ActivityTrap {
1287                reason: reason.to_string(),
1288                trap_kind: concepts::TrapKind::Trap,
1289                detail: Some(expected_detail.to_string()),
1290                version: Version::new(2),
1291                http_client_traces: None,
1292            },
1293        )));
1294        let execution_log = create_and_tick(
1295            CreateAndTickConfig {
1296                execution_id: ExecutionId::generate(),
1297                created_at,
1298                executed_at: created_at,
1299            },
1300            clock_fn,
1301            db_pool.connection_test().await.unwrap().as_ref(),
1302            db_pool.clone(),
1303            exec_config.clone(),
1304            worker,
1305            tick_fn,
1306        )
1307        .await;
1308        assert_eq!(3, execution_log.events.len());
1309        let (reason, kind, detail) = assert_matches!(
1310            &execution_log.events.get(2).unwrap(),
1311            ExecutionEvent {
1312                event: ExecutionRequest::Finished{
1313                    result: SupportedFunctionReturnValue::ExecutionError(FinishedExecutionError{reason, kind, detail}),
1314                    http_client_traces: None
1315                },
1316                created_at: at,
1317                backtrace_id: None,
1318                version: Version(2),
1319            } if *at == created_at
1320            => (reason, kind, detail)
1321        );
1322
1323        assert_eq!(Some(expected_reason), *reason);
1324        assert_eq!(Some(expected_detail), detail.as_deref());
1325        assert_eq!(ExecutionFailureKind::Uncategorized, *kind);
1326
1327        db_close.close().await;
1328    }
1329
1330    #[rstest]
1331    #[tokio::test]
1332    async fn child_execution_permanently_failed_should_notify_parent_permanent_failure(
1333        #[values(LockingStrategy::ByFfqns, LockingStrategy::ByComponentDigest)]
1334        locking_strategy: LockingStrategy,
1335    ) {
1336        let worker_error = WorkerError::ActivityTrap {
1337            reason: "error reason".to_string(),
1338            trap_kind: TrapKind::Trap,
1339            detail: Some("detail".to_string()),
1340            version: Version::new(2),
1341            http_client_traces: None,
1342        };
1343        let expected_child_err = FinishedExecutionError {
1344            kind: ExecutionFailureKind::Uncategorized,
1345            reason: Some("activity trap: error reason".to_string()),
1346            detail: Some("detail".to_string()),
1347        };
1348        child_execution_permanently_failed_should_notify_parent(
1349            WorkerResult::Err(worker_error),
1350            expected_child_err,
1351            locking_strategy,
1352        )
1353        .await;
1354    }
1355
1356    #[rstest]
1357    #[tokio::test]
1358    async fn child_execution_permanently_failed_handled_by_watcher_should_notify_parent_timeout(
1359        #[values(LockingStrategy::ByFfqns, LockingStrategy::ByComponentDigest)]
1360        locking_strategy: LockingStrategy,
1361    ) {
1362        let expected_child_err = FinishedExecutionError {
1363            kind: ExecutionFailureKind::TimedOut,
1364            reason: None,
1365            detail: None,
1366        };
1367        child_execution_permanently_failed_should_notify_parent(
1368            WorkerResult::Ok(WorkerResultOk::DbUpdatedByWorkerOrWatcher),
1369            expected_child_err,
1370            locking_strategy,
1371        )
1372        .await;
1373    }
1374
1375    async fn child_execution_permanently_failed_should_notify_parent(
1376        worker_result: WorkerResult,
1377        expected_child_err: FinishedExecutionError,
1378        locking_strategy: LockingStrategy,
1379    ) {
1380        use concepts::storage::JoinSetResponseEventOuter;
1381        const LOCK_EXPIRY: Duration = Duration::from_secs(1);
1382
1383        set_up();
1384        let sim_clock = SimClock::default();
1385        let (_guard, db_pool, db_close) = Database::Memory.set_up().await;
1386
1387        let parent_worker = Arc::new(SimpleWorker::with_single_result(WorkerResult::Ok(
1388            WorkerResultOk::DbUpdatedByWorkerOrWatcher,
1389        )));
1390        let parent_execution_id = ExecutionId::generate();
1391        db_pool
1392            .connection()
1393            .await
1394            .unwrap()
1395            .create(CreateRequest {
1396                created_at: sim_clock.now(),
1397                execution_id: parent_execution_id.clone(),
1398                ffqn: FFQN_SOME,
1399                params: Params::empty(),
1400                parent: None,
1401                metadata: concepts::ExecutionMetadata::empty(),
1402                scheduled_at: sim_clock.now(),
1403                component_id: ComponentId::dummy_activity(),
1404                deployment_id: DEPLOYMENT_ID_DUMMY,
1405                scheduled_by: None,
1406            })
1407            .await
1408            .unwrap();
1409        let parent_executor_id = ExecutorId::generate();
1410        tick_fn(
1411            ExecConfig {
1412                batch_size: 1,
1413                lock_expiry: LOCK_EXPIRY,
1414                tick_sleep: Duration::ZERO,
1415                component_id: ComponentId::dummy_activity(),
1416                task_limiter: None,
1417                executor_id: parent_executor_id,
1418                retry_config: ComponentRetryConfig::ZERO,
1419                locking_strategy,
1420            },
1421            sim_clock.clone_box(),
1422            db_pool.clone(),
1423            parent_worker,
1424            sim_clock.now(),
1425        )
1426        .await;
1427
1428        let join_set_id = JoinSetId::new(JoinSetKind::OneOff, StrVariant::empty()).unwrap();
1429        let child_execution_id = parent_execution_id.next_level(&join_set_id);
1430        // executor does not append anything, this should have been written by the worker:
1431        {
1432            let params = Params::empty();
1433            let child = CreateRequest {
1434                created_at: sim_clock.now(),
1435                execution_id: ExecutionId::Derived(child_execution_id.clone()),
1436                ffqn: FFQN_CHILD,
1437                params: params.clone(),
1438                parent: Some((parent_execution_id.clone(), join_set_id.clone())),
1439                metadata: concepts::ExecutionMetadata::empty(),
1440                scheduled_at: sim_clock.now(),
1441                component_id: ComponentId::dummy_activity(),
1442                deployment_id: DEPLOYMENT_ID_DUMMY,
1443                scheduled_by: None,
1444            };
1445            let current_time = sim_clock.now();
1446            let join_set = AppendRequest {
1447                created_at: current_time,
1448                event: ExecutionRequest::HistoryEvent {
1449                    event: HistoryEvent::JoinSetCreate {
1450                        join_set_id: join_set_id.clone(),
1451                    },
1452                },
1453            };
1454            let child_exec_req = AppendRequest {
1455                created_at: current_time,
1456                event: ExecutionRequest::HistoryEvent {
1457                    event: HistoryEvent::JoinSetRequest {
1458                        join_set_id: join_set_id.clone(),
1459                        request: JoinSetRequest::ChildExecutionRequest {
1460                            child_execution_id: child_execution_id.clone(),
1461                            target_ffqn: FFQN_CHILD,
1462                            params,
1463                        },
1464                    },
1465                },
1466            };
1467            let join_next = AppendRequest {
1468                created_at: current_time,
1469                event: ExecutionRequest::HistoryEvent {
1470                    event: HistoryEvent::JoinNext {
1471                        join_set_id: join_set_id.clone(),
1472                        run_expires_at: sim_clock.now(),
1473                        closing: false,
1474                        requested_ffqn: Some(FFQN_CHILD),
1475                    },
1476                },
1477            };
1478            db_pool
1479                .connection()
1480                .await
1481                .unwrap()
1482                .append_batch_create_new_execution(
1483                    current_time,
1484                    vec![join_set, child_exec_req, join_next],
1485                    parent_execution_id.clone(),
1486                    Version::new(2),
1487                    vec![child],
1488                    vec![],
1489                )
1490                .await
1491                .unwrap();
1492        }
1493
1494        let child_worker =
1495            Arc::new(SimpleWorker::with_single_result(worker_result).with_ffqn(FFQN_CHILD));
1496
1497        // execute the child
1498        tick_fn(
1499            ExecConfig {
1500                batch_size: 1,
1501                lock_expiry: LOCK_EXPIRY,
1502                tick_sleep: Duration::ZERO,
1503                component_id: ComponentId::dummy_activity(),
1504                task_limiter: None,
1505                executor_id: ExecutorId::generate(),
1506                retry_config: ComponentRetryConfig::ZERO,
1507                locking_strategy,
1508            },
1509            sim_clock.clone_box(),
1510            db_pool.clone(),
1511            child_worker,
1512            sim_clock.now(),
1513        )
1514        .await;
1515        if matches!(expected_child_err.kind, ExecutionFailureKind::TimedOut) {
1516            // In case of timeout, let the timers watcher handle it
1517            sim_clock.move_time_forward(LOCK_EXPIRY);
1518            expired_timers_watcher::tick(
1519                db_pool.connection().await.unwrap().as_ref(),
1520                sim_clock.now(),
1521            )
1522            .await
1523            .unwrap();
1524        }
1525        let child_log = db_pool
1526            .connection_test()
1527            .await
1528            .unwrap()
1529            .get(&ExecutionId::Derived(child_execution_id.clone()))
1530            .await
1531            .unwrap();
1532        assert!(child_log.pending_state.is_finished());
1533        assert_eq!(
1534            Version(2),
1535            child_log.next_version,
1536            "created = 0, locked = 1, with_single_result = 2"
1537        );
1538        assert_eq!(
1539            ExecutionRequest::Finished {
1540                result: SupportedFunctionReturnValue::ExecutionError(expected_child_err),
1541                http_client_traces: None
1542            },
1543            child_log.last_event().event
1544        );
1545        let parent_log = db_pool
1546            .connection_test()
1547            .await
1548            .unwrap()
1549            .get(&parent_execution_id)
1550            .await
1551            .unwrap();
1552        assert_matches!(
1553            parent_log.pending_state,
1554            PendingState::PendingAt(PendingStatePendingAt {
1555                scheduled_at,
1556                last_lock: Some(LockedBy { executor_id: found_executor_id, run_id: _}),
1557            }) if scheduled_at == sim_clock.now() && found_executor_id == parent_executor_id,
1558            "parent should be back to pending"
1559        );
1560        let (found_join_set_id, found_child_execution_id, child_finished_version, found_result) = assert_matches!(
1561            parent_log.responses.last().map(|resp| &resp.event),
1562            Some(JoinSetResponseEventOuter{
1563                created_at: at,
1564                event: JoinSetResponseEvent{
1565                    join_set_id: found_join_set_id,
1566                    event: JoinSetResponse::ChildExecutionFinished {
1567                        child_execution_id: found_child_execution_id,
1568                        finished_version,
1569                        result: found_result,
1570                    }
1571                }
1572            })
1573             if *at == sim_clock.now()
1574            => (found_join_set_id, found_child_execution_id, finished_version, found_result)
1575        );
1576        assert_eq!(join_set_id, *found_join_set_id);
1577        assert_eq!(child_execution_id, *found_child_execution_id);
1578        assert_eq!(child_log.next_version, *child_finished_version);
1579        assert_matches!(
1580            found_result,
1581            SupportedFunctionReturnValue::ExecutionError(_)
1582        );
1583
1584        db_close.close().await;
1585    }
1586
1587    #[derive(Clone, Debug)]
1588    struct SleepyWorker {
1589        duration: Duration,
1590        result: SupportedFunctionReturnValue,
1591        exported: [FunctionMetadata; 1],
1592    }
1593
1594    #[async_trait]
1595    impl Worker for SleepyWorker {
1596        async fn run(&self, ctx: WorkerContext) -> WorkerResult {
1597            tokio::time::sleep(self.duration).await;
1598            WorkerResult::Ok(WorkerResultOk::Finished {
1599                retval: self.result.clone(),
1600                version: ctx.version,
1601                http_client_traces: None,
1602            })
1603        }
1604
1605        fn exported_functions_noext(&self) -> &[FunctionMetadata] {
1606            &self.exported
1607        }
1608    }
1609
1610    #[rstest]
1611    #[tokio::test]
1612    async fn hanging_lock_should_be_cleaned_and_execution_retried(
1613        #[values(LockingStrategy::ByFfqns, LockingStrategy::ByComponentDigest)]
1614        locking_strategy: LockingStrategy,
1615    ) {
1616        set_up();
1617        let sim_clock = SimClock::default();
1618        let (_guard, db_pool, db_close) = Database::Memory.set_up().await;
1619        let lock_expiry = Duration::from_millis(100);
1620        let timeout_duration = Duration::from_millis(300);
1621        let retry_config = ComponentRetryConfig {
1622            max_retries: Some(1),
1623            retry_exp_backoff: timeout_duration,
1624        };
1625        let exec_config = ExecConfig {
1626            batch_size: 1,
1627            lock_expiry,
1628            tick_sleep: Duration::ZERO,
1629            component_id: ComponentId::dummy_activity(),
1630            task_limiter: None,
1631            executor_id: ExecutorId::generate(),
1632            retry_config,
1633            locking_strategy,
1634        };
1635
1636        let worker = Arc::new(SleepyWorker {
1637            duration: lock_expiry + Duration::from_millis(1), // sleep more than allowed by the lock expiry
1638            result: SUPPORTED_RETURN_VALUE_OK_EMPTY,
1639            exported: [FunctionMetadata {
1640                ffqn: FFQN_SOME,
1641                parameter_types: ParameterTypes::default(),
1642                return_type: RETURN_TYPE_DUMMY,
1643                extension: None,
1644                submittable: true,
1645            }],
1646        });
1647        // Create an execution
1648        let execution_id = ExecutionId::generate();
1649        let db_connection = db_pool.connection_test().await.unwrap();
1650        db_connection
1651            .create(CreateRequest {
1652                created_at: sim_clock.now(),
1653                execution_id: execution_id.clone(),
1654                ffqn: FFQN_SOME,
1655                params: Params::empty(),
1656                parent: None,
1657                metadata: concepts::ExecutionMetadata::empty(),
1658                scheduled_at: sim_clock.now(),
1659                component_id: ComponentId::dummy_activity(),
1660                deployment_id: DEPLOYMENT_ID_DUMMY,
1661                scheduled_by: None,
1662            })
1663            .await
1664            .unwrap();
1665
1666        let ffqns = super::extract_exported_ffqns_noext(worker.as_ref());
1667        let executor = ExecTask::new_test(
1668            exec_config.clone(),
1669            worker,
1670            sim_clock.clone_box(),
1671            db_pool.clone(),
1672            ffqns,
1673        );
1674        let db_exec = db_pool.db_exec_conn().await.unwrap();
1675        let mut first_execution_progress = executor
1676            .tick(
1677                db_exec.as_ref(),
1678                sim_clock.now(),
1679                RunId::generate(),
1680                DEPLOYMENT_ID_DUMMY,
1681            )
1682            .await
1683            .unwrap();
1684        assert_eq!(1, first_execution_progress.executions.len());
1685        // Started hanging, wait for lock expiry.
1686        sim_clock.move_time_forward(lock_expiry);
1687        // cleanup should be called
1688        let now_after_first_lock_expiry = sim_clock.now();
1689        {
1690            debug!(now = %now_after_first_lock_expiry, "Expecting an expired lock");
1691            let cleanup_progress = executor
1692                .tick(
1693                    db_pool.db_exec_conn().await.unwrap().as_ref(),
1694                    now_after_first_lock_expiry,
1695                    RunId::generate(),
1696                    DEPLOYMENT_ID_DUMMY,
1697                )
1698                .await
1699                .unwrap();
1700            assert!(cleanup_progress.executions.is_empty());
1701        }
1702        {
1703            let expired_locks = expired_timers_watcher::tick(
1704                db_pool.connection().await.unwrap().as_ref(),
1705                now_after_first_lock_expiry,
1706            )
1707            .await
1708            .unwrap()
1709            .expired_locks;
1710            assert_eq!(1, expired_locks);
1711        }
1712        assert!(
1713            !first_execution_progress
1714                .executions
1715                .pop()
1716                .unwrap()
1717                .1
1718                .is_finished()
1719        );
1720
1721        let execution_log = db_connection.get(&execution_id).await.unwrap();
1722        let expected_first_timeout_expiry = now_after_first_lock_expiry + timeout_duration;
1723        assert_matches!(
1724            &execution_log.events.get(2).unwrap(),
1725            ExecutionEvent {
1726                event: ExecutionRequest::TemporarilyTimedOut { backoff_expires_at, .. },
1727                created_at: at,
1728                backtrace_id: None,
1729                version: Version(2),
1730            } if *at == now_after_first_lock_expiry && *backoff_expires_at == expected_first_timeout_expiry
1731        );
1732        assert_matches!(
1733            execution_log.pending_state,
1734            PendingState::PendingAt(PendingStatePendingAt {
1735                scheduled_at: found_scheduled_by,
1736                last_lock: Some(LockedBy {
1737                    executor_id: found_executor_id,
1738                    run_id: _,
1739                }),
1740            }) if found_scheduled_by == expected_first_timeout_expiry && found_executor_id == exec_config.executor_id
1741        );
1742        sim_clock.move_time_forward(timeout_duration);
1743        let now_after_first_timeout = sim_clock.now();
1744        debug!(now = %now_after_first_timeout, "Second execution should hang again and result in a permanent timeout");
1745
1746        let mut second_execution_progress = executor
1747            .tick(
1748                db_pool.db_exec_conn().await.unwrap().as_ref(),
1749                now_after_first_timeout,
1750                RunId::generate(),
1751                DEPLOYMENT_ID_DUMMY,
1752            )
1753            .await
1754            .unwrap();
1755        assert_eq!(1, second_execution_progress.executions.len());
1756
1757        // Started hanging, wait for lock expiry.
1758        sim_clock.move_time_forward(lock_expiry);
1759        // cleanup should be called
1760        let now_after_second_lock_expiry = sim_clock.now();
1761        debug!(now = %now_after_second_lock_expiry, "Expecting the second lock to be expired");
1762        {
1763            let cleanup_progress = executor
1764                .tick(
1765                    db_pool.db_exec_conn().await.unwrap().as_ref(),
1766                    now_after_second_lock_expiry,
1767                    RunId::generate(),
1768                    DEPLOYMENT_ID_DUMMY,
1769                )
1770                .await
1771                .unwrap();
1772            assert!(cleanup_progress.executions.is_empty());
1773        }
1774        {
1775            let expired_locks = expired_timers_watcher::tick(
1776                db_pool.connection().await.unwrap().as_ref(),
1777                now_after_second_lock_expiry,
1778            )
1779            .await
1780            .unwrap()
1781            .expired_locks;
1782            assert_eq!(1, expired_locks);
1783        }
1784        assert!(
1785            !second_execution_progress
1786                .executions
1787                .pop()
1788                .unwrap()
1789                .1
1790                .is_finished()
1791        );
1792
1793        drop(db_connection);
1794        drop(executor);
1795        db_close.close().await;
1796    }
1797}