1use std::ops::Add;
16use std::sync::Arc;
17use std::time::Duration;
18
19use backon::{BackoffBuilder, ExponentialBuilder};
20use common_error::ext::PlainError;
21use common_error::status_code::StatusCode;
22use common_event_recorder::EventRecorderRef;
23use common_telemetry::tracing::warn;
24use common_telemetry::tracing_context::{FutureExt, TracingContext};
25use common_telemetry::{debug, error, info, tracing};
26use rand::Rng;
27use snafu::ResultExt;
28use tokio::time;
29
30use crate::error::{self, ProcedurePanicSnafu, Result, RollbackTimesExceededSnafu};
31use crate::event::ProcedureEvent;
32use crate::local::{ManagerContext, ProcedureMeta, ProcedureMetaRef};
33use crate::procedure::{Output, StringKey};
34use crate::rwlock::OwnedKeyRwLockGuard;
35use crate::store::{ProcedureMessage, ProcedureStore};
36use crate::{
37 BoxedProcedure, ChildSubmissionOutcome, Context, Error, EventContext, EventTrigger, Procedure,
38 ProcedureId, ProcedureState, ProcedureWithId, RetryPhase, Status,
39};
40
41struct ProcedureGuard {
43 meta: ProcedureMetaRef,
44 manager_ctx: Arc<ManagerContext>,
45 key_guards: Vec<OwnedKeyRwLockGuard>,
46 finish: bool,
47}
48
49impl ProcedureGuard {
50 fn new(meta: ProcedureMetaRef, manager_ctx: Arc<ManagerContext>) -> ProcedureGuard {
52 ProcedureGuard {
53 meta,
54 manager_ctx,
55 key_guards: vec![],
56 finish: false,
57 }
58 }
59
60 fn finish(mut self) {
62 self.finish = true;
63 }
64}
65
66impl Drop for ProcedureGuard {
67 fn drop(&mut self) {
68 if !self.finish {
69 error!("Procedure {} exits unexpectedly", self.meta.id);
70
71 let err = ProcedurePanicSnafu {
75 procedure_id: self.meta.id,
76 }
77 .build();
78 self.meta.set_state(ProcedureState::failed(Arc::new(err)));
79 }
80
81 if let Some(parent_id) = self.meta.parent_id {
83 self.manager_ctx.notify_by_subprocedure(parent_id);
84 }
85
86 while !self.key_guards.is_empty() {
88 self.key_guards.pop();
89 }
90
91 self.manager_ctx
93 .key_lock
94 .clean_keys(self.meta.lock_key.keys_to_lock().map(|k| k.as_string()));
95 }
96}
97
98fn find_lock_conflicts<'a>(
104 parent_keys: impl Iterator<Item = &'a StringKey>,
105 child_keys: impl Iterator<Item = &'a StringKey>,
106) -> Vec<String> {
107 use std::collections::HashMap;
108
109 let mut parent_map = HashMap::new();
111 for key in parent_keys {
112 match key {
113 StringKey::Exclusive(k) => {
114 parent_map.insert(k.as_str(), true);
115 }
116 StringKey::Share(k) => {
117 parent_map.entry(k.as_str()).or_insert(false);
118 }
119 }
120 }
121
122 child_keys
123 .filter_map(|child_key| match child_key {
124 StringKey::Exclusive(k) | StringKey::Share(k)
125 if parent_map.get(k.as_str()) == Some(&true) =>
126 {
127 Some(k.clone())
128 }
129 StringKey::Exclusive(k) if parent_map.get(k.as_str()) == Some(&false) => {
130 Some(k.clone())
131 }
132 _ => None,
133 })
134 .collect()
135}
136
137pub(crate) struct Runner {
138 pub(crate) meta: ProcedureMetaRef,
139 pub(crate) procedure: BoxedProcedure,
140 pub(crate) manager_ctx: Arc<ManagerContext>,
141 pub(crate) step: u32,
142 pub(crate) exponential_builder: ExponentialBuilder,
143 pub(crate) store: Arc<ProcedureStore>,
144 pub(crate) rolling_back: bool,
145 pub(crate) event_recorder: Option<EventRecorderRef>,
146 pub(crate) execute_retry_attempt: u32,
147 pub(crate) rollback_retry_attempt: u32,
148}
149
150impl Runner {
151 pub(crate) fn running(&self) -> bool {
153 self.manager_ctx.running()
154 }
155
156 pub(crate) async fn run(mut self) {
158 let mut guard = ProcedureGuard::new(self.meta.clone(), self.manager_ctx.clone());
160
161 info!(
162 "Runner {}-{} starts",
163 self.procedure.type_name(),
164 self.meta.id
165 );
166
167 for key in self.meta.lock_key.keys_to_lock() {
170 let key_guard = match key {
172 StringKey::Share(key) => self.manager_ctx.key_lock.read(key.clone()).await.into(),
173 StringKey::Exclusive(key) => {
174 self.manager_ctx.key_lock.write(key.clone()).await.into()
175 }
176 };
177
178 guard.key_guards.push(key_guard);
179 }
180
181 self.meta.set_start_time_ms();
184 self.execute_procedure_in_loop().await;
185 self.meta.set_end_time_ms();
186
187 guard.finish();
194
195 if self.meta.parent_id.is_none() {
197 let procedure_ids = self.manager_ctx.procedures_in_tree(&self.meta);
198 self.manager_ctx.on_procedures_finish(&procedure_ids);
200
201 if !self.running() {
203 return;
204 }
205
206 for id in procedure_ids {
207 if let Err(e) = self.store.delete_procedure(id).await {
208 error!(
209 e;
210 "Runner {}-{} failed to delete procedure {}",
211 self.procedure.type_name(),
212 self.meta.id,
213 id,
214 );
215 }
216 }
217 }
218
219 info!(
220 "Runner {}-{} exits",
221 self.procedure.type_name(),
222 self.meta.id
223 );
224 }
225
226 async fn execute_procedure_in_loop(&mut self) {
227 let ctx = Context {
228 procedure_id: self.meta.id,
229 provider: self.manager_ctx.clone(),
230 event_context: self.meta.context.event_context.clone(),
231 };
232
233 self.rolling_back = false;
234 self.execute_once_with_retry(&ctx).await;
235 }
236
237 async fn execute_once_with_retry(&mut self, ctx: &Context) {
238 let mut retry = self.exponential_builder.build();
239 let mut retry_times = 0;
240
241 let mut rollback = self.exponential_builder.build();
242 let mut rollback_times = 0;
243
244 loop {
245 if !self.running() {
247 self.set_state_and_record(ProcedureState::failed(Arc::new(
248 error::ManagerNotStartSnafu {}.build(),
249 )));
250 return;
251 }
252 let state = self.meta.state();
253 match state {
254 ProcedureState::Running => {}
255 ProcedureState::Retrying { error } => {
256 retry_times += 1;
257 if let Some(d) = retry.next() {
258 let millis = d.as_millis() as u64;
259 let noise = rand::rng().random_range(0..(millis / 4) + 1);
261 let d = d.add(Duration::from_millis(noise));
262
263 self.wait_on_err(d, retry_times).await;
264 } else {
265 self.meta
266 .set_state(ProcedureState::prepare_rollback(Arc::new(
267 Error::RetryTimesExceeded {
268 source: error.clone(),
269 procedure_id: self.meta.id,
270 },
271 )));
272 }
273 }
274 ProcedureState::PrepareRollback { error }
275 | ProcedureState::RollingBack { error } => {
276 rollback_times += 1;
277 if let Some(d) = rollback.next() {
278 self.wait_on_err(d, rollback_times as u64).await;
279 } else {
280 let err = Err::<(), Arc<Error>>(error)
281 .context(RollbackTimesExceededSnafu {
282 procedure_id: self.meta.id,
283 })
284 .unwrap_err();
285 self.set_state_and_record(ProcedureState::failed(Arc::new(err)));
286 return;
287 }
288 }
289 ProcedureState::Done { .. } => return,
290 ProcedureState::Failed { .. } => return,
291 ProcedureState::Poisoned { .. } => return,
292 }
293 self.execute_once(ctx).await;
294 }
295 }
296
297 async fn clean_poisons(&mut self) -> Result<()> {
298 let mut error = None;
299 for key in self.meta.poison_keys.iter() {
300 let key = key.to_string();
301 if let Err(e) = self
302 .manager_ctx
303 .poison_manager
304 .delete_poison(key, self.meta.id.to_string())
305 .await
306 {
307 error!(e; "Failed to clean poisons for procedure: {}", self.meta.id);
308 error = Some(e);
309 }
310 }
311
312 if let Some(e) = error {
314 return Err(e);
315 }
316 Ok(())
317 }
318
319 async fn rollback(&mut self, ctx: &Context, err: Arc<Error>) {
320 if self.procedure.rollback_supported()
321 && let Err(e) = self.procedure.rollback(ctx).await
322 {
323 self.set_state_and_record(ProcedureState::rolling_back(Arc::new(e)));
324 return;
325 }
326 self.set_state_and_record(ProcedureState::failed(err));
327 }
328
329 async fn prepare_rollback(&mut self, err: Arc<Error>) {
330 if let Err(e) = self.write_rollback_procedure_state(err.to_string()).await {
331 self.meta
332 .set_state(ProcedureState::prepare_rollback(Arc::new(e)));
333 return;
334 }
335 if self.procedure.rollback_supported() {
336 self.set_state_and_record(ProcedureState::rolling_back(err));
337 } else {
338 self.set_state_and_record(ProcedureState::failed(err));
339 }
340 }
341
342 async fn execute_once(&mut self, ctx: &Context) {
343 match self.meta.state() {
344 ProcedureState::Running | ProcedureState::Retrying { .. } => {
345 match self.procedure.execute(ctx).await {
346 Ok(status) => {
347 debug!(
348 "Execute procedure {}-{} once, status: {:?}, need_persist: {}",
349 self.procedure.type_name(),
350 self.meta.id,
351 status,
352 status.need_persist(),
353 );
354
355 if !self.running() {
357 self.set_state_and_record(ProcedureState::failed(Arc::new(
358 error::ManagerNotStartSnafu {}.build(),
359 )));
360 return;
361 }
362
363 if status.need_clean_poisons()
365 && let Err(e) = self.clean_poisons().await
366 {
367 error!(e; "Failed to clean poison for procedure: {}", self.meta.id);
368 self.set_state_and_record(ProcedureState::retrying(Arc::new(e)));
369 return;
370 }
371
372 if status.need_persist()
373 && let Err(e) = self.persist_procedure().await
374 {
375 error!(e; "Failed to persist procedure: {}", self.meta.id);
376 self.set_state_and_record(ProcedureState::retrying(Arc::new(e)));
377 return;
378 }
379
380 match status {
381 Status::Executing { .. } => {
382 let prev_state = self.meta.state();
383 if !matches!(prev_state, ProcedureState::Running) {
384 info!(
385 "Set Procedure {}-{} state to running, prev_state: {:?}",
386 self.procedure.type_name(),
387 self.meta.id,
388 prev_state
389 );
390 self.meta.set_state(ProcedureState::Running);
391 }
392 }
393 Status::Suspended { subprocedures, .. } => {
394 let prev_state = self.meta.state();
395 if !matches!(prev_state, ProcedureState::Running) {
396 info!(
397 "Set Procedure {}-{} state to running, prev_state: {:?}",
398 self.procedure.type_name(),
399 self.meta.id,
400 prev_state
401 );
402 self.meta.set_state(ProcedureState::Running);
403 }
404 self.on_suspended(subprocedures).await;
405 }
406 Status::Done { output } => {
407 if let Err(e) = self.commit_procedure().await {
408 error!(e; "Failed to commit procedure: {}", self.meta.id);
409 self.set_state_and_record(ProcedureState::retrying(Arc::new(
410 e,
411 )));
412 return;
413 }
414
415 self.done(output);
416 }
417 Status::Poisoned { error, keys } => {
418 error!(
419 error;
420 "Procedure {}-{} is poisoned, keys: {:?}",
421 self.procedure.type_name(),
422 self.meta.id,
423 keys,
424 );
425 self.set_state_and_record(ProcedureState::poisoned(
426 keys,
427 Arc::new(error),
428 ));
429 }
430 }
431 }
432 Err(e) => {
433 error!(
434 e;
435 "Failed to execute procedure {}-{}, retry: {}, clean_poisons: {}",
436 self.procedure.type_name(),
437 self.meta.id,
438 e.is_retry_later(),
439 e.need_clean_poisons(),
440 );
441
442 if !self.running() {
444 self.set_state_and_record(ProcedureState::failed(Arc::new(
445 error::ManagerNotStartSnafu {}.build(),
446 )));
447 return;
448 }
449
450 if e.need_clean_poisons() {
451 if let Err(e) = self.clean_poisons().await {
452 error!(e; "Failed to clean poison for procedure: {}", self.meta.id);
453 self.set_state_and_record(ProcedureState::retrying(Arc::new(e)));
454 return;
455 }
456 debug!(
457 "Procedure {}-{} cleaned poisons",
458 self.procedure.type_name(),
459 self.meta.id,
460 );
461 }
462
463 if e.is_retry_later() {
464 self.set_state_and_record(ProcedureState::retrying(Arc::new(e)));
465 return;
466 }
467
468 if self.procedure.rollback_supported() {
469 self.meta
470 .set_state(ProcedureState::prepare_rollback(Arc::new(e)));
471 } else {
472 self.set_state_and_record(ProcedureState::failed(Arc::new(e)));
473 }
474 }
475 }
476 }
477 ProcedureState::PrepareRollback { error } => self.prepare_rollback(error).await,
478 ProcedureState::RollingBack { error } => self.rollback(ctx, error).await,
479 ProcedureState::Failed { .. }
480 | ProcedureState::Done { .. }
481 | ProcedureState::Poisoned { .. } => (),
482 }
483 }
484
485 fn submit_subprocedure(
487 &self,
488 procedure_id: ProcedureId,
489 procedure_state: ProcedureState,
490 procedure: BoxedProcedure,
491 ) -> (ChildSubmissionOutcome, Option<ProcedureEvent>) {
492 if !self.running() {
493 warn!(
494 "ProcedureManager is not running, skip submitting subprocedure {}-{}",
495 procedure.type_name(),
496 procedure_id
497 );
498 return (ChildSubmissionOutcome::ManagerStopped, None);
499 }
500
501 if self.manager_ctx.contains_procedure(procedure_id) {
502 return (ChildSubmissionOutcome::AlreadyAccepted, None);
504 }
505
506 let step = 0;
507
508 let meta = Arc::new(ProcedureMeta::new(
509 procedure_id,
510 procedure_state,
511 Some(self.meta.id),
512 self.meta.context.clone(),
513 procedure.lock_key(),
514 procedure.poison_keys(),
515 procedure.type_name(),
516 ));
517 let runner = Runner {
518 meta: meta.clone(),
519 procedure,
520 manager_ctx: self.manager_ctx.clone(),
521 step,
522 exponential_builder: self.exponential_builder,
523 store: self.store.clone(),
524 rolling_back: false,
525 event_recorder: self.event_recorder.clone(),
526 execute_retry_attempt: 0,
527 rollback_retry_attempt: 0,
528 };
529
530 assert!(
534 self.manager_ctx.try_insert_procedure(meta),
535 "Procedure {}-{} submit an existing procedure {}-{}",
536 self.procedure.type_name(),
537 self.meta.id,
538 runner.procedure.type_name(),
539 procedure_id,
540 );
541
542 let submitted_event = runner.build_event(EventTrigger::Submitted);
543 let parent_id = self.meta.id;
544
545 let tracing_context = TracingContext::from_current_span();
546 if !self.manager_ctx.spawn_runner_task(procedure_id, || {
547 common_runtime::spawn_global(async move {
548 let span = tracing_context.attach(tracing::info_span!(
549 "LocalManager::submit_subprocedure",
550 procedure_name = %runner.meta.type_name,
551 procedure_id = %runner.meta.id,
552 parent_id = %parent_id,
553 ));
554 runner.run().trace(span).await
558 })
559 }) {
560 self.manager_ctx.remove_procedure(procedure_id);
561 return (ChildSubmissionOutcome::SpawnFailed, None);
562 }
563
564 self.meta.push_child(procedure_id);
566 (ChildSubmissionOutcome::Accepted, submitted_event)
567 }
568
569 async fn wait_on_err(&mut self, d: Duration, i: u64) {
571 info!(
572 "Procedure {}-{} retry for the {} times after {} millis",
573 self.procedure.type_name(),
574 self.meta.id,
575 i,
576 d.as_millis(),
577 );
578 time::sleep(d).await;
579 }
580
581 async fn on_suspended(&mut self, subprocedures: Vec<ProcedureWithId>) {
582 let has_child = !subprocedures.is_empty();
583
584 for sub in &subprocedures {
589 let conflicting = find_lock_conflicts(
590 self.meta.lock_key.keys_to_lock(),
591 sub.procedure.lock_key().keys_to_lock(),
592 );
593 if !conflicting.is_empty() {
594 let err_msg = format!(
595 "Deadlock prevented: subprocedure {}-{} shares conflicting lock key(s) {:?} \
596 with parent {}-{}. Parent holds these locks and would wait for child \
597 completion, but child cannot acquire them.",
598 sub.procedure.type_name(),
599 sub.id,
600 conflicting,
601 self.procedure.type_name(),
602 self.meta.id,
603 );
604 error!("{}", err_msg);
605 let err = Arc::new(Error::external(PlainError::new(
606 err_msg,
607 StatusCode::Internal,
608 )));
609 if self.procedure.rollback_supported() {
610 self.meta.set_state(ProcedureState::prepare_rollback(err));
611 } else {
612 self.set_state_and_record(ProcedureState::failed(err));
613 }
614 return;
615 }
616 }
617
618 for subprocedure in subprocedures {
619 info!(
620 "Procedure {}-{} submit subprocedure {}-{}",
621 self.procedure.type_name(),
622 self.meta.id,
623 subprocedure.procedure.type_name(),
624 subprocedure.id,
625 );
626
627 let child_id = subprocedure.id;
628 let (outcome, submitted_event) = self.submit_subprocedure(
629 subprocedure.id,
630 ProcedureState::Running,
631 subprocedure.procedure,
632 );
633 if let Some(event) = submitted_event
634 && let Some(recorder) = self.event_recorder.as_ref()
635 {
636 recorder.record(Box::new(event));
637 }
638 self.record_event(EventTrigger::ChildSubmitted {
639 procedure_id: child_id,
640 outcome,
641 });
642 }
643
644 info!(
645 "Procedure {}-{} is waiting for subprocedures",
646 self.procedure.type_name(),
647 self.meta.id,
648 );
649
650 if has_child {
652 self.meta.child_notify.notified().await;
653
654 info!(
655 "Procedure {}-{} is waked up",
656 self.procedure.type_name(),
657 self.meta.id,
658 );
659 }
660 }
661
662 async fn persist_procedure(&mut self) -> Result<()> {
663 let type_name = self.procedure.type_name().to_string();
664 let data = self.procedure.dump()?;
665
666 self.store
667 .store_procedure_with_context(
668 self.meta.id,
669 self.step,
670 type_name,
671 data,
672 self.meta.parent_id,
673 self.meta.context.clone(),
674 )
675 .await
676 .map_err(|e| {
677 error!(
678 e; "Failed to persist procedure {}-{}",
679 self.procedure.type_name(),
680 self.meta.id
681 );
682 e
683 })?;
684 self.step += 1;
685 Ok(())
686 }
687
688 async fn commit_procedure(&mut self) -> Result<()> {
689 self.store
690 .commit_procedure(self.meta.id, self.step)
691 .await
692 .map_err(|e| {
693 error!(
694 e; "Failed to commit procedure {}-{}",
695 self.procedure.type_name(),
696 self.meta.id
697 );
698 e
699 })?;
700 self.step += 1;
701 Ok(())
702 }
703
704 async fn write_rollback_procedure_state(&mut self, error: String) -> Result<()> {
705 let type_name = self.procedure.type_name().to_string();
707 let data = self.procedure.dump()?;
708 let message = ProcedureMessage {
709 type_name,
710 data,
711 parent_id: self.meta.parent_id,
712 step: self.step,
713 error: Some(error),
714 context: self.meta.context.clone(),
715 };
716 self.store
717 .rollback_procedure(self.meta.id, message)
718 .await
719 .map_err(|e| {
720 error!(
721 e; "Failed to write rollback key for procedure {}-{}",
722 self.procedure.type_name(),
723 self.meta.id
724 );
725 e
726 })?;
727 self.step += 1;
728 Ok(())
729 }
730
731 fn done(&mut self, output: Option<Output>) {
732 info!(
734 "Procedure {}-{} done",
735 self.procedure.type_name(),
736 self.meta.id,
737 );
738
739 self.set_state_and_record(ProcedureState::Done { output });
741 }
742
743 fn set_state_and_record(&mut self, state: ProcedureState) {
745 let trigger = match &state {
746 ProcedureState::Retrying { .. } => {
747 self.execute_retry_attempt += 1;
748 Some(EventTrigger::Retrying {
749 phase: RetryPhase::Execute,
750 attempt: self.execute_retry_attempt,
751 })
752 }
753 ProcedureState::RollingBack { .. } => {
754 if self.rolling_back {
755 self.rollback_retry_attempt += 1;
756 Some(EventTrigger::Retrying {
757 phase: RetryPhase::Rollback,
758 attempt: self.rollback_retry_attempt,
759 })
760 } else {
761 self.rolling_back = true;
762 Some(EventTrigger::RollingBack)
763 }
764 }
765 ProcedureState::Done { .. } => Some(EventTrigger::Succeeded),
766 ProcedureState::Failed { .. } => Some(EventTrigger::Failed),
767 ProcedureState::Poisoned { .. } => Some(EventTrigger::Poisoned),
768 ProcedureState::Running => None,
769 ProcedureState::PrepareRollback { .. } => None,
772 };
773 self.meta.set_state(state);
774 if let Some(trigger) = trigger {
775 self.record_event(trigger);
776 }
777 }
778
779 pub(crate) fn build_event(&self, trigger: EventTrigger) -> Option<ProcedureEvent> {
781 let recorder = self.event_recorder.as_ref()?;
782 let state = self.meta.state();
783 let context = EventContext {
784 procedure_id: self.meta.id,
785 lifecycle_state: &state,
786 trigger: trigger.clone(),
787 event_type_filter: recorder.event_type_filter(),
788 event_context: self.meta.context.event_context.as_ref(),
789 };
790 self.procedure.event(&context).map(|event| {
791 ProcedureEvent::new_with_context(
792 self.meta.id,
793 event,
794 state,
795 trigger,
796 self.meta.context.clone(),
797 )
798 })
799 }
800
801 pub(crate) fn record_event(&self, trigger: EventTrigger) {
804 if let Some(event) = self.build_event(trigger)
805 && let Some(recorder) = self.event_recorder.as_ref()
806 {
807 recorder.record(Box::new(event));
808 }
809 }
810}
811
812impl Drop for Runner {
813 fn drop(&mut self) {
814 self.manager_ctx.remove_runner_task(self.meta.id);
815 }
816}
817
818#[cfg(test)]
819mod tests {
820 use std::assert_matches;
821 use std::sync::Arc;
822 use std::sync::atomic::{AtomicU64, Ordering};
823
824 use async_trait::async_trait;
825 use common_error::ext::{ErrorExt, PlainError};
826 use common_error::mock::MockError;
827 use common_error::status_code::StatusCode;
828 use common_test_util::temp_dir::create_temp_dir;
829 use futures::future::join_all;
830 use futures_util::FutureExt;
831 use futures_util::future::BoxFuture;
832 use object_store::{EntryMode, ObjectStore};
833 use tokio::sync::mpsc;
834 use tokio::sync::watch::Receiver;
835
836 use super::*;
837 use crate::local::{DynamicKeyLockGuard, test_util};
838 use crate::procedure::PoisonKeys;
839 use crate::store::proc_path;
840 use crate::test_util::InMemoryPoisonStore;
841 use crate::{ContextProvider, Error, LockKey, PoisonKey, Procedure};
842
843 const ROOT_ID: &str = "9f805a1f-05f7-490c-9f91-bd56e3cc54c1";
844
845 fn new_runner(
846 meta: ProcedureMetaRef,
847 procedure: BoxedProcedure,
848 store: Arc<ProcedureStore>,
849 ) -> Runner {
850 Runner {
851 meta,
852 procedure,
853 manager_ctx: Arc::new(ManagerContext::new(
854 Arc::new(InMemoryPoisonStore::default()),
855 )),
856 step: 0,
857 exponential_builder: ExponentialBuilder::default(),
858 store,
859 rolling_back: false,
860 event_recorder: None,
861 execute_retry_attempt: 0,
862 rollback_retry_attempt: 0,
863 }
864 }
865
866 async fn check_files(
867 object_store: &ObjectStore,
868 procedure_store: &ProcedureStore,
869 procedure_id: ProcedureId,
870 files: &[&str],
871 ) {
872 let dir = proc_path!(procedure_store, "{procedure_id}/");
873 let lister = object_store.list(&dir).await.unwrap();
874 let mut files_in_dir: Vec<_> = lister
875 .into_iter()
876 .filter(|x| x.metadata().mode() == EntryMode::FILE)
877 .map(|de| de.name().to_string())
878 .collect();
879 files_in_dir.sort_unstable();
880 assert_eq!(files, files_in_dir);
881 }
882
883 fn context_with_provider(
884 procedure_id: ProcedureId,
885 provider: Arc<dyn ContextProvider>,
886 ) -> Context {
887 Context {
888 procedure_id,
889 provider,
890 event_context: None,
891 }
892 }
893
894 fn context_without_provider(procedure_id: ProcedureId) -> Context {
895 struct MockProvider;
896
897 #[async_trait]
898 impl ContextProvider for MockProvider {
899 async fn procedure_state(
900 &self,
901 _procedure_id: ProcedureId,
902 ) -> Result<Option<ProcedureState>> {
903 unimplemented!()
904 }
905
906 async fn procedure_state_receiver(
907 &self,
908 _procedure_id: ProcedureId,
909 ) -> Result<Option<Receiver<ProcedureState>>> {
910 unimplemented!()
911 }
912
913 async fn try_put_poison(
914 &self,
915 _key: &PoisonKey,
916 _procedure_id: ProcedureId,
917 ) -> Result<()> {
918 unimplemented!()
919 }
920
921 async fn acquire_lock(&self, _key: &StringKey) -> DynamicKeyLockGuard {
922 unimplemented!()
923 }
924 }
925
926 Context {
927 procedure_id,
928 provider: Arc::new(MockProvider),
929 event_context: None,
930 }
931 }
932
933 type RollbackFn = Box<dyn FnMut(Context) -> BoxFuture<'static, Result<()>> + Send>;
934
935 struct ProcedureAdapter<F> {
936 data: String,
937 lock_key: LockKey,
938 poison_keys: PoisonKeys,
939 exec_fn: F,
940 rollback_fn: Option<RollbackFn>,
941 }
942
943 impl<F> ProcedureAdapter<F> {
944 fn new_meta(&self, uuid: &str) -> ProcedureMetaRef {
945 let mut meta = test_util::procedure_meta_for_test();
946 meta.id = ProcedureId::parse_str(uuid).unwrap();
947 meta.lock_key = self.lock_key.clone();
948 meta.poison_keys = self.poison_keys.clone();
949
950 Arc::new(meta)
951 }
952 }
953
954 #[async_trait]
955 impl<F> Procedure for ProcedureAdapter<F>
956 where
957 F: FnMut(Context) -> BoxFuture<'static, Result<Status>> + Send + Sync,
958 {
959 fn type_name(&self) -> &str {
960 "ProcedureAdapter"
961 }
962
963 async fn execute(&mut self, ctx: &Context) -> Result<Status> {
964 let f = (self.exec_fn)(ctx.clone());
965 f.await
966 }
967
968 async fn rollback(&mut self, ctx: &Context) -> Result<()> {
969 if let Some(f) = &mut self.rollback_fn {
970 return (f)(ctx.clone()).await;
971 }
972 Ok(())
973 }
974
975 fn rollback_supported(&self) -> bool {
976 self.rollback_fn.is_some()
977 }
978
979 fn dump(&self) -> Result<String> {
980 Ok(self.data.clone())
981 }
982
983 fn lock_key(&self) -> LockKey {
984 self.lock_key.clone()
985 }
986
987 fn poison_keys(&self) -> PoisonKeys {
988 self.poison_keys.clone()
989 }
990 }
991
992 async fn execute_once_normal(persist: bool, first_files: &[&str], second_files: &[&str]) {
993 let mut times = 0;
994 let exec_fn = move |_| {
995 times += 1;
996 async move {
997 if times == 1 {
998 Ok(Status::executing(persist))
999 } else {
1000 Ok(Status::done())
1001 }
1002 }
1003 .boxed()
1004 };
1005 let normal = ProcedureAdapter {
1006 data: "normal".to_string(),
1007 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1008 poison_keys: PoisonKeys::default(),
1009 exec_fn,
1010 rollback_fn: None,
1011 };
1012
1013 let dir = create_temp_dir("normal");
1014 let meta = normal.new_meta(ROOT_ID);
1015 let ctx = context_without_provider(meta.id);
1016 let object_store = test_util::new_object_store(&dir);
1017 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1018 let mut runner = new_runner(meta, Box::new(normal), procedure_store.clone());
1019 runner.manager_ctx.start();
1020
1021 runner.execute_once(&ctx).await;
1022 let state = runner.meta.state();
1023 assert!(state.is_running(), "{state:?}");
1024 check_files(
1025 &object_store,
1026 &procedure_store,
1027 ctx.procedure_id,
1028 first_files,
1029 )
1030 .await;
1031
1032 runner.execute_once(&ctx).await;
1033 let state = runner.meta.state();
1034 assert!(state.is_done(), "{state:?}");
1035 check_files(
1036 &object_store,
1037 &procedure_store,
1038 ctx.procedure_id,
1039 second_files,
1040 )
1041 .await;
1042 }
1043
1044 #[tokio::test]
1045 async fn test_execute_once_normal() {
1046 execute_once_normal(
1047 true,
1048 &["0000000000.step"],
1049 &["0000000000.step", "0000000001.commit"],
1050 )
1051 .await;
1052 }
1053
1054 #[tokio::test]
1055 async fn test_execute_once_normal_skip_persist() {
1056 execute_once_normal(false, &[], &["0000000000.commit"]).await;
1057 }
1058
1059 #[tokio::test]
1060 async fn test_on_suspend_empty() {
1061 let exec_fn = move |_| {
1062 async move {
1063 Ok(Status::Suspended {
1064 subprocedures: Vec::new(),
1065 persist: false,
1066 })
1067 }
1068 .boxed()
1069 };
1070 let suspend = ProcedureAdapter {
1071 data: "suspend".to_string(),
1072 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1073 poison_keys: PoisonKeys::default(),
1074 exec_fn,
1075 rollback_fn: None,
1076 };
1077
1078 let dir = create_temp_dir("suspend");
1079 let meta = suspend.new_meta(ROOT_ID);
1080 let ctx = context_without_provider(meta.id);
1081 let object_store = test_util::new_object_store(&dir);
1082 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1083 let mut runner = new_runner(meta, Box::new(suspend), procedure_store);
1084 runner.manager_ctx.start();
1085
1086 runner.execute_once(&ctx).await;
1087 let state = runner.meta.state();
1088 assert!(state.is_running(), "{state:?}");
1089 }
1090
1091 fn new_child_procedure(procedure_id: ProcedureId, keys: &[&str]) -> ProcedureWithId {
1092 let mut times = 0;
1093 let exec_fn = move |_| {
1094 times += 1;
1095 async move {
1096 if times == 1 {
1097 time::sleep(Duration::from_millis(200)).await;
1098 Ok(Status::executing(true))
1099 } else {
1100 Ok(Status::done())
1101 }
1102 }
1103 .boxed()
1104 };
1105 let child = ProcedureAdapter {
1106 data: "child".to_string(),
1107 lock_key: LockKey::new_exclusive(keys.iter().map(|k| k.to_string())),
1108 poison_keys: PoisonKeys::default(),
1109 exec_fn,
1110 rollback_fn: None,
1111 };
1112
1113 ProcedureWithId {
1114 id: procedure_id,
1115 procedure: Box::new(child),
1116 context: Default::default(),
1117 }
1118 }
1119
1120 #[tokio::test]
1121 async fn test_on_suspend_by_subprocedures() {
1122 let mut times = 0;
1123 let children_ids = [ProcedureId::random(), ProcedureId::random()];
1124 let keys = [
1125 &[
1126 "catalog.schema.table.region-0",
1127 "catalog.schema.table.region-1",
1128 ],
1129 &[
1130 "catalog.schema.table.region-2",
1131 "catalog.schema.table.region-3",
1132 ],
1133 ];
1134
1135 let exec_fn = move |ctx: Context| {
1136 times += 1;
1137 async move {
1138 if times == 1 {
1139 Ok(Status::Suspended {
1141 subprocedures: children_ids
1142 .into_iter()
1143 .zip(keys)
1144 .map(|(id, key_slice)| new_child_procedure(id, key_slice))
1145 .collect(),
1146 persist: true,
1147 })
1148 } else {
1149 let mut all_child_done = true;
1151 for id in children_ids {
1152 let is_not_done = ctx
1153 .provider
1154 .procedure_state(id)
1155 .await
1156 .unwrap()
1157 .map(|s| !s.is_done())
1158 .unwrap_or(true);
1159 if is_not_done {
1160 all_child_done = false;
1161 }
1162 }
1163 if all_child_done {
1164 Ok(Status::done())
1165 } else {
1166 Ok(Status::Suspended {
1168 subprocedures: Vec::new(),
1169 persist: false,
1170 })
1171 }
1172 }
1173 }
1174 .boxed()
1175 };
1176 let parent = ProcedureAdapter {
1177 data: "parent".to_string(),
1178 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1179 poison_keys: PoisonKeys::default(),
1180 exec_fn,
1181 rollback_fn: None,
1182 };
1183
1184 let dir = create_temp_dir("parent");
1185 let meta = parent.new_meta(ROOT_ID);
1186 let procedure_id = meta.id;
1187
1188 let object_store = test_util::new_object_store(&dir);
1189 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1190 let mut runner = new_runner(meta.clone(), Box::new(parent), procedure_store.clone());
1191 let poison_manager = Arc::new(InMemoryPoisonStore::default());
1192 let manager_ctx = Arc::new(ManagerContext::new(poison_manager));
1193 manager_ctx.start();
1194 assert!(manager_ctx.try_insert_procedure(meta));
1196 runner.manager_ctx = manager_ctx.clone();
1198
1199 runner.run().await;
1200 assert!(manager_ctx.key_lock.is_empty());
1201
1202 for child_id in children_ids {
1204 let state = manager_ctx.state(child_id).unwrap();
1205 assert!(state.is_done(), "{state:?}");
1206 }
1207 let state = manager_ctx.state(procedure_id).unwrap();
1208 assert!(state.is_done(), "{state:?}");
1209 check_files(&object_store, &procedure_store, procedure_id, &[]).await;
1211
1212 tokio::time::sleep(Duration::from_millis(5)).await;
1213 manager_ctx.remove_outdated_meta(Duration::from_millis(1));
1215 assert!(manager_ctx.state(procedure_id).is_none());
1216 assert!(manager_ctx.finished_procedures.lock().unwrap().is_empty());
1217 for child_id in children_ids {
1218 assert!(manager_ctx.state(child_id).is_none());
1219 }
1220 }
1221
1222 #[tokio::test]
1223 async fn test_running_is_stopped() {
1224 let exec_fn = move |_| async move { Ok(Status::executing(true)) }.boxed();
1225 let normal = ProcedureAdapter {
1226 data: "normal".to_string(),
1227 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1228 poison_keys: PoisonKeys::default(),
1229 exec_fn,
1230 rollback_fn: None,
1231 };
1232
1233 let dir = create_temp_dir("test_running_is_stopped");
1234 let meta = normal.new_meta(ROOT_ID);
1235 let ctx = context_without_provider(meta.id);
1236 let object_store = test_util::new_object_store(&dir);
1237 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1238 let mut runner = new_runner(meta, Box::new(normal), procedure_store.clone());
1239 runner.manager_ctx.start();
1240
1241 runner.execute_once(&ctx).await;
1242 let state = runner.meta.state();
1243 assert!(state.is_running(), "{state:?}");
1244 check_files(
1245 &object_store,
1246 &procedure_store,
1247 ctx.procedure_id,
1248 &["0000000000.step"],
1249 )
1250 .await;
1251
1252 runner.manager_ctx.stop();
1253 runner.execute_once(&ctx).await;
1254 let state = runner.meta.state();
1255 assert!(state.is_failed(), "{state:?}");
1256 check_files(
1258 &object_store,
1259 &procedure_store,
1260 ctx.procedure_id,
1261 &["0000000000.step"],
1262 )
1263 .await;
1264 }
1265
1266 #[tokio::test]
1267 async fn test_running_is_stopped_on_error() {
1268 let exec_fn =
1269 |_| async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }.boxed();
1270 let normal = ProcedureAdapter {
1271 data: "fail".to_string(),
1272 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1273 poison_keys: PoisonKeys::default(),
1274 exec_fn,
1275 rollback_fn: None,
1276 };
1277
1278 let dir = create_temp_dir("test_running_is_stopped_on_error");
1279 let meta = normal.new_meta(ROOT_ID);
1280 let ctx = context_without_provider(meta.id);
1281 let object_store = test_util::new_object_store(&dir);
1282 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1283 let mut runner = new_runner(meta, Box::new(normal), procedure_store.clone());
1284 runner.manager_ctx.stop();
1285
1286 runner.execute_once(&ctx).await;
1287 let state = runner.meta.state();
1288 assert!(state.is_failed(), "{state:?}");
1289 check_files(&object_store, &procedure_store, ctx.procedure_id, &[]).await;
1291 }
1292
1293 #[tokio::test]
1294 async fn test_execute_on_error() {
1295 let exec_fn =
1296 |_| async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }.boxed();
1297 let fail = ProcedureAdapter {
1298 data: "fail".to_string(),
1299 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1300 poison_keys: PoisonKeys::default(),
1301 exec_fn,
1302 rollback_fn: None,
1303 };
1304
1305 let dir = create_temp_dir("fail");
1306 let meta = fail.new_meta(ROOT_ID);
1307 let ctx = context_without_provider(meta.id);
1308 let object_store = test_util::new_object_store(&dir);
1309 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1310 let mut runner = new_runner(meta.clone(), Box::new(fail), procedure_store.clone());
1311 runner.manager_ctx.start();
1312
1313 runner.execute_once(&ctx).await;
1314 let state = runner.meta.state();
1315 assert!(state.is_failed(), "{state:?}");
1316 check_files(&object_store, &procedure_store, ctx.procedure_id, &[]).await;
1317 }
1318
1319 #[tokio::test]
1320 async fn test_execute_with_rollback_on_error() {
1321 let exec_fn =
1322 |_| async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }.boxed();
1323 let rollback_fn = move |_| async move { Ok(()) }.boxed();
1324 let fail = ProcedureAdapter {
1325 data: "fail".to_string(),
1326 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1327 poison_keys: PoisonKeys::default(),
1328 exec_fn,
1329 rollback_fn: Some(Box::new(rollback_fn)),
1330 };
1331
1332 let dir = create_temp_dir("fail");
1333 let meta = fail.new_meta(ROOT_ID);
1334 let ctx = context_without_provider(meta.id);
1335 let object_store = test_util::new_object_store(&dir);
1336 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1337 let mut runner = new_runner(meta.clone(), Box::new(fail), procedure_store.clone());
1338 runner.manager_ctx.start();
1339
1340 runner.execute_once(&ctx).await;
1341 let state = runner.meta.state();
1342 assert!(state.is_prepare_rollback(), "{state:?}");
1343
1344 runner.execute_once(&ctx).await;
1345 let state = runner.meta.state();
1346 assert!(state.is_rolling_back(), "{state:?}");
1347
1348 runner.execute_once(&ctx).await;
1349 let state = runner.meta.state();
1350 assert!(state.is_failed(), "{state:?}");
1351 check_files(
1352 &object_store,
1353 &procedure_store,
1354 ctx.procedure_id,
1355 &["0000000000.rollback"],
1356 )
1357 .await;
1358 }
1359
1360 #[tokio::test]
1361 async fn test_execute_on_retry_later_error() {
1362 let mut times = 0;
1363
1364 let exec_fn = move |_| {
1365 times += 1;
1366 async move {
1367 if times == 1 {
1368 Err(Error::retry_later(MockError::new(StatusCode::Unexpected)))
1369 } else if times == 2 {
1370 Ok(Status::executing(false))
1371 } else {
1372 Ok(Status::done())
1373 }
1374 }
1375 .boxed()
1376 };
1377
1378 let retry_later = ProcedureAdapter {
1379 data: "retry_later".to_string(),
1380 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1381 poison_keys: PoisonKeys::default(),
1382 exec_fn,
1383 rollback_fn: None,
1384 };
1385
1386 let dir = create_temp_dir("retry_later");
1387 let meta = retry_later.new_meta(ROOT_ID);
1388 let ctx = context_without_provider(meta.id);
1389 let object_store = test_util::new_object_store(&dir);
1390 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1391 let mut runner = new_runner(meta.clone(), Box::new(retry_later), procedure_store.clone());
1392 runner.manager_ctx.start();
1393 runner.execute_once(&ctx).await;
1394 let state = runner.meta.state();
1395 assert!(state.is_retrying(), "{state:?}");
1396
1397 runner.execute_once(&ctx).await;
1398 let state = runner.meta.state();
1399 assert!(state.is_running(), "{state:?}");
1400
1401 runner.execute_once(&ctx).await;
1402 let state = runner.meta.state();
1403 assert!(state.is_done(), "{state:?}");
1404 assert!(meta.state().is_done());
1405 check_files(
1406 &object_store,
1407 &procedure_store,
1408 ctx.procedure_id,
1409 &["0000000000.commit"],
1410 )
1411 .await;
1412 }
1413
1414 #[tokio::test]
1415 async fn test_retrying_state_visible_in_context_on_retry() {
1416 let retrying_states = Arc::new(std::sync::Mutex::new(Vec::new()));
1417 let captured = retrying_states.clone();
1418 let mut times = 0;
1419
1420 let exec_fn = move |ctx: Context| {
1421 times += 1;
1422 let captured = captured.clone();
1423 async move {
1424 let is_retrying = ctx.is_retrying().await;
1425 captured.lock().unwrap().push(is_retrying);
1426 if times == 1 {
1427 Err(Error::retry_later(MockError::new(StatusCode::Unexpected)))
1428 } else {
1429 Ok(Status::done())
1430 }
1431 }
1432 .boxed()
1433 };
1434
1435 let procedure = ProcedureAdapter {
1436 data: "retrying_state".to_string(),
1437 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1438 poison_keys: PoisonKeys::default(),
1439 exec_fn,
1440 rollback_fn: None,
1441 };
1442
1443 let dir = create_temp_dir("retrying_state");
1444 let meta = procedure.new_meta(ROOT_ID);
1445 let object_store = test_util::new_object_store(&dir);
1446 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store));
1447 let mut runner = new_runner(meta.clone(), Box::new(procedure), procedure_store);
1448 let ctx = context_with_provider(
1449 meta.id,
1450 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1451 );
1452
1453 runner
1454 .manager_ctx
1455 .procedures
1456 .write()
1457 .unwrap()
1458 .insert(meta.id, runner.meta.clone());
1459 runner.manager_ctx.start();
1460
1461 runner.execute_once(&ctx).await;
1462 runner.execute_once(&ctx).await;
1463
1464 let states = retrying_states.lock().unwrap().clone();
1465 assert_eq!(states, vec![Some(false), Some(true)]);
1466 }
1467
1468 #[tokio::test(flavor = "multi_thread")]
1469 async fn test_execute_on_retry_later_error_with_child() {
1470 common_telemetry::init_default_ut_logging();
1471 let mut times = 0;
1472 let child_id = ProcedureId::random();
1473
1474 let exec_fn = move |_| {
1475 times += 1;
1476 async move {
1477 debug!("times: {}", times);
1478 if times == 1 {
1479 Err(Error::retry_later(MockError::new(StatusCode::Unexpected)))
1480 } else if times == 2 {
1481 let exec_fn = |_| {
1482 async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }
1483 .boxed()
1484 };
1485 let fail = ProcedureAdapter {
1486 data: "fail".to_string(),
1487 lock_key: LockKey::single_exclusive("catalog.schema.table.region-0"),
1488 poison_keys: PoisonKeys::default(),
1489 exec_fn,
1490 rollback_fn: None,
1491 };
1492
1493 Ok(Status::Suspended {
1494 subprocedures: vec![ProcedureWithId {
1495 id: child_id,
1496 procedure: Box::new(fail),
1497 context: Default::default(),
1498 }],
1499 persist: true,
1500 })
1501 } else {
1502 Ok(Status::done())
1503 }
1504 }
1505 .boxed()
1506 };
1507
1508 let retry_later = ProcedureAdapter {
1509 data: "retry_later".to_string(),
1510 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1511 poison_keys: PoisonKeys::default(),
1512 exec_fn,
1513 rollback_fn: None,
1514 };
1515
1516 let dir = create_temp_dir("retry_later");
1517 let meta = retry_later.new_meta(ROOT_ID);
1518 let ctx = context_without_provider(meta.id);
1519 let object_store = test_util::new_object_store(&dir);
1520 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1521 let mut runner = new_runner(meta.clone(), Box::new(retry_later), procedure_store.clone());
1522 runner.manager_ctx.start();
1523 debug!("execute_once 1");
1524 runner.execute_once(&ctx).await;
1525 let state = runner.meta.state();
1526 assert!(state.is_retrying(), "{state:?}");
1527
1528 let moved_meta = meta.clone();
1529 tokio::spawn(async move {
1530 moved_meta.child_notify.notify_one();
1531 });
1532 runner.execute_once(&ctx).await;
1533 let state = runner.meta.state();
1534 assert!(state.is_running(), "{state:?}");
1535
1536 runner.execute_once(&ctx).await;
1537 let state = runner.meta.state();
1538 assert!(state.is_done(), "{state:?}");
1539 assert!(meta.state().is_done());
1540 check_files(
1541 &object_store,
1542 &procedure_store,
1543 ctx.procedure_id,
1544 &["0000000000.step", "0000000001.commit"],
1545 )
1546 .await;
1547 }
1548
1549 #[tokio::test]
1550 async fn test_execute_exceed_max_retry_later() {
1551 let exec_fn =
1552 |_| async { Err(Error::retry_later(MockError::new(StatusCode::Unexpected))) }.boxed();
1553
1554 let exceed_max_retry_later = ProcedureAdapter {
1555 data: "exceed_max_retry_later".to_string(),
1556 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1557 poison_keys: PoisonKeys::default(),
1558 exec_fn,
1559 rollback_fn: None,
1560 };
1561
1562 let dir = create_temp_dir("exceed_max_retry_later");
1563 let meta = exceed_max_retry_later.new_meta(ROOT_ID);
1564 let object_store = test_util::new_object_store(&dir);
1565 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1566 let mut runner = new_runner(
1567 meta.clone(),
1568 Box::new(exceed_max_retry_later),
1569 procedure_store,
1570 );
1571 runner.manager_ctx.start();
1572
1573 runner.exponential_builder = ExponentialBuilder::default()
1574 .with_min_delay(Duration::from_millis(1))
1575 .with_max_times(3);
1576
1577 runner.execute_procedure_in_loop().await;
1579 let err = meta.state().error().unwrap().to_string();
1580 assert!(err.contains("Procedure retry exceeded max times"));
1581 }
1582
1583 #[tokio::test]
1584 async fn test_rollback_exceed_max_retry_later() {
1585 let exec_fn =
1586 |_| async { Err(Error::retry_later(MockError::new(StatusCode::Unexpected))) }.boxed();
1587 let rollback_fn = move |_| {
1588 async move { Err(Error::retry_later(MockError::new(StatusCode::Unexpected))) }.boxed()
1589 };
1590 let exceed_max_retry_later = ProcedureAdapter {
1591 data: "exceed_max_rollback".to_string(),
1592 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1593 poison_keys: PoisonKeys::default(),
1594 exec_fn,
1595 rollback_fn: Some(Box::new(rollback_fn)),
1596 };
1597
1598 let dir = create_temp_dir("exceed_max_rollback");
1599 let meta = exceed_max_retry_later.new_meta(ROOT_ID);
1600 let object_store = test_util::new_object_store(&dir);
1601 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1602 let mut runner = new_runner(
1603 meta.clone(),
1604 Box::new(exceed_max_retry_later),
1605 procedure_store,
1606 );
1607 runner.manager_ctx.start();
1608 runner.exponential_builder = ExponentialBuilder::default()
1609 .with_min_delay(Duration::from_millis(1))
1610 .with_max_times(3);
1611
1612 runner.execute_procedure_in_loop().await;
1614 let err = meta.state().error().unwrap().to_string();
1615 assert!(err.contains("Procedure rollback exceeded max times"));
1616 }
1617
1618 #[tokio::test]
1619 async fn test_rollback_after_retry_fail() {
1620 let exec_fn = move |_| {
1621 async move { Err(Error::retry_later(MockError::new(StatusCode::Unexpected))) }.boxed()
1622 };
1623
1624 let (tx, mut rx) = mpsc::channel(1);
1625 let rollback_fn = move |_| {
1626 let tx = tx.clone();
1627 async move {
1628 tx.send(()).await.unwrap();
1629 Ok(())
1630 }
1631 .boxed()
1632 };
1633 let retry_later = ProcedureAdapter {
1634 data: "rollback_after_retry_fail".to_string(),
1635 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1636 poison_keys: PoisonKeys::default(),
1637 exec_fn,
1638 rollback_fn: Some(Box::new(rollback_fn)),
1639 };
1640
1641 let dir = create_temp_dir("retry_later");
1642 let meta = retry_later.new_meta(ROOT_ID);
1643 let ctx = context_without_provider(meta.id);
1644 let object_store = test_util::new_object_store(&dir);
1645 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1646 let mut runner = new_runner(meta.clone(), Box::new(retry_later), procedure_store.clone());
1647 runner.manager_ctx.start();
1648 runner.exponential_builder = ExponentialBuilder::default()
1649 .with_min_delay(Duration::from_millis(1))
1650 .with_max_times(3);
1651 runner.execute_procedure_in_loop().await;
1653 rx.recv().await.unwrap();
1654 assert_eq!(rx.try_recv().unwrap_err(), mpsc::error::TryRecvError::Empty);
1655 check_files(
1656 &object_store,
1657 &procedure_store,
1658 ctx.procedure_id,
1659 &["0000000000.rollback"],
1660 )
1661 .await;
1662 }
1663
1664 #[tokio::test]
1665 async fn test_child_error() {
1666 let mut times = 0;
1667 let child_id = ProcedureId::random();
1668 common_telemetry::init_default_ut_logging();
1669 let exec_fn = move |ctx: Context| {
1670 times += 1;
1671 async move {
1672 if times == 1 {
1673 let exec_fn = |_| {
1675 async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }
1676 .boxed()
1677 };
1678 let fail = ProcedureAdapter {
1679 data: "fail".to_string(),
1680 lock_key: LockKey::single_exclusive("catalog.schema.table.region-0"),
1681 poison_keys: PoisonKeys::default(),
1682 exec_fn,
1683 rollback_fn: None,
1684 };
1685
1686 Ok(Status::Suspended {
1687 subprocedures: vec![ProcedureWithId {
1688 id: child_id,
1689 procedure: Box::new(fail),
1690 context: Default::default(),
1691 }],
1692 persist: true,
1693 })
1694 } else {
1695 let state = ctx.provider.procedure_state(child_id).await.unwrap();
1697 let is_failed = state.map(|s| s.is_failed()).unwrap_or(false);
1698 if is_failed {
1699 Err(Error::from_error_ext(PlainError::new(
1701 "subprocedure failed".to_string(),
1702 StatusCode::Unexpected,
1703 )))
1704 } else {
1705 Ok(Status::Suspended {
1707 subprocedures: Vec::new(),
1708 persist: false,
1709 })
1710 }
1711 }
1712 }
1713 .boxed()
1714 };
1715 let parent = ProcedureAdapter {
1716 data: "parent".to_string(),
1717 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1718 poison_keys: PoisonKeys::default(),
1719 exec_fn,
1720 rollback_fn: None,
1721 };
1722
1723 let dir = create_temp_dir("child_err");
1724 let meta = parent.new_meta(ROOT_ID);
1725
1726 let object_store = test_util::new_object_store(&dir);
1727 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1728 let mut runner = new_runner(meta.clone(), Box::new(parent), procedure_store);
1729 let poison_manager = Arc::new(InMemoryPoisonStore::default());
1730 let manager_ctx = Arc::new(ManagerContext::new(poison_manager));
1731 manager_ctx.start();
1732 assert!(manager_ctx.try_insert_procedure(meta.clone()));
1734 runner.manager_ctx = manager_ctx.clone();
1736
1737 runner.run().await;
1739 assert!(manager_ctx.key_lock.is_empty());
1740 let err = meta.state().error().unwrap().output_msg();
1741 assert!(err.contains("subprocedure failed"), "{err}");
1742 }
1743
1744 #[tokio::test]
1745 async fn test_execute_with_clean_poisons() {
1746 common_telemetry::init_default_ut_logging();
1747 let mut times = 0;
1748 let poison_key = PoisonKey::new("table/1024");
1749 let moved_poison_key = poison_key.clone();
1750 let exec_fn = move |ctx: Context| {
1751 times += 1;
1752 let poison_key = moved_poison_key.clone();
1753 async move {
1754 if times == 1 {
1755 ctx.provider
1757 .try_put_poison(&poison_key, ctx.procedure_id)
1758 .await
1759 .unwrap();
1760
1761 Ok(Status::executing(true))
1762 } else {
1763 Ok(Status::executing_with_clean_poisons(true))
1764 }
1765 }
1766 .boxed()
1767 };
1768 let poison = ProcedureAdapter {
1769 data: "poison".to_string(),
1770 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1771 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
1772 exec_fn,
1773 rollback_fn: None,
1774 };
1775
1776 let dir = create_temp_dir("clean_poisons");
1777 let meta = poison.new_meta(ROOT_ID);
1778
1779 let object_store = test_util::new_object_store(&dir);
1780 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1781 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store.clone());
1782
1783 let ctx = context_with_provider(
1785 meta.id,
1786 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1787 );
1788 runner
1790 .manager_ctx
1791 .procedures
1792 .write()
1793 .unwrap()
1794 .insert(meta.id, runner.meta.clone());
1795
1796 runner.manager_ctx.start();
1797 runner.execute_once(&ctx).await;
1798 let state = runner.meta.state();
1799 assert!(state.is_running(), "{state:?}");
1800
1801 let procedure_id = runner
1802 .manager_ctx
1803 .poison_manager
1804 .get_poison(&poison_key.to_string())
1805 .await
1806 .unwrap();
1807 assert!(procedure_id.is_some());
1809
1810 runner.execute_once(&ctx).await;
1811 let state = runner.meta.state();
1812 assert!(state.is_running(), "{state:?}");
1813
1814 let procedure_id = runner
1815 .manager_ctx
1816 .poison_manager
1817 .get_poison(&poison_key.to_string())
1818 .await
1819 .unwrap();
1820 assert!(procedure_id.is_none());
1822 }
1823
1824 #[tokio::test]
1825 async fn test_execute_error_with_clean_poisons() {
1826 common_telemetry::init_default_ut_logging();
1827 let mut times = 0;
1828 let poison_key = PoisonKey::new("table/1024");
1829 let moved_poison_key = poison_key.clone();
1830 let exec_fn = move |ctx: Context| {
1831 times += 1;
1832 let poison_key = moved_poison_key.clone();
1833 async move {
1834 if times == 1 {
1835 ctx.provider
1837 .try_put_poison(&poison_key, ctx.procedure_id)
1838 .await
1839 .unwrap();
1840
1841 Ok(Status::executing(true))
1842 } else {
1843 Err(Error::external_and_clean_poisons(MockError::new(
1844 StatusCode::Unexpected,
1845 )))
1846 }
1847 }
1848 .boxed()
1849 };
1850 let poison = ProcedureAdapter {
1851 data: "poison".to_string(),
1852 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1853 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
1854 exec_fn,
1855 rollback_fn: None,
1856 };
1857
1858 let dir = create_temp_dir("error_with_clean_poisons");
1859 let meta = poison.new_meta(ROOT_ID);
1860
1861 let object_store = test_util::new_object_store(&dir);
1862 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1863 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store.clone());
1864
1865 let ctx = context_with_provider(
1867 meta.id,
1868 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1869 );
1870 runner
1872 .manager_ctx
1873 .procedures
1874 .write()
1875 .unwrap()
1876 .insert(meta.id, runner.meta.clone());
1877
1878 runner.manager_ctx.start();
1879 runner.execute_once(&ctx).await;
1880 let state = runner.meta.state();
1881 assert!(state.is_running(), "{state:?}");
1882
1883 let procedure_id = runner
1884 .manager_ctx
1885 .poison_manager
1886 .get_poison(&poison_key.to_string())
1887 .await
1888 .unwrap();
1889 assert!(procedure_id.is_some());
1891
1892 runner.execute_once(&ctx).await;
1893 let state = runner.meta.state();
1894 assert!(state.is_failed(), "{state:?}");
1895
1896 let procedure_id = runner
1897 .manager_ctx
1898 .poison_manager
1899 .get_poison(&poison_key.to_string())
1900 .await
1901 .unwrap();
1902 assert!(procedure_id.is_none());
1904 }
1905
1906 #[tokio::test]
1907 async fn test_execute_failed_after_set_poison() {
1908 let mut times = 0;
1909 let poison_key = PoisonKey::new("table/1024");
1910 let moved_poison_key = poison_key.clone();
1911 let exec_fn = move |ctx: Context| {
1912 times += 1;
1913 let poison_key = moved_poison_key.clone();
1914 async move {
1915 if times == 1 {
1916 Ok(Status::executing(true))
1917 } else {
1918 ctx.provider
1920 .try_put_poison(&poison_key, ctx.procedure_id)
1921 .await
1922 .unwrap();
1923 Err(Error::external(MockError::new(StatusCode::Unexpected)))
1924 }
1925 }
1926 .boxed()
1927 };
1928 let poison = ProcedureAdapter {
1929 data: "poison".to_string(),
1930 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1931 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
1932 exec_fn,
1933 rollback_fn: None,
1934 };
1935
1936 let dir = create_temp_dir("poison");
1937 let meta = poison.new_meta(ROOT_ID);
1938
1939 let object_store = test_util::new_object_store(&dir);
1940 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1941 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store.clone());
1942
1943 let ctx = context_with_provider(
1945 meta.id,
1946 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1947 );
1948 runner
1950 .manager_ctx
1951 .procedures
1952 .write()
1953 .unwrap()
1954 .insert(meta.id, runner.meta.clone());
1955
1956 runner.manager_ctx.start();
1957 runner.execute_once(&ctx).await;
1958 let state = runner.meta.state();
1959 assert!(state.is_running(), "{state:?}");
1960
1961 runner.execute_once(&ctx).await;
1962 let state = runner.meta.state();
1963 assert!(state.is_failed(), "{state:?}");
1964 assert!(meta.state().is_failed());
1965
1966 let procedure_id = runner
1968 .manager_ctx
1969 .poison_manager
1970 .get_poison(&poison_key.to_string())
1971 .await
1972 .unwrap()
1973 .unwrap();
1974
1975 assert_eq!(&procedure_id.clone(), ROOT_ID);
1977 }
1978
1979 #[tokio::test]
1980 async fn test_execute_exceed_max_retry_after_set_poison() {
1981 common_telemetry::init_default_ut_logging();
1982 let mut times = 0;
1983 let poison_key = PoisonKey::new("table/1024");
1984 let moved_poison_key = poison_key.clone();
1985 let exec_fn = move |ctx: Context| {
1986 times += 1;
1987 let poison_key = moved_poison_key.clone();
1988 async move {
1989 if times == 1 {
1990 Ok(Status::executing(true))
1991 } else {
1992 ctx.provider
1994 .try_put_poison(&poison_key, ctx.procedure_id)
1995 .await
1996 .unwrap();
1997 Err(Error::retry_later_and_clean_poisons(MockError::new(
1998 StatusCode::Unexpected,
1999 )))
2000 }
2001 }
2002 .boxed()
2003 };
2004 let poison = ProcedureAdapter {
2005 data: "poison".to_string(),
2006 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2007 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
2008 exec_fn,
2009 rollback_fn: None,
2010 };
2011
2012 let dir = create_temp_dir("exceed_max_after_set_poison");
2013 let meta = poison.new_meta(ROOT_ID);
2014 let object_store = test_util::new_object_store(&dir);
2015 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2016 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store);
2017 runner.manager_ctx.start();
2018 runner.exponential_builder = ExponentialBuilder::default()
2019 .with_min_delay(Duration::from_millis(1))
2020 .with_max_times(3);
2021 let ctx = context_with_provider(
2023 meta.id,
2024 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
2025 );
2026 runner
2028 .manager_ctx
2029 .procedures
2030 .write()
2031 .unwrap()
2032 .insert(meta.id, runner.meta.clone());
2033 runner.execute_once_with_retry(&ctx).await;
2035 let err = meta.state().error().unwrap().clone();
2036 assert_matches!(&*err, Error::RetryTimesExceeded { .. });
2037
2038 let procedure_id = runner
2040 .manager_ctx
2041 .poison_manager
2042 .get_poison(&poison_key.to_string())
2043 .await
2044 .unwrap();
2045 assert_eq!(procedure_id, None);
2046 }
2047
2048 #[tokio::test]
2049 async fn test_execute_poisoned() {
2050 let mut times = 0;
2051 let poison_key = PoisonKey::new("table/1024");
2052 let moved_poison_key = poison_key.clone();
2053 let exec_fn = move |ctx: Context| {
2054 times += 1;
2055 let poison_key = moved_poison_key.clone();
2056 async move {
2057 if times == 1 {
2058 Ok(Status::executing(true))
2059 } else {
2060 ctx.provider
2062 .try_put_poison(&poison_key, ctx.procedure_id)
2063 .await
2064 .unwrap();
2065 Ok(Status::Poisoned {
2066 keys: PoisonKeys::new(vec![poison_key.clone()]),
2067 error: Error::external(MockError::new(StatusCode::Unexpected)),
2068 })
2069 }
2070 }
2071 .boxed()
2072 };
2073 let poison = ProcedureAdapter {
2074 data: "poison".to_string(),
2075 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2076 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
2077 exec_fn,
2078 rollback_fn: None,
2079 };
2080
2081 let dir = create_temp_dir("poison");
2082 let meta = poison.new_meta(ROOT_ID);
2083
2084 let object_store = test_util::new_object_store(&dir);
2085 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2086 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store.clone());
2087
2088 let ctx = context_with_provider(
2090 meta.id,
2091 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
2092 );
2093 runner
2095 .manager_ctx
2096 .procedures
2097 .write()
2098 .unwrap()
2099 .insert(meta.id, runner.meta.clone());
2100
2101 runner.manager_ctx.start();
2102 runner.execute_once(&ctx).await;
2103 let state = runner.meta.state();
2104 assert!(state.is_running(), "{state:?}");
2105
2106 runner.execute_once(&ctx).await;
2107 let state = runner.meta.state();
2108 assert!(state.is_poisoned(), "{state:?}");
2109 assert!(meta.state().is_poisoned());
2110 check_files(
2111 &object_store,
2112 &procedure_store,
2113 ctx.procedure_id,
2114 &["0000000000.step"],
2115 )
2116 .await;
2117
2118 let procedure_id = runner
2120 .manager_ctx
2121 .poison_manager
2122 .get_poison(&poison_key.to_string())
2123 .await
2124 .unwrap()
2125 .unwrap();
2126
2127 assert_eq!(procedure_id, ROOT_ID);
2129 }
2130
2131 fn test_procedure_with_dynamic_lock(
2132 shared_atomic_value: Arc<AtomicU64>,
2133 id: u64,
2134 ) -> (BoxedProcedure, Arc<ProcedureMeta>) {
2135 let exec_fn = move |ctx: Context| {
2136 let moved_shared_atomic_value = shared_atomic_value.clone();
2137 let moved_ctx = ctx.clone();
2138 async move {
2139 debug!("Acquiring write lock, id: {}", id);
2140 let key = StringKey::Exclusive("test_lock".to_string());
2141 let guard = moved_ctx.provider.acquire_lock(&key).await;
2142 debug!("Acquired write lock, id: {}", id);
2143 let millis = rand::rng().random_range(10..=50);
2144 tokio::time::sleep(Duration::from_millis(millis)).await;
2145 let value = moved_shared_atomic_value.load(Ordering::Relaxed);
2146 moved_shared_atomic_value.store(value + 1, Ordering::Relaxed);
2147 debug!("Dropping write lock, id: {}", id);
2148 drop(guard);
2149
2150 Ok(Status::done())
2151 }
2152 .boxed()
2153 };
2154
2155 let adapter = ProcedureAdapter {
2156 data: "dynamic_lock".to_string(),
2157 lock_key: LockKey::new_exclusive([]),
2158 poison_keys: PoisonKeys::new([]),
2159 exec_fn,
2160 rollback_fn: None,
2161 };
2162 let meta = adapter.new_meta(ROOT_ID);
2163
2164 (Box::new(adapter), meta)
2165 }
2166
2167 #[tokio::test(flavor = "multi_thread")]
2168 async fn test_execute_with_dynamic_lock() {
2169 common_telemetry::init_default_ut_logging();
2170 let shared_atomic_value = Arc::new(AtomicU64::new(0));
2171 let (procedure1, meta1) = test_procedure_with_dynamic_lock(shared_atomic_value.clone(), 1);
2172 let (procedure2, meta2) = test_procedure_with_dynamic_lock(shared_atomic_value.clone(), 2);
2173
2174 let dir = create_temp_dir("dynamic_lock");
2175 let object_store = test_util::new_object_store(&dir);
2176 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2177 let mut runner1 = new_runner(meta1.clone(), procedure1, procedure_store.clone());
2178 let mut runner2 = new_runner(meta2.clone(), procedure2, procedure_store.clone());
2179 let ctx1 = context_with_provider(
2180 meta1.id,
2181 runner1.manager_ctx.clone() as Arc<dyn ContextProvider>,
2182 );
2183 let ctx2 = context_with_provider(
2184 meta2.id,
2185 runner1.manager_ctx.clone() as Arc<dyn ContextProvider>,
2187 );
2188 let tasks = [runner1.execute_once(&ctx1), runner2.execute_once(&ctx2)];
2189 join_all(tasks).await;
2190 assert_eq!(shared_atomic_value.load(Ordering::Relaxed), 2);
2191 }
2192 #[tokio::test]
2193 async fn test_on_suspend_deadlock_detected_no_rollback() {
2194 let child_id = ProcedureId::random();
2197 let exec_fn = move |_| {
2198 async move {
2199 let child_exec_fn = |_| async { Ok(Status::done()) }.boxed();
2200 let child = ProcedureAdapter {
2201 data: "child".to_string(),
2202 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2203 poison_keys: PoisonKeys::default(),
2204 exec_fn: child_exec_fn,
2205 rollback_fn: None,
2206 };
2207 Ok(Status::Suspended {
2208 subprocedures: vec![ProcedureWithId {
2209 id: child_id,
2210 procedure: Box::new(child),
2211 context: Default::default(),
2212 }],
2213 persist: false,
2214 })
2215 }
2216 .boxed()
2217 };
2218 let parent = ProcedureAdapter {
2219 data: "parent".to_string(),
2220 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2221 poison_keys: PoisonKeys::default(),
2222 exec_fn,
2223 rollback_fn: None, };
2225
2226 let dir = create_temp_dir("deadlock_no_rollback");
2227 let meta = parent.new_meta(ROOT_ID);
2228 let ctx = context_without_provider(meta.id);
2229 let object_store = test_util::new_object_store(&dir);
2230 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2231 let mut runner = new_runner(meta.clone(), Box::new(parent), procedure_store);
2232 runner.manager_ctx.start();
2233
2234 runner.execute_once(&ctx).await;
2235 let state = runner.meta.state();
2236 assert!(state.is_failed(), "Expected Failed, got {state:?}");
2237 assert!(
2239 state.error().is_some(),
2240 "Failed state should contain an error"
2241 );
2242 assert!(
2244 !runner.manager_ctx.contains_procedure(child_id),
2245 "Child procedure should not be submitted when deadlock is detected"
2246 );
2247 }
2248
2249 #[tokio::test]
2250 async fn test_on_suspend_deadlock_detected_with_rollback() {
2251 let child_id = ProcedureId::random();
2254 let exec_fn = move |_| {
2255 async move {
2256 let child_exec_fn = |_| async { Ok(Status::done()) }.boxed();
2257 let child = ProcedureAdapter {
2258 data: "child".to_string(),
2259 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2260 poison_keys: PoisonKeys::default(),
2261 exec_fn: child_exec_fn,
2262 rollback_fn: None,
2263 };
2264 Ok(Status::Suspended {
2265 subprocedures: vec![ProcedureWithId {
2266 id: child_id,
2267 procedure: Box::new(child),
2268 context: Default::default(),
2269 }],
2270 persist: false,
2271 })
2272 }
2273 .boxed()
2274 };
2275 let rollback_fn = move |_| async move { Ok(()) }.boxed();
2276 let parent = ProcedureAdapter {
2277 data: "parent".to_string(),
2278 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2279 poison_keys: PoisonKeys::default(),
2280 exec_fn,
2281 rollback_fn: Some(Box::new(rollback_fn)), };
2283
2284 let dir = create_temp_dir("deadlock_with_rollback");
2285 let meta = parent.new_meta(ROOT_ID);
2286 let ctx = context_without_provider(meta.id);
2287 let object_store = test_util::new_object_store(&dir);
2288 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2289 let mut runner = new_runner(meta.clone(), Box::new(parent), procedure_store);
2290 runner.manager_ctx.start();
2291
2292 runner.execute_once(&ctx).await;
2293 let state = runner.meta.state();
2294 assert!(
2295 state.is_prepare_rollback(),
2296 "Expected PrepareRollback, got {state:?}"
2297 );
2298 match &state {
2300 ProcedureState::PrepareRollback { error } => {
2301 assert!(!error.to_string().is_empty(), "Error should not be empty");
2302 }
2303 _ => panic!("Expected PrepareRollback, got {state:?}"),
2304 }
2305 assert!(
2307 !runner.manager_ctx.contains_procedure(child_id),
2308 "Child procedure should not be submitted when deadlock is detected"
2309 );
2310 }
2311
2312 #[test]
2313 fn test_find_lock_conflicts() {
2314 use crate::procedure::StringKey;
2315
2316 let parent = [StringKey::Share("A".to_string())];
2318 let child = [StringKey::Share("A".to_string())];
2319 assert!(super::find_lock_conflicts(parent.iter(), child.iter()).is_empty());
2320
2321 let parent = [StringKey::Share("A".to_string())];
2323 let child = [StringKey::Exclusive("A".to_string())];
2324 assert_eq!(
2325 super::find_lock_conflicts(parent.iter(), child.iter()),
2326 vec!["A".to_string()]
2327 );
2328
2329 let parent = [StringKey::Exclusive("A".to_string())];
2331 let child = [StringKey::Share("A".to_string())];
2332 assert_eq!(
2333 super::find_lock_conflicts(parent.iter(), child.iter()),
2334 vec!["A".to_string()]
2335 );
2336
2337 let parent = [StringKey::Exclusive("A".to_string())];
2339 let child = [StringKey::Exclusive("A".to_string())];
2340 assert_eq!(
2341 super::find_lock_conflicts(parent.iter(), child.iter()),
2342 vec!["A".to_string()]
2343 );
2344
2345 let parent = [
2347 StringKey::Share("A".to_string()),
2348 StringKey::Exclusive("B".to_string()),
2349 ];
2350 let child = [
2351 StringKey::Exclusive("A".to_string()), StringKey::Share("B".to_string()), StringKey::Exclusive("C".to_string()), ];
2355 let mut conflicts = super::find_lock_conflicts(parent.iter(), child.iter());
2356 conflicts.sort();
2357 assert_eq!(conflicts, vec!["A".to_string(), "B".to_string()]);
2358 }
2359}