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