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, SubprocedureRecovery,
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 {
394 subprocedures,
395 recovery,
396 ..
397 } => {
398 let prev_state = self.meta.state();
399 if !matches!(prev_state, ProcedureState::Running) {
400 info!(
401 "Set Procedure {}-{} state to running, prev_state: {:?}",
402 self.procedure.type_name(),
403 self.meta.id,
404 prev_state
405 );
406 self.meta.set_state(ProcedureState::Running);
407 }
408 self.on_suspended(subprocedures, recovery).await;
409 }
410 Status::Done { output } => {
411 if let Err(e) = self.commit_procedure().await {
412 error!(e; "Failed to commit procedure: {}", self.meta.id);
413 self.set_state_and_record(ProcedureState::retrying(Arc::new(
414 e,
415 )));
416 return;
417 }
418
419 self.done(output);
420 }
421 Status::Poisoned { error, keys } => {
422 error!(
423 error;
424 "Procedure {}-{} is poisoned, keys: {:?}",
425 self.procedure.type_name(),
426 self.meta.id,
427 keys,
428 );
429 self.set_state_and_record(ProcedureState::poisoned(
430 keys,
431 Arc::new(error),
432 ));
433 }
434 }
435 }
436 Err(e) => {
437 error!(
438 e;
439 "Failed to execute procedure {}-{}, retry: {}, clean_poisons: {}",
440 self.procedure.type_name(),
441 self.meta.id,
442 e.is_retry_later(),
443 e.need_clean_poisons(),
444 );
445
446 if !self.running() {
448 self.set_state_and_record(ProcedureState::failed(Arc::new(
449 error::ManagerNotStartSnafu {}.build(),
450 )));
451 return;
452 }
453
454 if e.need_clean_poisons() {
455 if let Err(e) = self.clean_poisons().await {
456 error!(e; "Failed to clean poison for procedure: {}", self.meta.id);
457 self.set_state_and_record(ProcedureState::retrying(Arc::new(e)));
458 return;
459 }
460 debug!(
461 "Procedure {}-{} cleaned poisons",
462 self.procedure.type_name(),
463 self.meta.id,
464 );
465 }
466
467 if e.is_retry_later() {
468 self.set_state_and_record(ProcedureState::retrying(Arc::new(e)));
469 return;
470 }
471
472 if self.procedure.rollback_supported() {
473 self.meta
474 .set_state(ProcedureState::prepare_rollback(Arc::new(e)));
475 } else {
476 self.set_state_and_record(ProcedureState::failed(Arc::new(e)));
477 }
478 }
479 }
480 }
481 ProcedureState::PrepareRollback { error } => self.prepare_rollback(error).await,
482 ProcedureState::RollingBack { error } => self.rollback(ctx, error).await,
483 ProcedureState::Failed { .. }
484 | ProcedureState::Done { .. }
485 | ProcedureState::Poisoned { .. } => (),
486 }
487 }
488
489 fn submit_subprocedure(
491 &self,
492 procedure_id: ProcedureId,
493 procedure_state: ProcedureState,
494 procedure: BoxedProcedure,
495 recovery: SubprocedureRecovery,
496 ) -> (ChildSubmissionOutcome, Option<ProcedureEvent>) {
497 if !self.running() {
498 warn!(
499 "ProcedureManager is not running, skip submitting subprocedure {}-{}",
500 procedure.type_name(),
501 procedure_id
502 );
503 return (ChildSubmissionOutcome::ManagerStopped, None);
504 }
505
506 if self.manager_ctx.contains_procedure(procedure_id) {
507 return (ChildSubmissionOutcome::AlreadyAccepted, None);
509 }
510
511 let step = 0;
512
513 let meta = ProcedureMeta::new(
514 procedure_id,
515 procedure_state,
516 Some(self.meta.id),
517 self.meta.context.clone(),
518 procedure.lock_key(),
519 procedure.poison_keys(),
520 procedure.type_name(),
521 )
522 .with_subprocedure_recovery(recovery);
523 let meta = Arc::new(meta);
524 let runner = Runner {
525 meta: meta.clone(),
526 procedure,
527 manager_ctx: self.manager_ctx.clone(),
528 step,
529 exponential_builder: self.exponential_builder,
530 store: self.store.clone(),
531 rolling_back: false,
532 event_recorder: self.event_recorder.clone(),
533 execute_retry_attempt: 0,
534 rollback_retry_attempt: 0,
535 };
536
537 assert!(
541 self.manager_ctx.try_insert_procedure(meta),
542 "Procedure {}-{} submit an existing procedure {}-{}",
543 self.procedure.type_name(),
544 self.meta.id,
545 runner.procedure.type_name(),
546 procedure_id,
547 );
548
549 let submitted_event = runner.build_event(EventTrigger::Submitted);
550 let parent_id = self.meta.id;
551
552 let tracing_context = TracingContext::from_current_span();
553 if !self.manager_ctx.spawn_runner_task(procedure_id, || {
554 common_runtime::spawn_global(async move {
555 let span = tracing_context.attach(tracing::info_span!(
556 "LocalManager::submit_subprocedure",
557 procedure_name = %runner.meta.type_name,
558 procedure_id = %runner.meta.id,
559 parent_id = %parent_id,
560 ));
561 runner.run().trace(span).await
565 })
566 }) {
567 self.manager_ctx.remove_procedure(procedure_id);
568 return (ChildSubmissionOutcome::SpawnFailed, None);
569 }
570
571 self.meta.push_child(procedure_id);
573 (ChildSubmissionOutcome::Accepted, submitted_event)
574 }
575
576 async fn wait_on_err(&mut self, d: Duration, i: u64) {
578 info!(
579 "Procedure {}-{} retry for the {} times after {} millis",
580 self.procedure.type_name(),
581 self.meta.id,
582 i,
583 d.as_millis(),
584 );
585 time::sleep(d).await;
586 }
587
588 async fn on_suspended(
589 &mut self,
590 subprocedures: Vec<ProcedureWithId>,
591 recovery: SubprocedureRecovery,
592 ) {
593 let has_child = !subprocedures.is_empty();
594
595 for sub in &subprocedures {
600 let conflicting = find_lock_conflicts(
601 self.meta.lock_key.keys_to_lock(),
602 sub.procedure.lock_key().keys_to_lock(),
603 );
604 if !conflicting.is_empty() {
605 let err_msg = format!(
606 "Deadlock prevented: subprocedure {}-{} shares conflicting lock key(s) {:?} \
607 with parent {}-{}. Parent holds these locks and would wait for child \
608 completion, but child cannot acquire them.",
609 sub.procedure.type_name(),
610 sub.id,
611 conflicting,
612 self.procedure.type_name(),
613 self.meta.id,
614 );
615 error!("{}", err_msg);
616 let err = Arc::new(Error::external(PlainError::new(
617 err_msg,
618 StatusCode::Internal,
619 )));
620 if self.procedure.rollback_supported() {
621 self.meta.set_state(ProcedureState::prepare_rollback(err));
622 } else {
623 self.set_state_and_record(ProcedureState::failed(err));
624 }
625 return;
626 }
627 }
628
629 for subprocedure in subprocedures {
630 info!(
631 "Procedure {}-{} submit subprocedure {}-{}",
632 self.procedure.type_name(),
633 self.meta.id,
634 subprocedure.procedure.type_name(),
635 subprocedure.id,
636 );
637
638 let child_id = subprocedure.id;
639 let (outcome, submitted_event) = self.submit_subprocedure(
640 subprocedure.id,
641 ProcedureState::Running,
642 subprocedure.procedure,
643 recovery,
644 );
645 if let Some(event) = submitted_event
646 && let Some(recorder) = self.event_recorder.as_ref()
647 {
648 recorder.record(Box::new(event));
649 }
650 self.record_event(EventTrigger::ChildSubmitted {
651 procedure_id: child_id,
652 outcome,
653 });
654 }
655
656 info!(
657 "Procedure {}-{} is waiting for subprocedures",
658 self.procedure.type_name(),
659 self.meta.id,
660 );
661
662 if has_child {
664 self.meta.child_notify.notified().await;
665
666 info!(
667 "Procedure {}-{} is waked up",
668 self.procedure.type_name(),
669 self.meta.id,
670 );
671 }
672 }
673
674 async fn persist_procedure(&mut self) -> Result<()> {
675 let type_name = self.procedure.type_name().to_string();
676 let data = self.procedure.dump()?;
677
678 self.store
679 .store_procedure_message(
680 self.meta.id,
681 &ProcedureMessage {
682 type_name,
683 data,
684 parent_id: self.meta.parent_id,
685 subprocedure_recovery: self.meta.subprocedure_recovery,
686 step: self.step,
687 error: None,
688 context: self.meta.context.clone(),
689 },
690 )
691 .await
692 .map_err(|e| {
693 error!(
694 e; "Failed to persist 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 commit_procedure(&mut self) -> Result<()> {
705 self.store
706 .commit_procedure(self.meta.id, self.step)
707 .await
708 .map_err(|e| {
709 error!(
710 e; "Failed to commit procedure {}-{}",
711 self.procedure.type_name(),
712 self.meta.id
713 );
714 e
715 })?;
716 self.step += 1;
717 Ok(())
718 }
719
720 async fn write_rollback_procedure_state(&mut self, error: String) -> Result<()> {
721 let type_name = self.procedure.type_name().to_string();
723 let data = self.procedure.dump()?;
724 let message = ProcedureMessage {
725 type_name,
726 data,
727 parent_id: self.meta.parent_id,
728 subprocedure_recovery: self.meta.subprocedure_recovery,
729 step: self.step,
730 error: Some(error),
731 context: self.meta.context.clone(),
732 };
733 self.store
734 .rollback_procedure(self.meta.id, message)
735 .await
736 .map_err(|e| {
737 error!(
738 e; "Failed to write rollback key for procedure {}-{}",
739 self.procedure.type_name(),
740 self.meta.id
741 );
742 e
743 })?;
744 self.step += 1;
745 Ok(())
746 }
747
748 fn done(&mut self, output: Option<Output>) {
749 info!(
751 "Procedure {}-{} done",
752 self.procedure.type_name(),
753 self.meta.id,
754 );
755
756 self.set_state_and_record(ProcedureState::Done { output });
758 }
759
760 fn set_state_and_record(&mut self, state: ProcedureState) {
762 let trigger = match &state {
763 ProcedureState::Retrying { .. } => {
764 self.execute_retry_attempt += 1;
765 Some(EventTrigger::Retrying {
766 phase: RetryPhase::Execute,
767 attempt: self.execute_retry_attempt,
768 })
769 }
770 ProcedureState::RollingBack { .. } => {
771 if self.rolling_back {
772 self.rollback_retry_attempt += 1;
773 Some(EventTrigger::Retrying {
774 phase: RetryPhase::Rollback,
775 attempt: self.rollback_retry_attempt,
776 })
777 } else {
778 self.rolling_back = true;
779 Some(EventTrigger::RollingBack)
780 }
781 }
782 ProcedureState::Done { .. } => Some(EventTrigger::Succeeded),
783 ProcedureState::Failed { .. } => Some(EventTrigger::Failed),
784 ProcedureState::Poisoned { .. } => Some(EventTrigger::Poisoned),
785 ProcedureState::Running => None,
786 ProcedureState::PrepareRollback { .. } => None,
789 };
790 self.meta.set_state(state);
791 if let Some(trigger) = trigger {
792 self.record_event(trigger);
793 }
794 }
795
796 pub(crate) fn build_event(&self, trigger: EventTrigger) -> Option<ProcedureEvent> {
798 let recorder = self.event_recorder.as_ref()?;
799 let state = self.meta.state();
800 let context = EventContext {
801 procedure_id: self.meta.id,
802 lifecycle_state: &state,
803 trigger: trigger.clone(),
804 event_type_filter: recorder.event_type_filter(),
805 event_context: self.meta.context.event_context.as_ref(),
806 };
807 self.procedure.event(&context).map(|event| {
808 ProcedureEvent::new_with_context(
809 self.meta.id,
810 event,
811 state,
812 trigger,
813 self.meta.context.clone(),
814 )
815 })
816 }
817
818 pub(crate) fn record_event(&self, trigger: EventTrigger) {
821 if let Some(event) = self.build_event(trigger)
822 && let Some(recorder) = self.event_recorder.as_ref()
823 {
824 recorder.record(Box::new(event));
825 }
826 }
827}
828
829impl Drop for Runner {
830 fn drop(&mut self) {
831 self.manager_ctx.remove_runner_task(self.meta.id);
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use std::assert_matches;
838 use std::sync::Arc;
839 use std::sync::atomic::{AtomicU64, Ordering};
840
841 use async_trait::async_trait;
842 use common_error::ext::{ErrorExt, PlainError};
843 use common_error::mock::MockError;
844 use common_error::status_code::StatusCode;
845 use common_test_util::temp_dir::create_temp_dir;
846 use futures::future::join_all;
847 use futures_util::FutureExt;
848 use futures_util::future::BoxFuture;
849 use object_store::{EntryMode, ObjectStore};
850 use tokio::sync::mpsc;
851 use tokio::sync::watch::Receiver;
852
853 use super::*;
854 use crate::local::{DynamicKeyLockGuard, test_util};
855 use crate::procedure::PoisonKeys;
856 use crate::store::proc_path;
857 use crate::test_util::InMemoryPoisonStore;
858 use crate::{ContextProvider, Error, LockKey, PoisonKey, Procedure};
859
860 const ROOT_ID: &str = "9f805a1f-05f7-490c-9f91-bd56e3cc54c1";
861
862 fn new_runner(
863 meta: ProcedureMetaRef,
864 procedure: BoxedProcedure,
865 store: Arc<ProcedureStore>,
866 ) -> Runner {
867 Runner {
868 meta,
869 procedure,
870 manager_ctx: Arc::new(ManagerContext::new(
871 Arc::new(InMemoryPoisonStore::default()),
872 )),
873 step: 0,
874 exponential_builder: ExponentialBuilder::default(),
875 store,
876 rolling_back: false,
877 event_recorder: None,
878 execute_retry_attempt: 0,
879 rollback_retry_attempt: 0,
880 }
881 }
882
883 async fn check_files(
884 object_store: &ObjectStore,
885 procedure_store: &ProcedureStore,
886 procedure_id: ProcedureId,
887 files: &[&str],
888 ) {
889 let dir = proc_path!(procedure_store, "{procedure_id}/");
890 let lister = object_store.list(&dir).await.unwrap();
891 let mut files_in_dir: Vec<_> = lister
892 .into_iter()
893 .filter(|x| x.metadata().mode() == EntryMode::FILE)
894 .map(|de| de.name().to_string())
895 .collect();
896 files_in_dir.sort_unstable();
897 assert_eq!(files, files_in_dir);
898 }
899
900 fn context_with_provider(
901 procedure_id: ProcedureId,
902 provider: Arc<dyn ContextProvider>,
903 ) -> Context {
904 Context {
905 procedure_id,
906 provider,
907 event_context: None,
908 }
909 }
910
911 fn context_without_provider(procedure_id: ProcedureId) -> Context {
912 struct MockProvider;
913
914 #[async_trait]
915 impl ContextProvider for MockProvider {
916 async fn procedure_state(
917 &self,
918 _procedure_id: ProcedureId,
919 ) -> Result<Option<ProcedureState>> {
920 unimplemented!()
921 }
922
923 async fn procedure_state_receiver(
924 &self,
925 _procedure_id: ProcedureId,
926 ) -> Result<Option<Receiver<ProcedureState>>> {
927 unimplemented!()
928 }
929
930 async fn try_put_poison(
931 &self,
932 _key: &PoisonKey,
933 _procedure_id: ProcedureId,
934 ) -> Result<()> {
935 unimplemented!()
936 }
937
938 async fn acquire_lock(&self, _key: &StringKey) -> DynamicKeyLockGuard {
939 unimplemented!()
940 }
941 }
942
943 Context {
944 procedure_id,
945 provider: Arc::new(MockProvider),
946 event_context: None,
947 }
948 }
949
950 type RollbackFn = Box<dyn FnMut(Context) -> BoxFuture<'static, Result<()>> + Send>;
951
952 struct ProcedureAdapter<F> {
953 data: String,
954 lock_key: LockKey,
955 poison_keys: PoisonKeys,
956 exec_fn: F,
957 rollback_fn: Option<RollbackFn>,
958 }
959
960 impl<F> ProcedureAdapter<F> {
961 fn new_meta(&self, uuid: &str) -> ProcedureMetaRef {
962 let mut meta = test_util::procedure_meta_for_test();
963 meta.id = ProcedureId::parse_str(uuid).unwrap();
964 meta.lock_key = self.lock_key.clone();
965 meta.poison_keys = self.poison_keys.clone();
966
967 Arc::new(meta)
968 }
969 }
970
971 #[async_trait]
972 impl<F> Procedure for ProcedureAdapter<F>
973 where
974 F: FnMut(Context) -> BoxFuture<'static, Result<Status>> + Send + Sync,
975 {
976 fn type_name(&self) -> &str {
977 "ProcedureAdapter"
978 }
979
980 async fn execute(&mut self, ctx: &Context) -> Result<Status> {
981 let f = (self.exec_fn)(ctx.clone());
982 f.await
983 }
984
985 async fn rollback(&mut self, ctx: &Context) -> Result<()> {
986 if let Some(f) = &mut self.rollback_fn {
987 return (f)(ctx.clone()).await;
988 }
989 Ok(())
990 }
991
992 fn rollback_supported(&self) -> bool {
993 self.rollback_fn.is_some()
994 }
995
996 fn dump(&self) -> Result<String> {
997 Ok(self.data.clone())
998 }
999
1000 fn lock_key(&self) -> LockKey {
1001 self.lock_key.clone()
1002 }
1003
1004 fn poison_keys(&self) -> PoisonKeys {
1005 self.poison_keys.clone()
1006 }
1007 }
1008
1009 async fn execute_once_normal(persist: bool, first_files: &[&str], second_files: &[&str]) {
1010 let mut times = 0;
1011 let exec_fn = move |_| {
1012 times += 1;
1013 async move {
1014 if times == 1 {
1015 Ok(Status::executing(persist))
1016 } else {
1017 Ok(Status::done())
1018 }
1019 }
1020 .boxed()
1021 };
1022 let normal = ProcedureAdapter {
1023 data: "normal".to_string(),
1024 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1025 poison_keys: PoisonKeys::default(),
1026 exec_fn,
1027 rollback_fn: None,
1028 };
1029
1030 let dir = create_temp_dir("normal");
1031 let meta = normal.new_meta(ROOT_ID);
1032 let ctx = context_without_provider(meta.id);
1033 let object_store = test_util::new_object_store(&dir);
1034 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1035 let mut runner = new_runner(meta, Box::new(normal), procedure_store.clone());
1036 runner.manager_ctx.start();
1037
1038 runner.execute_once(&ctx).await;
1039 let state = runner.meta.state();
1040 assert!(state.is_running(), "{state:?}");
1041 check_files(
1042 &object_store,
1043 &procedure_store,
1044 ctx.procedure_id,
1045 first_files,
1046 )
1047 .await;
1048
1049 runner.execute_once(&ctx).await;
1050 let state = runner.meta.state();
1051 assert!(state.is_done(), "{state:?}");
1052 check_files(
1053 &object_store,
1054 &procedure_store,
1055 ctx.procedure_id,
1056 second_files,
1057 )
1058 .await;
1059 }
1060
1061 #[tokio::test]
1062 async fn test_execute_once_normal() {
1063 execute_once_normal(
1064 true,
1065 &["0000000000.step"],
1066 &["0000000000.step", "0000000001.commit"],
1067 )
1068 .await;
1069 }
1070
1071 #[tokio::test]
1072 async fn test_execute_once_normal_skip_persist() {
1073 execute_once_normal(false, &[], &["0000000000.commit"]).await;
1074 }
1075
1076 #[tokio::test]
1077 async fn test_on_suspend_empty() {
1078 let exec_fn = move |_| {
1079 async move {
1080 Ok(Status::Suspended {
1081 recovery: SubprocedureRecovery::Discard,
1082 subprocedures: Vec::new(),
1083 persist: false,
1084 })
1085 }
1086 .boxed()
1087 };
1088 let suspend = ProcedureAdapter {
1089 data: "suspend".to_string(),
1090 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1091 poison_keys: PoisonKeys::default(),
1092 exec_fn,
1093 rollback_fn: None,
1094 };
1095
1096 let dir = create_temp_dir("suspend");
1097 let meta = suspend.new_meta(ROOT_ID);
1098 let ctx = context_without_provider(meta.id);
1099 let object_store = test_util::new_object_store(&dir);
1100 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1101 let mut runner = new_runner(meta, Box::new(suspend), procedure_store);
1102 runner.manager_ctx.start();
1103
1104 runner.execute_once(&ctx).await;
1105 let state = runner.meta.state();
1106 assert!(state.is_running(), "{state:?}");
1107 }
1108
1109 fn new_child_procedure(procedure_id: ProcedureId, keys: &[&str]) -> ProcedureWithId {
1110 let mut times = 0;
1111 let exec_fn = move |_| {
1112 times += 1;
1113 async move {
1114 if times == 1 {
1115 time::sleep(Duration::from_millis(200)).await;
1116 Ok(Status::executing(true))
1117 } else {
1118 Ok(Status::done())
1119 }
1120 }
1121 .boxed()
1122 };
1123 let child = ProcedureAdapter {
1124 data: "child".to_string(),
1125 lock_key: LockKey::new_exclusive(keys.iter().map(|k| k.to_string())),
1126 poison_keys: PoisonKeys::default(),
1127 exec_fn,
1128 rollback_fn: None,
1129 };
1130
1131 ProcedureWithId {
1132 id: procedure_id,
1133 procedure: Box::new(child),
1134 context: Default::default(),
1135 }
1136 }
1137
1138 #[tokio::test]
1139 async fn test_on_suspend_by_subprocedures() {
1140 let mut times = 0;
1141 let children_ids = [ProcedureId::random(), ProcedureId::random()];
1142 let keys = [
1143 &[
1144 "catalog.schema.table.region-0",
1145 "catalog.schema.table.region-1",
1146 ],
1147 &[
1148 "catalog.schema.table.region-2",
1149 "catalog.schema.table.region-3",
1150 ],
1151 ];
1152
1153 let exec_fn = move |ctx: Context| {
1154 times += 1;
1155 async move {
1156 if times == 1 {
1157 Ok(Status::Suspended {
1159 recovery: SubprocedureRecovery::Discard,
1160 subprocedures: children_ids
1161 .into_iter()
1162 .zip(keys)
1163 .map(|(id, key_slice)| new_child_procedure(id, key_slice))
1164 .collect(),
1165 persist: true,
1166 })
1167 } else {
1168 let mut all_child_done = true;
1170 for id in children_ids {
1171 let is_not_done = ctx
1172 .provider
1173 .procedure_state(id)
1174 .await
1175 .unwrap()
1176 .map(|s| !s.is_done())
1177 .unwrap_or(true);
1178 if is_not_done {
1179 all_child_done = false;
1180 }
1181 }
1182 if all_child_done {
1183 Ok(Status::done())
1184 } else {
1185 Ok(Status::Suspended {
1187 recovery: SubprocedureRecovery::Discard,
1188 subprocedures: Vec::new(),
1189 persist: false,
1190 })
1191 }
1192 }
1193 }
1194 .boxed()
1195 };
1196 let parent = ProcedureAdapter {
1197 data: "parent".to_string(),
1198 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1199 poison_keys: PoisonKeys::default(),
1200 exec_fn,
1201 rollback_fn: None,
1202 };
1203
1204 let dir = create_temp_dir("parent");
1205 let meta = parent.new_meta(ROOT_ID);
1206 let procedure_id = meta.id;
1207
1208 let object_store = test_util::new_object_store(&dir);
1209 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1210 let mut runner = new_runner(meta.clone(), Box::new(parent), procedure_store.clone());
1211 let poison_manager = Arc::new(InMemoryPoisonStore::default());
1212 let manager_ctx = Arc::new(ManagerContext::new(poison_manager));
1213 manager_ctx.start();
1214 assert!(manager_ctx.try_insert_procedure(meta));
1216 runner.manager_ctx = manager_ctx.clone();
1218
1219 runner.run().await;
1220 assert!(manager_ctx.key_lock.is_empty());
1221
1222 for child_id in children_ids {
1224 let state = manager_ctx.state(child_id).unwrap();
1225 assert!(state.is_done(), "{state:?}");
1226 }
1227 let state = manager_ctx.state(procedure_id).unwrap();
1228 assert!(state.is_done(), "{state:?}");
1229 check_files(&object_store, &procedure_store, procedure_id, &[]).await;
1231
1232 tokio::time::sleep(Duration::from_millis(5)).await;
1233 manager_ctx.remove_outdated_meta(Duration::from_millis(1));
1235 assert!(manager_ctx.state(procedure_id).is_none());
1236 assert!(manager_ctx.finished_procedures.lock().unwrap().is_empty());
1237 for child_id in children_ids {
1238 assert!(manager_ctx.state(child_id).is_none());
1239 }
1240 }
1241
1242 #[tokio::test]
1243 async fn test_running_is_stopped() {
1244 let exec_fn = move |_| async move { Ok(Status::executing(true)) }.boxed();
1245 let normal = ProcedureAdapter {
1246 data: "normal".to_string(),
1247 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1248 poison_keys: PoisonKeys::default(),
1249 exec_fn,
1250 rollback_fn: None,
1251 };
1252
1253 let dir = create_temp_dir("test_running_is_stopped");
1254 let meta = normal.new_meta(ROOT_ID);
1255 let ctx = context_without_provider(meta.id);
1256 let object_store = test_util::new_object_store(&dir);
1257 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1258 let mut runner = new_runner(meta, Box::new(normal), procedure_store.clone());
1259 runner.manager_ctx.start();
1260
1261 runner.execute_once(&ctx).await;
1262 let state = runner.meta.state();
1263 assert!(state.is_running(), "{state:?}");
1264 check_files(
1265 &object_store,
1266 &procedure_store,
1267 ctx.procedure_id,
1268 &["0000000000.step"],
1269 )
1270 .await;
1271
1272 runner.manager_ctx.stop();
1273 runner.execute_once(&ctx).await;
1274 let state = runner.meta.state();
1275 assert!(state.is_failed(), "{state:?}");
1276 check_files(
1278 &object_store,
1279 &procedure_store,
1280 ctx.procedure_id,
1281 &["0000000000.step"],
1282 )
1283 .await;
1284 }
1285
1286 #[tokio::test]
1287 async fn test_running_is_stopped_on_error() {
1288 let exec_fn =
1289 |_| async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }.boxed();
1290 let normal = ProcedureAdapter {
1291 data: "fail".to_string(),
1292 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1293 poison_keys: PoisonKeys::default(),
1294 exec_fn,
1295 rollback_fn: None,
1296 };
1297
1298 let dir = create_temp_dir("test_running_is_stopped_on_error");
1299 let meta = normal.new_meta(ROOT_ID);
1300 let ctx = context_without_provider(meta.id);
1301 let object_store = test_util::new_object_store(&dir);
1302 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1303 let mut runner = new_runner(meta, Box::new(normal), procedure_store.clone());
1304 runner.manager_ctx.stop();
1305
1306 runner.execute_once(&ctx).await;
1307 let state = runner.meta.state();
1308 assert!(state.is_failed(), "{state:?}");
1309 check_files(&object_store, &procedure_store, ctx.procedure_id, &[]).await;
1311 }
1312
1313 #[tokio::test]
1314 async fn test_execute_on_error() {
1315 let exec_fn =
1316 |_| async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }.boxed();
1317 let fail = ProcedureAdapter {
1318 data: "fail".to_string(),
1319 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1320 poison_keys: PoisonKeys::default(),
1321 exec_fn,
1322 rollback_fn: None,
1323 };
1324
1325 let dir = create_temp_dir("fail");
1326 let meta = fail.new_meta(ROOT_ID);
1327 let ctx = context_without_provider(meta.id);
1328 let object_store = test_util::new_object_store(&dir);
1329 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1330 let mut runner = new_runner(meta.clone(), Box::new(fail), procedure_store.clone());
1331 runner.manager_ctx.start();
1332
1333 runner.execute_once(&ctx).await;
1334 let state = runner.meta.state();
1335 assert!(state.is_failed(), "{state:?}");
1336 check_files(&object_store, &procedure_store, ctx.procedure_id, &[]).await;
1337 }
1338
1339 #[tokio::test]
1340 async fn test_execute_with_rollback_on_error() {
1341 let exec_fn =
1342 |_| async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }.boxed();
1343 let rollback_fn = move |_| async move { Ok(()) }.boxed();
1344 let fail = ProcedureAdapter {
1345 data: "fail".to_string(),
1346 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1347 poison_keys: PoisonKeys::default(),
1348 exec_fn,
1349 rollback_fn: Some(Box::new(rollback_fn)),
1350 };
1351
1352 let dir = create_temp_dir("fail");
1353 let meta = fail.new_meta(ROOT_ID);
1354 let ctx = context_without_provider(meta.id);
1355 let object_store = test_util::new_object_store(&dir);
1356 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1357 let mut runner = new_runner(meta.clone(), Box::new(fail), procedure_store.clone());
1358 runner.manager_ctx.start();
1359
1360 runner.execute_once(&ctx).await;
1361 let state = runner.meta.state();
1362 assert!(state.is_prepare_rollback(), "{state:?}");
1363
1364 runner.execute_once(&ctx).await;
1365 let state = runner.meta.state();
1366 assert!(state.is_rolling_back(), "{state:?}");
1367
1368 runner.execute_once(&ctx).await;
1369 let state = runner.meta.state();
1370 assert!(state.is_failed(), "{state:?}");
1371 check_files(
1372 &object_store,
1373 &procedure_store,
1374 ctx.procedure_id,
1375 &["0000000000.rollback"],
1376 )
1377 .await;
1378 }
1379
1380 #[tokio::test]
1381 async fn test_execute_on_retry_later_error() {
1382 let mut times = 0;
1383
1384 let exec_fn = move |_| {
1385 times += 1;
1386 async move {
1387 if times == 1 {
1388 Err(Error::retry_later(MockError::new(StatusCode::Unexpected)))
1389 } else if times == 2 {
1390 Ok(Status::executing(false))
1391 } else {
1392 Ok(Status::done())
1393 }
1394 }
1395 .boxed()
1396 };
1397
1398 let retry_later = ProcedureAdapter {
1399 data: "retry_later".to_string(),
1400 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1401 poison_keys: PoisonKeys::default(),
1402 exec_fn,
1403 rollback_fn: None,
1404 };
1405
1406 let dir = create_temp_dir("retry_later");
1407 let meta = retry_later.new_meta(ROOT_ID);
1408 let ctx = context_without_provider(meta.id);
1409 let object_store = test_util::new_object_store(&dir);
1410 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1411 let mut runner = new_runner(meta.clone(), Box::new(retry_later), procedure_store.clone());
1412 runner.manager_ctx.start();
1413 runner.execute_once(&ctx).await;
1414 let state = runner.meta.state();
1415 assert!(state.is_retrying(), "{state:?}");
1416
1417 runner.execute_once(&ctx).await;
1418 let state = runner.meta.state();
1419 assert!(state.is_running(), "{state:?}");
1420
1421 runner.execute_once(&ctx).await;
1422 let state = runner.meta.state();
1423 assert!(state.is_done(), "{state:?}");
1424 assert!(meta.state().is_done());
1425 check_files(
1426 &object_store,
1427 &procedure_store,
1428 ctx.procedure_id,
1429 &["0000000000.commit"],
1430 )
1431 .await;
1432 }
1433
1434 #[tokio::test]
1435 async fn test_retrying_state_visible_in_context_on_retry() {
1436 let retrying_states = Arc::new(std::sync::Mutex::new(Vec::new()));
1437 let captured = retrying_states.clone();
1438 let mut times = 0;
1439
1440 let exec_fn = move |ctx: Context| {
1441 times += 1;
1442 let captured = captured.clone();
1443 async move {
1444 let is_retrying = ctx.is_retrying().await;
1445 captured.lock().unwrap().push(is_retrying);
1446 if times == 1 {
1447 Err(Error::retry_later(MockError::new(StatusCode::Unexpected)))
1448 } else {
1449 Ok(Status::done())
1450 }
1451 }
1452 .boxed()
1453 };
1454
1455 let procedure = ProcedureAdapter {
1456 data: "retrying_state".to_string(),
1457 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1458 poison_keys: PoisonKeys::default(),
1459 exec_fn,
1460 rollback_fn: None,
1461 };
1462
1463 let dir = create_temp_dir("retrying_state");
1464 let meta = procedure.new_meta(ROOT_ID);
1465 let object_store = test_util::new_object_store(&dir);
1466 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store));
1467 let mut runner = new_runner(meta.clone(), Box::new(procedure), procedure_store);
1468 let ctx = context_with_provider(
1469 meta.id,
1470 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1471 );
1472
1473 runner
1474 .manager_ctx
1475 .procedures
1476 .write()
1477 .unwrap()
1478 .insert(meta.id, runner.meta.clone());
1479 runner.manager_ctx.start();
1480
1481 runner.execute_once(&ctx).await;
1482 runner.execute_once(&ctx).await;
1483
1484 let states = retrying_states.lock().unwrap().clone();
1485 assert_eq!(states, vec![Some(false), Some(true)]);
1486 }
1487
1488 #[tokio::test(flavor = "multi_thread")]
1489 async fn test_execute_on_retry_later_error_with_child() {
1490 common_telemetry::init_default_ut_logging();
1491 let mut times = 0;
1492 let child_id = ProcedureId::random();
1493
1494 let exec_fn = move |_| {
1495 times += 1;
1496 async move {
1497 debug!("times: {}", times);
1498 if times == 1 {
1499 Err(Error::retry_later(MockError::new(StatusCode::Unexpected)))
1500 } else if times == 2 {
1501 let exec_fn = |_| {
1502 async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }
1503 .boxed()
1504 };
1505 let fail = ProcedureAdapter {
1506 data: "fail".to_string(),
1507 lock_key: LockKey::single_exclusive("catalog.schema.table.region-0"),
1508 poison_keys: PoisonKeys::default(),
1509 exec_fn,
1510 rollback_fn: None,
1511 };
1512
1513 Ok(Status::Suspended {
1514 recovery: SubprocedureRecovery::Discard,
1515 subprocedures: vec![ProcedureWithId {
1516 id: child_id,
1517 procedure: Box::new(fail),
1518 context: Default::default(),
1519 }],
1520 persist: true,
1521 })
1522 } else {
1523 Ok(Status::done())
1524 }
1525 }
1526 .boxed()
1527 };
1528
1529 let retry_later = ProcedureAdapter {
1530 data: "retry_later".to_string(),
1531 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1532 poison_keys: PoisonKeys::default(),
1533 exec_fn,
1534 rollback_fn: None,
1535 };
1536
1537 let dir = create_temp_dir("retry_later");
1538 let meta = retry_later.new_meta(ROOT_ID);
1539 let ctx = context_without_provider(meta.id);
1540 let object_store = test_util::new_object_store(&dir);
1541 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1542 let mut runner = new_runner(meta.clone(), Box::new(retry_later), procedure_store.clone());
1543 runner.manager_ctx.start();
1544 debug!("execute_once 1");
1545 runner.execute_once(&ctx).await;
1546 let state = runner.meta.state();
1547 assert!(state.is_retrying(), "{state:?}");
1548
1549 let moved_meta = meta.clone();
1550 tokio::spawn(async move {
1551 moved_meta.child_notify.notify_one();
1552 });
1553 runner.execute_once(&ctx).await;
1554 let state = runner.meta.state();
1555 assert!(state.is_running(), "{state:?}");
1556
1557 runner.execute_once(&ctx).await;
1558 let state = runner.meta.state();
1559 assert!(state.is_done(), "{state:?}");
1560 assert!(meta.state().is_done());
1561 check_files(
1562 &object_store,
1563 &procedure_store,
1564 ctx.procedure_id,
1565 &["0000000000.step", "0000000001.commit"],
1566 )
1567 .await;
1568 }
1569
1570 #[tokio::test]
1571 async fn test_execute_exceed_max_retry_later() {
1572 let exec_fn =
1573 |_| async { Err(Error::retry_later(MockError::new(StatusCode::Unexpected))) }.boxed();
1574
1575 let exceed_max_retry_later = ProcedureAdapter {
1576 data: "exceed_max_retry_later".to_string(),
1577 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1578 poison_keys: PoisonKeys::default(),
1579 exec_fn,
1580 rollback_fn: None,
1581 };
1582
1583 let dir = create_temp_dir("exceed_max_retry_later");
1584 let meta = exceed_max_retry_later.new_meta(ROOT_ID);
1585 let object_store = test_util::new_object_store(&dir);
1586 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1587 let mut runner = new_runner(
1588 meta.clone(),
1589 Box::new(exceed_max_retry_later),
1590 procedure_store,
1591 );
1592 runner.manager_ctx.start();
1593
1594 runner.exponential_builder = ExponentialBuilder::default()
1595 .with_min_delay(Duration::from_millis(1))
1596 .with_max_times(3);
1597
1598 runner.execute_procedure_in_loop().await;
1600 let err = meta.state().error().unwrap().to_string();
1601 assert!(err.contains("Procedure retry exceeded max times"));
1602 }
1603
1604 #[tokio::test]
1605 async fn test_rollback_exceed_max_retry_later() {
1606 let exec_fn =
1607 |_| async { Err(Error::retry_later(MockError::new(StatusCode::Unexpected))) }.boxed();
1608 let rollback_fn = move |_| {
1609 async move { Err(Error::retry_later(MockError::new(StatusCode::Unexpected))) }.boxed()
1610 };
1611 let exceed_max_retry_later = ProcedureAdapter {
1612 data: "exceed_max_rollback".to_string(),
1613 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1614 poison_keys: PoisonKeys::default(),
1615 exec_fn,
1616 rollback_fn: Some(Box::new(rollback_fn)),
1617 };
1618
1619 let dir = create_temp_dir("exceed_max_rollback");
1620 let meta = exceed_max_retry_later.new_meta(ROOT_ID);
1621 let object_store = test_util::new_object_store(&dir);
1622 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1623 let mut runner = new_runner(
1624 meta.clone(),
1625 Box::new(exceed_max_retry_later),
1626 procedure_store,
1627 );
1628 runner.manager_ctx.start();
1629 runner.exponential_builder = ExponentialBuilder::default()
1630 .with_min_delay(Duration::from_millis(1))
1631 .with_max_times(3);
1632
1633 runner.execute_procedure_in_loop().await;
1635 let err = meta.state().error().unwrap().to_string();
1636 assert!(err.contains("Procedure rollback exceeded max times"));
1637 }
1638
1639 #[tokio::test]
1640 async fn test_rollback_after_retry_fail() {
1641 let exec_fn = move |_| {
1642 async move { Err(Error::retry_later(MockError::new(StatusCode::Unexpected))) }.boxed()
1643 };
1644
1645 let (tx, mut rx) = mpsc::channel(1);
1646 let rollback_fn = move |_| {
1647 let tx = tx.clone();
1648 async move {
1649 tx.send(()).await.unwrap();
1650 Ok(())
1651 }
1652 .boxed()
1653 };
1654 let retry_later = ProcedureAdapter {
1655 data: "rollback_after_retry_fail".to_string(),
1656 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1657 poison_keys: PoisonKeys::default(),
1658 exec_fn,
1659 rollback_fn: Some(Box::new(rollback_fn)),
1660 };
1661
1662 let dir = create_temp_dir("retry_later");
1663 let meta = retry_later.new_meta(ROOT_ID);
1664 let ctx = context_without_provider(meta.id);
1665 let object_store = test_util::new_object_store(&dir);
1666 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1667 let mut runner = new_runner(meta.clone(), Box::new(retry_later), procedure_store.clone());
1668 runner.manager_ctx.start();
1669 runner.exponential_builder = ExponentialBuilder::default()
1670 .with_min_delay(Duration::from_millis(1))
1671 .with_max_times(3);
1672 runner.execute_procedure_in_loop().await;
1674 rx.recv().await.unwrap();
1675 assert_eq!(rx.try_recv().unwrap_err(), mpsc::error::TryRecvError::Empty);
1676 check_files(
1677 &object_store,
1678 &procedure_store,
1679 ctx.procedure_id,
1680 &["0000000000.rollback"],
1681 )
1682 .await;
1683 }
1684
1685 #[tokio::test]
1686 async fn test_child_error() {
1687 let mut times = 0;
1688 let child_id = ProcedureId::random();
1689 common_telemetry::init_default_ut_logging();
1690 let exec_fn = move |ctx: Context| {
1691 times += 1;
1692 async move {
1693 if times == 1 {
1694 let exec_fn = |_| {
1696 async { Err(Error::external(MockError::new(StatusCode::Unexpected))) }
1697 .boxed()
1698 };
1699 let fail = ProcedureAdapter {
1700 data: "fail".to_string(),
1701 lock_key: LockKey::single_exclusive("catalog.schema.table.region-0"),
1702 poison_keys: PoisonKeys::default(),
1703 exec_fn,
1704 rollback_fn: None,
1705 };
1706
1707 Ok(Status::Suspended {
1708 recovery: SubprocedureRecovery::Discard,
1709 subprocedures: vec![ProcedureWithId {
1710 id: child_id,
1711 procedure: Box::new(fail),
1712 context: Default::default(),
1713 }],
1714 persist: true,
1715 })
1716 } else {
1717 let state = ctx.provider.procedure_state(child_id).await.unwrap();
1719 let is_failed = state.map(|s| s.is_failed()).unwrap_or(false);
1720 if is_failed {
1721 Err(Error::from_error_ext(PlainError::new(
1723 "subprocedure failed".to_string(),
1724 StatusCode::Unexpected,
1725 )))
1726 } else {
1727 Ok(Status::Suspended {
1729 recovery: SubprocedureRecovery::Discard,
1730 subprocedures: Vec::new(),
1731 persist: false,
1732 })
1733 }
1734 }
1735 }
1736 .boxed()
1737 };
1738 let parent = ProcedureAdapter {
1739 data: "parent".to_string(),
1740 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1741 poison_keys: PoisonKeys::default(),
1742 exec_fn,
1743 rollback_fn: None,
1744 };
1745
1746 let dir = create_temp_dir("child_err");
1747 let meta = parent.new_meta(ROOT_ID);
1748
1749 let object_store = test_util::new_object_store(&dir);
1750 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1751 let mut runner = new_runner(meta.clone(), Box::new(parent), procedure_store);
1752 let poison_manager = Arc::new(InMemoryPoisonStore::default());
1753 let manager_ctx = Arc::new(ManagerContext::new(poison_manager));
1754 manager_ctx.start();
1755 assert!(manager_ctx.try_insert_procedure(meta.clone()));
1757 runner.manager_ctx = manager_ctx.clone();
1759
1760 runner.run().await;
1762 assert!(manager_ctx.key_lock.is_empty());
1763 let err = meta.state().error().unwrap().output_msg();
1764 assert!(err.contains("subprocedure failed"), "{err}");
1765 }
1766
1767 #[tokio::test]
1768 async fn test_execute_with_clean_poisons() {
1769 common_telemetry::init_default_ut_logging();
1770 let mut times = 0;
1771 let poison_key = PoisonKey::new("table/1024");
1772 let moved_poison_key = poison_key.clone();
1773 let exec_fn = move |ctx: Context| {
1774 times += 1;
1775 let poison_key = moved_poison_key.clone();
1776 async move {
1777 if times == 1 {
1778 ctx.provider
1780 .try_put_poison(&poison_key, ctx.procedure_id)
1781 .await
1782 .unwrap();
1783
1784 Ok(Status::executing(true))
1785 } else {
1786 Ok(Status::executing_with_clean_poisons(true))
1787 }
1788 }
1789 .boxed()
1790 };
1791 let poison = ProcedureAdapter {
1792 data: "poison".to_string(),
1793 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1794 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
1795 exec_fn,
1796 rollback_fn: None,
1797 };
1798
1799 let dir = create_temp_dir("clean_poisons");
1800 let meta = poison.new_meta(ROOT_ID);
1801
1802 let object_store = test_util::new_object_store(&dir);
1803 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1804 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store.clone());
1805
1806 let ctx = context_with_provider(
1808 meta.id,
1809 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1810 );
1811 runner
1813 .manager_ctx
1814 .procedures
1815 .write()
1816 .unwrap()
1817 .insert(meta.id, runner.meta.clone());
1818
1819 runner.manager_ctx.start();
1820 runner.execute_once(&ctx).await;
1821 let state = runner.meta.state();
1822 assert!(state.is_running(), "{state:?}");
1823
1824 let procedure_id = runner
1825 .manager_ctx
1826 .poison_manager
1827 .get_poison(&poison_key.to_string())
1828 .await
1829 .unwrap();
1830 assert!(procedure_id.is_some());
1832
1833 runner.execute_once(&ctx).await;
1834 let state = runner.meta.state();
1835 assert!(state.is_running(), "{state:?}");
1836
1837 let procedure_id = runner
1838 .manager_ctx
1839 .poison_manager
1840 .get_poison(&poison_key.to_string())
1841 .await
1842 .unwrap();
1843 assert!(procedure_id.is_none());
1845 }
1846
1847 #[tokio::test]
1848 async fn test_execute_error_with_clean_poisons() {
1849 common_telemetry::init_default_ut_logging();
1850 let mut times = 0;
1851 let poison_key = PoisonKey::new("table/1024");
1852 let moved_poison_key = poison_key.clone();
1853 let exec_fn = move |ctx: Context| {
1854 times += 1;
1855 let poison_key = moved_poison_key.clone();
1856 async move {
1857 if times == 1 {
1858 ctx.provider
1860 .try_put_poison(&poison_key, ctx.procedure_id)
1861 .await
1862 .unwrap();
1863
1864 Ok(Status::executing(true))
1865 } else {
1866 Err(Error::external_and_clean_poisons(MockError::new(
1867 StatusCode::Unexpected,
1868 )))
1869 }
1870 }
1871 .boxed()
1872 };
1873 let poison = ProcedureAdapter {
1874 data: "poison".to_string(),
1875 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1876 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
1877 exec_fn,
1878 rollback_fn: None,
1879 };
1880
1881 let dir = create_temp_dir("error_with_clean_poisons");
1882 let meta = poison.new_meta(ROOT_ID);
1883
1884 let object_store = test_util::new_object_store(&dir);
1885 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1886 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store.clone());
1887
1888 let ctx = context_with_provider(
1890 meta.id,
1891 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1892 );
1893 runner
1895 .manager_ctx
1896 .procedures
1897 .write()
1898 .unwrap()
1899 .insert(meta.id, runner.meta.clone());
1900
1901 runner.manager_ctx.start();
1902 runner.execute_once(&ctx).await;
1903 let state = runner.meta.state();
1904 assert!(state.is_running(), "{state:?}");
1905
1906 let procedure_id = runner
1907 .manager_ctx
1908 .poison_manager
1909 .get_poison(&poison_key.to_string())
1910 .await
1911 .unwrap();
1912 assert!(procedure_id.is_some());
1914
1915 runner.execute_once(&ctx).await;
1916 let state = runner.meta.state();
1917 assert!(state.is_failed(), "{state:?}");
1918
1919 let procedure_id = runner
1920 .manager_ctx
1921 .poison_manager
1922 .get_poison(&poison_key.to_string())
1923 .await
1924 .unwrap();
1925 assert!(procedure_id.is_none());
1927 }
1928
1929 #[tokio::test]
1930 async fn test_execute_failed_after_set_poison() {
1931 let mut times = 0;
1932 let poison_key = PoisonKey::new("table/1024");
1933 let moved_poison_key = poison_key.clone();
1934 let exec_fn = move |ctx: Context| {
1935 times += 1;
1936 let poison_key = moved_poison_key.clone();
1937 async move {
1938 if times == 1 {
1939 Ok(Status::executing(true))
1940 } else {
1941 ctx.provider
1943 .try_put_poison(&poison_key, ctx.procedure_id)
1944 .await
1945 .unwrap();
1946 Err(Error::external(MockError::new(StatusCode::Unexpected)))
1947 }
1948 }
1949 .boxed()
1950 };
1951 let poison = ProcedureAdapter {
1952 data: "poison".to_string(),
1953 lock_key: LockKey::single_exclusive("catalog.schema.table"),
1954 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
1955 exec_fn,
1956 rollback_fn: None,
1957 };
1958
1959 let dir = create_temp_dir("poison");
1960 let meta = poison.new_meta(ROOT_ID);
1961
1962 let object_store = test_util::new_object_store(&dir);
1963 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
1964 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store.clone());
1965
1966 let ctx = context_with_provider(
1968 meta.id,
1969 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1970 );
1971 runner
1973 .manager_ctx
1974 .procedures
1975 .write()
1976 .unwrap()
1977 .insert(meta.id, runner.meta.clone());
1978
1979 runner.manager_ctx.start();
1980 runner.execute_once(&ctx).await;
1981 let state = runner.meta.state();
1982 assert!(state.is_running(), "{state:?}");
1983
1984 runner.execute_once(&ctx).await;
1985 let state = runner.meta.state();
1986 assert!(state.is_failed(), "{state:?}");
1987 assert!(meta.state().is_failed());
1988
1989 let procedure_id = runner
1991 .manager_ctx
1992 .poison_manager
1993 .get_poison(&poison_key.to_string())
1994 .await
1995 .unwrap()
1996 .unwrap();
1997
1998 assert_eq!(&procedure_id.clone(), ROOT_ID);
2000 }
2001
2002 #[tokio::test]
2003 async fn test_execute_exceed_max_retry_after_set_poison() {
2004 common_telemetry::init_default_ut_logging();
2005 let mut times = 0;
2006 let poison_key = PoisonKey::new("table/1024");
2007 let moved_poison_key = poison_key.clone();
2008 let exec_fn = move |ctx: Context| {
2009 times += 1;
2010 let poison_key = moved_poison_key.clone();
2011 async move {
2012 if times == 1 {
2013 Ok(Status::executing(true))
2014 } else {
2015 ctx.provider
2017 .try_put_poison(&poison_key, ctx.procedure_id)
2018 .await
2019 .unwrap();
2020 Err(Error::retry_later_and_clean_poisons(MockError::new(
2021 StatusCode::Unexpected,
2022 )))
2023 }
2024 }
2025 .boxed()
2026 };
2027 let poison = ProcedureAdapter {
2028 data: "poison".to_string(),
2029 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2030 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
2031 exec_fn,
2032 rollback_fn: None,
2033 };
2034
2035 let dir = create_temp_dir("exceed_max_after_set_poison");
2036 let meta = poison.new_meta(ROOT_ID);
2037 let object_store = test_util::new_object_store(&dir);
2038 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2039 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store);
2040 runner.manager_ctx.start();
2041 runner.exponential_builder = ExponentialBuilder::default()
2042 .with_min_delay(Duration::from_millis(1))
2043 .with_max_times(3);
2044 let ctx = context_with_provider(
2046 meta.id,
2047 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
2048 );
2049 runner
2051 .manager_ctx
2052 .procedures
2053 .write()
2054 .unwrap()
2055 .insert(meta.id, runner.meta.clone());
2056 runner.execute_once_with_retry(&ctx).await;
2058 let err = meta.state().error().unwrap().clone();
2059 assert_matches!(&*err, Error::RetryTimesExceeded { .. });
2060
2061 let procedure_id = runner
2063 .manager_ctx
2064 .poison_manager
2065 .get_poison(&poison_key.to_string())
2066 .await
2067 .unwrap();
2068 assert_eq!(procedure_id, None);
2069 }
2070
2071 #[tokio::test]
2072 async fn test_execute_poisoned() {
2073 let mut times = 0;
2074 let poison_key = PoisonKey::new("table/1024");
2075 let moved_poison_key = poison_key.clone();
2076 let exec_fn = move |ctx: Context| {
2077 times += 1;
2078 let poison_key = moved_poison_key.clone();
2079 async move {
2080 if times == 1 {
2081 Ok(Status::executing(true))
2082 } else {
2083 ctx.provider
2085 .try_put_poison(&poison_key, ctx.procedure_id)
2086 .await
2087 .unwrap();
2088 Ok(Status::Poisoned {
2089 keys: PoisonKeys::new(vec![poison_key.clone()]),
2090 error: Error::external(MockError::new(StatusCode::Unexpected)),
2091 })
2092 }
2093 }
2094 .boxed()
2095 };
2096 let poison = ProcedureAdapter {
2097 data: "poison".to_string(),
2098 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2099 poison_keys: PoisonKeys::new(vec![poison_key.clone()]),
2100 exec_fn,
2101 rollback_fn: None,
2102 };
2103
2104 let dir = create_temp_dir("poison");
2105 let meta = poison.new_meta(ROOT_ID);
2106
2107 let object_store = test_util::new_object_store(&dir);
2108 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2109 let mut runner = new_runner(meta.clone(), Box::new(poison), procedure_store.clone());
2110
2111 let ctx = context_with_provider(
2113 meta.id,
2114 runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
2115 );
2116 runner
2118 .manager_ctx
2119 .procedures
2120 .write()
2121 .unwrap()
2122 .insert(meta.id, runner.meta.clone());
2123
2124 runner.manager_ctx.start();
2125 runner.execute_once(&ctx).await;
2126 let state = runner.meta.state();
2127 assert!(state.is_running(), "{state:?}");
2128
2129 runner.execute_once(&ctx).await;
2130 let state = runner.meta.state();
2131 assert!(state.is_poisoned(), "{state:?}");
2132 assert!(meta.state().is_poisoned());
2133 check_files(
2134 &object_store,
2135 &procedure_store,
2136 ctx.procedure_id,
2137 &["0000000000.step"],
2138 )
2139 .await;
2140
2141 let procedure_id = runner
2143 .manager_ctx
2144 .poison_manager
2145 .get_poison(&poison_key.to_string())
2146 .await
2147 .unwrap()
2148 .unwrap();
2149
2150 assert_eq!(procedure_id, ROOT_ID);
2152 }
2153
2154 fn test_procedure_with_dynamic_lock(
2155 shared_atomic_value: Arc<AtomicU64>,
2156 id: u64,
2157 ) -> (BoxedProcedure, Arc<ProcedureMeta>) {
2158 let exec_fn = move |ctx: Context| {
2159 let moved_shared_atomic_value = shared_atomic_value.clone();
2160 let moved_ctx = ctx.clone();
2161 async move {
2162 debug!("Acquiring write lock, id: {}", id);
2163 let key = StringKey::Exclusive("test_lock".to_string());
2164 let guard = moved_ctx.provider.acquire_lock(&key).await;
2165 debug!("Acquired write lock, id: {}", id);
2166 let millis = rand::rng().random_range(10..=50);
2167 tokio::time::sleep(Duration::from_millis(millis)).await;
2168 let value = moved_shared_atomic_value.load(Ordering::Relaxed);
2169 moved_shared_atomic_value.store(value + 1, Ordering::Relaxed);
2170 debug!("Dropping write lock, id: {}", id);
2171 drop(guard);
2172
2173 Ok(Status::done())
2174 }
2175 .boxed()
2176 };
2177
2178 let adapter = ProcedureAdapter {
2179 data: "dynamic_lock".to_string(),
2180 lock_key: LockKey::new_exclusive([]),
2181 poison_keys: PoisonKeys::new([]),
2182 exec_fn,
2183 rollback_fn: None,
2184 };
2185 let meta = adapter.new_meta(ROOT_ID);
2186
2187 (Box::new(adapter), meta)
2188 }
2189
2190 #[tokio::test(flavor = "multi_thread")]
2191 async fn test_execute_with_dynamic_lock() {
2192 common_telemetry::init_default_ut_logging();
2193 let shared_atomic_value = Arc::new(AtomicU64::new(0));
2194 let (procedure1, meta1) = test_procedure_with_dynamic_lock(shared_atomic_value.clone(), 1);
2195 let (procedure2, meta2) = test_procedure_with_dynamic_lock(shared_atomic_value.clone(), 2);
2196
2197 let dir = create_temp_dir("dynamic_lock");
2198 let object_store = test_util::new_object_store(&dir);
2199 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2200 let mut runner1 = new_runner(meta1.clone(), procedure1, procedure_store.clone());
2201 let mut runner2 = new_runner(meta2.clone(), procedure2, procedure_store.clone());
2202 let ctx1 = context_with_provider(
2203 meta1.id,
2204 runner1.manager_ctx.clone() as Arc<dyn ContextProvider>,
2205 );
2206 let ctx2 = context_with_provider(
2207 meta2.id,
2208 runner1.manager_ctx.clone() as Arc<dyn ContextProvider>,
2210 );
2211 let tasks = [runner1.execute_once(&ctx1), runner2.execute_once(&ctx2)];
2212 join_all(tasks).await;
2213 assert_eq!(shared_atomic_value.load(Ordering::Relaxed), 2);
2214 }
2215 #[tokio::test]
2216 async fn test_on_suspend_deadlock_detected_no_rollback() {
2217 let child_id = ProcedureId::random();
2220 let exec_fn = move |_| {
2221 async move {
2222 let child_exec_fn = |_| async { Ok(Status::done()) }.boxed();
2223 let child = ProcedureAdapter {
2224 data: "child".to_string(),
2225 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2226 poison_keys: PoisonKeys::default(),
2227 exec_fn: child_exec_fn,
2228 rollback_fn: None,
2229 };
2230 Ok(Status::Suspended {
2231 recovery: SubprocedureRecovery::Discard,
2232 subprocedures: vec![ProcedureWithId {
2233 id: child_id,
2234 procedure: Box::new(child),
2235 context: Default::default(),
2236 }],
2237 persist: false,
2238 })
2239 }
2240 .boxed()
2241 };
2242 let parent = ProcedureAdapter {
2243 data: "parent".to_string(),
2244 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2245 poison_keys: PoisonKeys::default(),
2246 exec_fn,
2247 rollback_fn: None, };
2249
2250 let dir = create_temp_dir("deadlock_no_rollback");
2251 let meta = parent.new_meta(ROOT_ID);
2252 let ctx = context_without_provider(meta.id);
2253 let object_store = test_util::new_object_store(&dir);
2254 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2255 let mut runner = new_runner(meta.clone(), Box::new(parent), procedure_store);
2256 runner.manager_ctx.start();
2257
2258 runner.execute_once(&ctx).await;
2259 let state = runner.meta.state();
2260 assert!(state.is_failed(), "Expected Failed, got {state:?}");
2261 assert!(
2263 state.error().is_some(),
2264 "Failed state should contain an error"
2265 );
2266 assert!(
2268 !runner.manager_ctx.contains_procedure(child_id),
2269 "Child procedure should not be submitted when deadlock is detected"
2270 );
2271 }
2272
2273 #[tokio::test]
2274 async fn test_on_suspend_deadlock_detected_with_rollback() {
2275 let child_id = ProcedureId::random();
2278 let exec_fn = move |_| {
2279 async move {
2280 let child_exec_fn = |_| async { Ok(Status::done()) }.boxed();
2281 let child = ProcedureAdapter {
2282 data: "child".to_string(),
2283 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2284 poison_keys: PoisonKeys::default(),
2285 exec_fn: child_exec_fn,
2286 rollback_fn: None,
2287 };
2288 Ok(Status::Suspended {
2289 recovery: SubprocedureRecovery::Discard,
2290 subprocedures: vec![ProcedureWithId {
2291 id: child_id,
2292 procedure: Box::new(child),
2293 context: Default::default(),
2294 }],
2295 persist: false,
2296 })
2297 }
2298 .boxed()
2299 };
2300 let rollback_fn = move |_| async move { Ok(()) }.boxed();
2301 let parent = ProcedureAdapter {
2302 data: "parent".to_string(),
2303 lock_key: LockKey::single_exclusive("catalog.schema.table"),
2304 poison_keys: PoisonKeys::default(),
2305 exec_fn,
2306 rollback_fn: Some(Box::new(rollback_fn)), };
2308
2309 let dir = create_temp_dir("deadlock_with_rollback");
2310 let meta = parent.new_meta(ROOT_ID);
2311 let ctx = context_without_provider(meta.id);
2312 let object_store = test_util::new_object_store(&dir);
2313 let procedure_store = Arc::new(ProcedureStore::from_object_store(object_store.clone()));
2314 let mut runner = new_runner(meta.clone(), Box::new(parent), procedure_store);
2315 runner.manager_ctx.start();
2316
2317 runner.execute_once(&ctx).await;
2318 let state = runner.meta.state();
2319 assert!(
2320 state.is_prepare_rollback(),
2321 "Expected PrepareRollback, got {state:?}"
2322 );
2323 match &state {
2325 ProcedureState::PrepareRollback { error } => {
2326 assert!(!error.to_string().is_empty(), "Error should not be empty");
2327 }
2328 _ => panic!("Expected PrepareRollback, got {state:?}"),
2329 }
2330 assert!(
2332 !runner.manager_ctx.contains_procedure(child_id),
2333 "Child procedure should not be submitted when deadlock is detected"
2334 );
2335 }
2336
2337 #[test]
2338 fn test_find_lock_conflicts() {
2339 use crate::procedure::StringKey;
2340
2341 let parent = [StringKey::Share("A".to_string())];
2343 let child = [StringKey::Share("A".to_string())];
2344 assert!(super::find_lock_conflicts(parent.iter(), child.iter()).is_empty());
2345
2346 let parent = [StringKey::Share("A".to_string())];
2348 let child = [StringKey::Exclusive("A".to_string())];
2349 assert_eq!(
2350 super::find_lock_conflicts(parent.iter(), child.iter()),
2351 vec!["A".to_string()]
2352 );
2353
2354 let parent = [StringKey::Exclusive("A".to_string())];
2356 let child = [StringKey::Share("A".to_string())];
2357 assert_eq!(
2358 super::find_lock_conflicts(parent.iter(), child.iter()),
2359 vec!["A".to_string()]
2360 );
2361
2362 let parent = [StringKey::Exclusive("A".to_string())];
2364 let child = [StringKey::Exclusive("A".to_string())];
2365 assert_eq!(
2366 super::find_lock_conflicts(parent.iter(), child.iter()),
2367 vec!["A".to_string()]
2368 );
2369
2370 let parent = [
2372 StringKey::Share("A".to_string()),
2373 StringKey::Exclusive("B".to_string()),
2374 ];
2375 let child = [
2376 StringKey::Exclusive("A".to_string()), StringKey::Share("B".to_string()), StringKey::Exclusive("C".to_string()), ];
2380 let mut conflicts = super::find_lock_conflicts(parent.iter(), child.iter());
2381 conflicts.sort();
2382 assert_eq!(conflicts, vec!["A".to_string(), "B".to_string()]);
2383 }
2384}