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>, 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 #[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 execution_interrupt_sender: tokio::sync::watch::Sender<bool>,
94 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 execution_interrupt_sender: tokio::sync::watch::Sender<bool>,
104 worker_count_rx: tokio::sync::watch::Receiver<usize>,
106}
107
108impl ExecutorTaskHandle {
109 #[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 #[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)] struct 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 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, ffqns.clone(),
470 executed_at, 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, ffqns.clone(),
486 executed_at, 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, &self.config.component_id,
502 deployment_id,
503 executed_at, self.config.executor_id,
505 lock_expires_at,
506 run_id,
507 self.config.retry_config,
508 )
509 .await?
510 }
511 };
512 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 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 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(); 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: _, 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, 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 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 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 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 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 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 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 {
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 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 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), 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 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 sim_clock.move_time_forward(lock_expiry);
1951 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 sim_clock.move_time_forward(lock_expiry);
2023 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}