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