Skip to main content

obeli_sk_executor/
executor.rs

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