Skip to main content

common_procedure/local/
runner.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
41/// A guard to cleanup procedure state.
42struct ProcedureGuard {
43    meta: ProcedureMetaRef,
44    manager_ctx: Arc<ManagerContext>,
45    key_guards: Vec<OwnedKeyRwLockGuard>,
46    finish: bool,
47}
48
49impl ProcedureGuard {
50    /// Returns a new [ProcedureGuard].
51    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    /// The procedure is finished successfully.
61    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            // Set state to failed. This is useful in test as runtime may not abort when the runner task panics.
72            // See https://github.com/tokio-rs/tokio/issues/2002 .
73            // We set set_panic_hook() in the application's main function. But our tests don't have this panic hook.
74            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        // Notify parent procedure.
82        if let Some(parent_id) = self.meta.parent_id {
83            self.manager_ctx.notify_by_subprocedure(parent_id);
84        }
85
86        // Drops the key guards in the reverse order.
87        while !self.key_guards.is_empty() {
88            self.key_guards.pop();
89        }
90
91        // Clean the staled locks.
92        self.manager_ctx
93            .key_lock
94            .clean_keys(self.meta.lock_key.keys_to_lock().map(|k| k.as_string()));
95    }
96}
97
98/// Returns a list of conflicting lock keys between a parent and a child procedure.
99/// Evaluates the Read/Write lock compatibility matrix:
100/// - Share + Share => Compatible
101/// - Exclusive + Any => Conflict
102/// - Any + Exclusive => Conflict
103fn 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    // Map from key string slice (&str) to a boolean indicating if the parent holds it EXCLUSIVELY.
110    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    /// Return `ProcedureManager` is running.
152    pub(crate) fn running(&self) -> bool {
153        self.manager_ctx.running()
154    }
155
156    /// Run the procedure.
157    pub(crate) async fn run(mut self) {
158        // Ensure we can update the procedure state.
159        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        // TODO(yingwen): Detect recursive locking (and deadlock) if possible. Maybe we could detect
168        // recursive locking by adding a root procedure id to the meta.
169        for key in self.meta.lock_key.keys_to_lock() {
170            // Acquire lock for each key.
171            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        // Execute the procedure. We need to release the lock whenever the execution
182        // is successful or fail.
183        self.meta.set_start_time_ms();
184        self.execute_procedure_in_loop().await;
185        self.meta.set_end_time_ms();
186
187        // We can't remove the metadata of the procedure now as users and its parent might
188        // need to query its state.
189        // TODO(yingwen): 1. Add TTL to the metadata; 2. Only keep state in the procedure store
190        // so we don't need to always store the metadata in memory after the procedure is done.
191
192        // Release locks and notify parent procedure.
193        guard.finish();
194
195        // If this is the root procedure, clean up message cache.
196        if self.meta.parent_id.is_none() {
197            let procedure_ids = self.manager_ctx.procedures_in_tree(&self.meta);
198            // Clean resources.
199            self.manager_ctx.on_procedures_finish(&procedure_ids);
200
201            // If `ProcedureManager` is stopped, it stops the current task immediately without deleting the procedure.
202            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            // Don't store state if `ProcedureManager` is stopped.
246            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                        // Add random noise to the retry delay to avoid retry storms.
260                        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        // returns the last error if any.
313        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                        // Don't store state if `ProcedureManager` is stopped.
356                        if !self.running() {
357                            self.set_state_and_record(ProcedureState::failed(Arc::new(
358                                error::ManagerNotStartSnafu {}.build(),
359                            )));
360                            return;
361                        }
362
363                        // Cleans poisons before persist.
364                        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                        // Don't store state if `ProcedureManager` is stopped.
447                        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    /// Submit a subprocedure with specific `procedure_id`.
490    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            // If the parent has already submitted this procedure, don't submit it again.
508            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        // Insert the procedure. We already check the procedure existence before inserting
538        // so we add an assertion to ensure the procedure id is unique and no other procedures
539        // using the same procedure id.
540        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                // Run the root procedure.
562                // The task was moved to another runtime for execution.
563                // In order not to interrupt tracing, a span needs to be created to continue tracing the current task.
564                runner.run().trace(span).await
565            })
566        }) {
567            self.manager_ctx.remove_procedure(procedure_id);
568            return (ChildSubmissionOutcome::SpawnFailed, None);
569        }
570
571        // Add the id of the subprocedure to the metadata.
572        self.meta.push_child(procedure_id);
573        (ChildSubmissionOutcome::Accepted, submitted_event)
574    }
575
576    /// Extend the retry time to wait for the next retry.
577    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        // Pre-check: detect potential deadlocks BEFORE submitting any subprocedure.
596        // If a child shares conflicting lock keys with the parent, submitting it would
597        // cause a Hold-and-Wait deadlock — the child blocks on lock acquisition while
598        // the parent holds the lock and waits for the child to finish.
599        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        // Wait for subprocedures.
663        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        // Persists procedure state
722        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        // TODO(yingwen): Add files to remove list.
750        info!(
751            "Procedure {}-{} done",
752            self.procedure.type_name(),
753            self.meta.id,
754        );
755
756        // Mark the state of this procedure to done.
757        self.set_state_and_record(ProcedureState::Done { output });
758    }
759
760    /// Updates framework state and records the lifecycle event implied by that state.
761    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            // We don't record the prepare rollback state.
787            // The final result will be recorded as ProcedureState::Failed or ProcedureState::Succeeded.
788            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    /// Builds an event from the live procedure.
797    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    /// Builds and dispatches an event from the live procedure. Delivery is best effort and is
819    /// intentionally not part of procedure execution or persistence.
820    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                    // Submit subprocedures.
1158                    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                    // Wait for subprocedures.
1169                    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                        // Return suspended to wait for notify.
1186                        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        // Manually add this procedure to the manager ctx.
1215        assert!(manager_ctx.try_insert_procedure(meta));
1216        // Replace the manager ctx.
1217        runner.manager_ctx = manager_ctx.clone();
1218
1219        runner.run().await;
1220        assert!(manager_ctx.key_lock.is_empty());
1221
1222        // Check child procedures.
1223        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        // Files are removed.
1230        check_files(&object_store, &procedure_store, procedure_id, &[]).await;
1231
1232        tokio::time::sleep(Duration::from_millis(5)).await;
1233        // Clean outdated meta.
1234        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        // Shouldn't write any files
1277        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        // Shouldn't write any files
1310        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        // Run the runner and execute the procedure.
1599        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        // Run the runner and execute the procedure.
1634        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        // Run the runner and execute the procedure.
1673        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                    // Submit subprocedures.
1695                    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                    // Wait for subprocedures.
1718                    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                        // The parent procedure to abort itself if child procedure is failed.
1722                        Err(Error::from_error_ext(PlainError::new(
1723                            "subprocedure failed".to_string(),
1724                            StatusCode::Unexpected,
1725                        )))
1726                    } else {
1727                        // Return suspended to wait for notify.
1728                        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        // Manually add this procedure to the manager ctx.
1756        assert!(manager_ctx.try_insert_procedure(meta.clone()));
1757        // Replace the manager ctx.
1758        runner.manager_ctx = manager_ctx.clone();
1759
1760        // Run the runner and execute the procedure.
1761        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                    // Put the poison to the context.
1779                    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        // Use the manager ctx as the context provider.
1807        let ctx = context_with_provider(
1808            meta.id,
1809            runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1810        );
1811        // Manually add this procedure to the manager ctx.
1812        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        // poison key should be exist.
1831        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        // poison key should be deleted.
1844        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                    // Put the poison to the context.
1859                    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        // Use the manager ctx as the context provider.
1889        let ctx = context_with_provider(
1890            meta.id,
1891            runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1892        );
1893        // Manually add this procedure to the manager ctx.
1894        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        // poison key should be exist.
1913        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        // poison key should be deleted.
1926        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                    // Put the poison to the context.
1942                    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        // Use the manager ctx as the context provider.
1967        let ctx = context_with_provider(
1968            meta.id,
1969            runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
1970        );
1971        // Manually add this procedure to the manager ctx.
1972        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        // Check the poison is set.
1990        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        // If the procedure is poisoned, the poison key shouldn't be deleted.
1999        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                    // Put the poison to the context.
2016                    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        // Use the manager ctx as the context provider.
2045        let ctx = context_with_provider(
2046            meta.id,
2047            runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
2048        );
2049        // Manually add this procedure to the manager ctx.
2050        runner
2051            .manager_ctx
2052            .procedures
2053            .write()
2054            .unwrap()
2055            .insert(meta.id, runner.meta.clone());
2056        // Run the runner and execute the procedure.
2057        runner.execute_once_with_retry(&ctx).await;
2058        let err = meta.state().error().unwrap().clone();
2059        assert_matches!(&*err, Error::RetryTimesExceeded { .. });
2060
2061        // Check the poison is deleted.
2062        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                    // Put the poison to the context.
2084                    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        // Use the manager ctx as the context provider.
2112        let ctx = context_with_provider(
2113            meta.id,
2114            runner.manager_ctx.clone() as Arc<dyn ContextProvider>,
2115        );
2116        // Manually add this procedure to the manager ctx.
2117        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        // Check the poison is set.
2142        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        // If the procedure is poisoned, the poison key shouldn't be deleted.
2151        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            // use same manager ctx as runner1
2209            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        // Parent holds Exclusive("catalog.schema.table"), child also requests Exclusive("catalog.schema.table").
2218        // Since parent does NOT support rollback, state should become Failed.
2219        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, // No rollback support
2248        };
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        // Verify the error exists
2262        assert!(
2263            state.error().is_some(),
2264            "Failed state should contain an error"
2265        );
2266        // Child should NOT have been submitted
2267        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        // Parent holds Exclusive("catalog.schema.table"), child also requests Exclusive("catalog.schema.table").
2276        // Since parent DOES support rollback, state should become PrepareRollback.
2277        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)), // Supports rollback
2307        };
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        // Verify the error exists in PrepareRollback variant
2324        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        // Child should NOT have been submitted
2331        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        // 1. Share + Share = No conflict (Compatible)
2342        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        // 2. Share + Exclusive = Conflict
2347        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        // 3. Exclusive + Share = Conflict
2355        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        // 4. Exclusive + Exclusive = Conflict
2363        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        // 5. Multiple keys, partial overlap
2371        let parent = [
2372            StringKey::Share("A".to_string()),
2373            StringKey::Exclusive("B".to_string()),
2374        ];
2375        let child = [
2376            StringKey::Exclusive("A".to_string()), // Conflict with Share("A")
2377            StringKey::Share("B".to_string()),     // Conflict with Exclusive("B")
2378            StringKey::Exclusive("C".to_string()), // No conflict, parent doesn't hold C
2379        ];
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}