Skip to main content

obeli_sk_executor/
executor.rs

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