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