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