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