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