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