Skip to main content

common_procedure/
procedure.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::any::Any;
16use std::fmt;
17use std::fmt::Display;
18use std::str::FromStr;
19use std::sync::Arc;
20
21use async_trait::async_trait;
22use common_event_recorder::{Event, EventTypeFilterRef, PersistentEventContext};
23use serde::{Deserialize, Serialize};
24use smallvec::{SmallVec, smallvec};
25use snafu::{ResultExt, Snafu};
26use tokio::sync::watch::Receiver;
27use uuid::Uuid;
28
29use crate::error::{self, Error, Result};
30use crate::local::DynamicKeyLockGuard;
31use crate::watcher::Watcher;
32
33pub type Output = Arc<dyn Any + Send + Sync>;
34
35/// Context attached to a procedure submission and inherited by its children.
36#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
37pub struct ProcedureContext {
38    /// Effective user that submitted the procedure.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub actor: Option<String>,
41    /// Context describing why and how the procedure was submitted.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub event_context: Option<PersistentEventContext>,
44}
45
46impl ProcedureContext {
47    pub fn from_event_context(event_context: PersistentEventContext) -> Self {
48        Self {
49            actor: None,
50            event_context: Some(event_context),
51        }
52    }
53
54    pub fn is_empty(&self) -> bool {
55        self.actor.is_none() && self.event_context.is_none()
56    }
57}
58
59/// Controls how children submitted by [`Status::Suspended`] are handled after a restart.
60///
61/// The policy is captured at submission and persisted with each child's checkpoints.
62/// It does not change parent-child ownership during normal execution. A child without
63/// a persisted checkpoint cannot be recovered, regardless of its policy.
64#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
65pub enum SubprocedureRecovery {
66    /// Does not recover the child. The parent decides whether to resubmit its work.
67    /// This does not roll back side effects or delete the child's persisted records.
68    #[default]
69    Discard,
70    /// Resumes the child's persisted execution or rollback as an independent root.
71    /// Its original id and context are retained, but its parent link is removed.
72    /// Completion no longer notifies the former parent.
73    /// All eligible procedures are loaded and their recovery hooks are called before
74    /// any recovered runner starts. Runner execution is not ordered relative to the
75    /// former parent; callers must prevent conflicting work when the parent recovers.
76    Detach,
77}
78
79impl SubprocedureRecovery {
80    /// Returns whether the child is excluded from recovery.
81    pub(crate) fn is_discard(&self) -> bool {
82        matches!(self, Self::Discard)
83    }
84}
85
86/// Procedure execution status.
87#[derive(Debug)]
88pub enum Status {
89    /// The procedure is still executing.
90    Executing {
91        /// Whether the framework needs to persist the procedure.
92        persist: bool,
93        /// Whether the framework needs to clean the poisons.
94        clean_poisons: bool,
95    },
96    /// The procedure has suspended itself and is waiting for subprocedures.
97    Suspended {
98        subprocedures: Vec<ProcedureWithId>,
99        /// Recovery policy for every child in this submission, recorded at its first checkpoint.
100        recovery: SubprocedureRecovery,
101        /// Whether the framework needs to persist the procedure.
102        persist: bool,
103    },
104    /// The procedure is poisoned.
105    Poisoned {
106        /// The keys that cause the procedure to be poisoned.
107        keys: PoisonKeys,
108        /// The error that cause the procedure to be poisoned.
109        error: Error,
110    },
111    /// the procedure is done.
112    Done { output: Option<Output> },
113}
114
115impl Status {
116    /// Returns a [Status::Suspended] with given `subprocedures` and `persist` flag.
117    /// Children default to [`SubprocedureRecovery::Discard`].
118    pub fn suspended(subprocedures: Vec<ProcedureWithId>, persist: bool) -> Status {
119        Status::Suspended {
120            subprocedures,
121            recovery: SubprocedureRecovery::Discard,
122            persist,
123        }
124    }
125
126    /// Returns a [Status::Poisoned] with given `keys` and `error`.
127    pub fn poisoned(keys: impl IntoIterator<Item = PoisonKey>, error: Error) -> Status {
128        Status::Poisoned {
129            keys: PoisonKeys::new(keys),
130            error,
131        }
132    }
133
134    /// Returns a [Status::Executing] with given `persist` flag.
135    pub fn executing(persist: bool) -> Status {
136        Status::Executing {
137            persist,
138            clean_poisons: false,
139        }
140    }
141
142    /// Returns a [Status::Executing] with given `persist` flag and clean poisons.
143    pub fn executing_with_clean_poisons(persist: bool) -> Status {
144        Status::Executing {
145            persist,
146            clean_poisons: true,
147        }
148    }
149
150    /// Returns a [Status::Done] without output.
151    pub fn done() -> Status {
152        Status::Done { output: None }
153    }
154
155    #[cfg(any(test, feature = "testing"))]
156    /// Downcasts [Status::Done]'s output to &T
157    ///  #Panic:
158    /// - if [Status] is not the [Status::Done].
159    /// - if the output is None.
160    pub fn downcast_output_ref<T: 'static>(&self) -> Option<&T> {
161        if let Status::Done { output } = self {
162            output
163                .as_ref()
164                .expect("Try to downcast the output of Status::Done, but the output is None")
165                .downcast_ref()
166        } else {
167            panic!("Expected the Status::Done, but got: {:?}", self)
168        }
169    }
170
171    /// Returns a [Status::Done] with output.
172    pub fn done_with_output<T: Any + Send + Sync>(output: T) -> Status {
173        Status::Done {
174            output: Some(Arc::new(output)),
175        }
176    }
177    /// Returns `true` if the procedure is done.
178    pub fn is_done(&self) -> bool {
179        matches!(self, Status::Done { .. })
180    }
181
182    /// Returns `true` if the procedure needs the framework to persist its intermediate state.
183    pub fn need_persist(&self) -> bool {
184        match self {
185            // If the procedure is done/poisoned, the framework doesn't need to persist the procedure
186            // anymore. It only needs to mark the procedure as committed.
187            Status::Executing { persist, .. } | Status::Suspended { persist, .. } => *persist,
188            Status::Done { .. } | Status::Poisoned { .. } => false,
189        }
190    }
191
192    /// Returns `true` if the framework needs to clean the poisons.
193    pub fn need_clean_poisons(&self) -> bool {
194        match self {
195            Status::Executing { clean_poisons, .. } => *clean_poisons,
196            Status::Done { .. } => true,
197            _ => false,
198        }
199    }
200}
201
202/// [ContextProvider] provides information about procedures in the [ProcedureManager].
203#[async_trait]
204pub trait ContextProvider: Send + Sync {
205    /// Query the procedure state.
206    async fn procedure_state(&self, procedure_id: ProcedureId) -> Result<Option<ProcedureState>>;
207
208    async fn procedure_state_receiver(
209        &self,
210        procedure_id: ProcedureId,
211    ) -> Result<Option<Receiver<ProcedureState>>>;
212
213    /// Try to put a poison key for a procedure.
214    ///
215    /// This method is used to mark a resource as being operated on by a procedure.
216    /// If the poison key already exists with a different value, the operation will fail.
217    async fn try_put_poison(&self, key: &PoisonKey, procedure_id: ProcedureId) -> Result<()>;
218
219    /// Acquires a key lock for the procedure.
220    async fn acquire_lock(&self, key: &StringKey) -> DynamicKeyLockGuard;
221}
222
223/// Reference-counted pointer to [ContextProvider].
224pub type ContextProviderRef = Arc<dyn ContextProvider>;
225
226/// Procedure execution context.
227#[derive(Clone)]
228pub struct Context {
229    /// Id of the procedure.
230    pub procedure_id: ProcedureId,
231    /// [ProcedureManager] context provider.
232    pub provider: ContextProviderRef,
233    /// Event context inherited from the root submission.
234    pub event_context: Option<PersistentEventContext>,
235}
236
237impl Context {
238    /// Returns true if current procedure state is retrying.
239    pub async fn is_retrying(&self) -> Option<bool> {
240        self.provider
241            .procedure_state(self.procedure_id)
242            .await
243            .ok()
244            .flatten()
245            .map(|s| s.is_retrying())
246    }
247}
248
249/// A `Procedure` represents an operation or a set of operations to be performed step-by-step.
250#[async_trait]
251pub trait Procedure: Send {
252    /// Type name of the procedure.
253    fn type_name(&self) -> &str;
254
255    /// Execute the procedure.
256    ///
257    /// The implementation must be idempotent.
258    async fn execute(&mut self, ctx: &Context) -> Result<Status>;
259
260    /// Rollback the failed procedure.
261    ///
262    /// The implementation must be idempotent.
263    async fn rollback(&mut self, _: &Context) -> Result<()> {
264        error::RollbackNotSupportedSnafu {}.fail()
265    }
266
267    /// Indicates whether it supports rolling back the procedure.
268    fn rollback_supported(&self) -> bool {
269        false
270    }
271
272    /// Dump the state of the procedure to a string.
273    fn dump(&self) -> Result<String>;
274
275    /// The hook is called after the procedure recovery.
276    fn recover(&mut self) -> Result<()> {
277        Ok(())
278    }
279
280    /// Returns the [LockKey] that this procedure needs to acquire.
281    fn lock_key(&self) -> LockKey;
282
283    /// Returns the [PoisonKeys] that may cause this procedure to become poisoned during execution.
284    fn poison_keys(&self) -> PoisonKeys {
285        PoisonKeys::default()
286    }
287
288    /// Builds an event for a framework lifecycle trigger.
289    ///
290    /// The hook is called with the current procedure instance, so an event can
291    /// include state that was produced after the procedure was submitted. A
292    /// return value of `None` means that this trigger should not be recorded.
293    /// Events that share an [`Event::event_type`] must return identical
294    /// [`Event::extra_schema`] values. The event recorder batches events by type
295    /// and rejects incompatible schemas; use a distinct event type for a
296    /// different schema.
297    fn event(&self, _ctx: &EventContext<'_>) -> Option<Box<dyn Event>> {
298        None
299    }
300}
301
302/// Framework-owned context supplied when a procedure builds a lifecycle event.
303pub struct EventContext<'a> {
304    /// Id of the procedure associated with the event.
305    pub procedure_id: ProcedureId,
306    /// Current framework state of the procedure.
307    pub lifecycle_state: &'a ProcedureState,
308    /// Lifecycle action that caused the event hook to be called.
309    pub trigger: EventTrigger,
310    /// Event types retained by the configured recorder.
311    pub event_type_filter: EventTypeFilterRef,
312    /// Event context inherited from the root submission.
313    pub event_context: Option<&'a PersistentEventContext>,
314}
315
316/// Lifecycle action that causes the framework to invoke [`Procedure::event`].
317///
318/// It is recorded as a tagged JSON object in the `procedure_trigger` event envelope column.
319#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
320#[serde(tag = "type")]
321pub enum EventTrigger {
322    /// The procedure was submitted to the manager.
323    Submitted,
324    /// The root procedure was recovered from persisted state.
325    Recovered,
326    /// A child submission was attempted.
327    ChildSubmitted {
328        /// The submitted child procedure.
329        procedure_id: ProcedureId,
330        /// The result of the submission attempt.
331        outcome: ChildSubmissionOutcome,
332    },
333    /// Procedure execution is being retried.
334    Retrying {
335        /// Phase in which the retry occurs.
336        phase: RetryPhase,
337        /// Retry attempt within the current runner lifecycle.
338        attempt: u32,
339    },
340    /// Procedure rollback is starting.
341    RollingBack,
342    /// The procedure reached a successful terminal state.
343    Succeeded,
344    /// The procedure reached a failed terminal state.
345    Failed,
346    /// The procedure was poisoned and cannot proceed.
347    Poisoned,
348}
349
350/// Phase of a procedure retry.
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
352pub enum RetryPhase {
353    /// Retrying procedure execution.
354    Execute,
355    /// Retrying procedure rollback.
356    Rollback,
357}
358
359/// Outcome of submitting a child procedure.
360#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
361pub enum ChildSubmissionOutcome {
362    Accepted,
363    AlreadyAccepted,
364    ManagerStopped,
365    SpawnFailed,
366}
367
368#[async_trait]
369impl<T: Procedure + ?Sized> Procedure for Box<T> {
370    fn type_name(&self) -> &str {
371        (**self).type_name()
372    }
373
374    async fn execute(&mut self, ctx: &Context) -> Result<Status> {
375        (**self).execute(ctx).await
376    }
377
378    async fn rollback(&mut self, ctx: &Context) -> Result<()> {
379        (**self).rollback(ctx).await
380    }
381
382    fn rollback_supported(&self) -> bool {
383        (**self).rollback_supported()
384    }
385
386    fn dump(&self) -> Result<String> {
387        (**self).dump()
388    }
389
390    fn lock_key(&self) -> LockKey {
391        (**self).lock_key()
392    }
393
394    fn poison_keys(&self) -> PoisonKeys {
395        (**self).poison_keys()
396    }
397
398    fn event(&self, ctx: &EventContext<'_>) -> Option<Box<dyn Event>> {
399        (**self).event(ctx)
400    }
401}
402
403#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
404pub struct PoisonKey(String);
405
406impl Display for PoisonKey {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        write!(f, "{}", self.0)
409    }
410}
411
412impl PoisonKey {
413    /// Creates a new [PoisonKey] from a [String].
414    pub fn new(key: impl Into<String>) -> Self {
415        Self(key.into())
416    }
417}
418
419/// A collection of [PoisonKey]s.
420///
421/// This type is used to represent the keys that may cause the procedure to become poisoned during execution.
422#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
423pub struct PoisonKeys(SmallVec<[PoisonKey; 2]>);
424
425impl PoisonKeys {
426    /// Creates a new [PoisonKeys] from a [String].
427    pub fn single(key: impl Into<String>) -> Self {
428        Self(smallvec![PoisonKey::new(key)])
429    }
430
431    /// Creates a new [PoisonKeys] from a [PoisonKey].
432    pub fn new(keys: impl IntoIterator<Item = PoisonKey>) -> Self {
433        Self(keys.into_iter().collect())
434    }
435
436    /// Returns `true` if the [PoisonKeys] contains the given [PoisonKey].
437    pub fn contains(&self, key: &PoisonKey) -> bool {
438        self.0.contains(key)
439    }
440
441    /// Returns an iterator over the [PoisonKey]s.
442    pub fn iter(&self) -> impl Iterator<Item = &PoisonKey> {
443        self.0.iter()
444    }
445}
446
447#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
448pub enum StringKey {
449    Share(String),
450    Exclusive(String),
451}
452
453/// Keys to identify required locks.
454///
455/// [LockKey] always sorts keys lexicographically so that they can be acquired
456/// in the same order.
457/// Most procedures should only acquire 1 ~ 2 locks so we use smallvec to hold keys.
458#[derive(Clone, Debug, Default, PartialEq, Eq)]
459pub struct LockKey(SmallVec<[StringKey; 2]>);
460
461impl StringKey {
462    pub fn into_string(self) -> String {
463        match self {
464            StringKey::Share(s) => s,
465            StringKey::Exclusive(s) => s,
466        }
467    }
468
469    pub fn as_string(&self) -> &String {
470        match self {
471            StringKey::Share(s) => s,
472            StringKey::Exclusive(s) => s,
473        }
474    }
475}
476
477impl LockKey {
478    /// Returns a new [LockKey] with only one key.
479    pub fn single(key: impl Into<StringKey>) -> LockKey {
480        LockKey(smallvec![key.into()])
481    }
482
483    /// Returns a new [LockKey] with only one key.
484    pub fn single_exclusive(key: impl Into<String>) -> LockKey {
485        LockKey(smallvec![StringKey::Exclusive(key.into())])
486    }
487
488    /// Returns a new [LockKey] with keys from specific `iter`.
489    pub fn new(iter: impl IntoIterator<Item = StringKey>) -> LockKey {
490        let mut vec: SmallVec<_> = iter.into_iter().collect();
491        vec.sort();
492        // Dedup keys to avoid acquiring the same key multiple times.
493        vec.dedup();
494        LockKey(vec)
495    }
496
497    /// Returns a new [LockKey] with keys from specific `iter`.
498    pub fn new_exclusive(iter: impl IntoIterator<Item = String>) -> LockKey {
499        Self::new(iter.into_iter().map(StringKey::Exclusive))
500    }
501
502    /// Returns the keys to lock.
503    pub fn keys_to_lock(&self) -> impl Iterator<Item = &StringKey> {
504        self.0.iter()
505    }
506
507    /// Returns the keys to lock.
508    pub fn get_keys(&self) -> Vec<String> {
509        self.0.iter().map(|key| format!("{:?}", key)).collect()
510    }
511}
512
513/// Boxed [Procedure].
514pub type BoxedProcedure = Box<dyn Procedure>;
515
516/// A procedure with specific id.
517pub struct ProcedureWithId {
518    /// Id of the procedure.
519    pub id: ProcedureId,
520    pub procedure: BoxedProcedure,
521    /// Context associated with this procedure submission.
522    pub context: ProcedureContext,
523}
524
525impl ProcedureWithId {
526    /// Returns a new [ProcedureWithId] that holds specific `procedure`
527    /// and a random [ProcedureId].
528    pub fn with_random_id(procedure: BoxedProcedure) -> ProcedureWithId {
529        ProcedureWithId {
530            id: ProcedureId::random(),
531            procedure,
532            context: ProcedureContext::default(),
533        }
534    }
535
536    /// Attaches a complete persisted context to this procedure submission.
537    pub fn with_context(mut self, context: ProcedureContext) -> Self {
538        self.context = context;
539        self
540    }
541}
542
543impl fmt::Debug for ProcedureWithId {
544    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
545        write!(f, "{}-{}", self.procedure.type_name(), self.id)
546    }
547}
548
549#[derive(Debug, Snafu)]
550pub struct ParseIdError {
551    source: uuid::Error,
552}
553
554/// Unique id for [Procedure].
555#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
556pub struct ProcedureId(Uuid);
557
558impl ProcedureId {
559    /// Returns a new unique [ProcedureId] randomly.
560    pub fn random() -> ProcedureId {
561        ProcedureId(Uuid::new_v4())
562    }
563
564    /// Parses id from string.
565    pub fn parse_str(input: &str) -> std::result::Result<ProcedureId, ParseIdError> {
566        Uuid::parse_str(input)
567            .map(ProcedureId)
568            .context(ParseIdSnafu)
569    }
570}
571
572impl fmt::Display for ProcedureId {
573    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574        write!(f, "{}", self.0)
575    }
576}
577
578impl FromStr for ProcedureId {
579    type Err = ParseIdError;
580
581    fn from_str(s: &str) -> std::result::Result<ProcedureId, ParseIdError> {
582        ProcedureId::parse_str(s)
583    }
584}
585
586/// Loader to recover the [Procedure] instance from serialized data.
587pub type BoxedProcedureLoader = Box<dyn Fn(&str) -> Result<BoxedProcedure> + Send>;
588
589/// State of a submitted procedure.
590#[derive(Debug, Default, Clone)]
591pub enum ProcedureState {
592    /// The procedure is running.
593    #[default]
594    Running,
595    /// The procedure is finished.
596    Done { output: Option<Output> },
597    /// The procedure is failed and can be retried.
598    Retrying { error: Arc<Error> },
599    /// The procedure is failed and commits state before rolling back the procedure.
600    PrepareRollback { error: Arc<Error> },
601    /// The procedure is failed and can be rollback.
602    RollingBack { error: Arc<Error> },
603    /// The procedure is failed and cannot proceed anymore.
604    Failed { error: Arc<Error> },
605    /// The procedure is poisoned.
606    Poisoned { keys: PoisonKeys, error: Arc<Error> },
607}
608
609impl ProcedureState {
610    /// Returns a [ProcedureState] with failed state.
611    pub fn failed(error: Arc<Error>) -> ProcedureState {
612        ProcedureState::Failed { error }
613    }
614
615    /// Returns a [ProcedureState] with prepare rollback state.
616    pub fn prepare_rollback(error: Arc<Error>) -> ProcedureState {
617        ProcedureState::PrepareRollback { error }
618    }
619
620    /// Returns a [ProcedureState] with rolling back state.
621    pub fn rolling_back(error: Arc<Error>) -> ProcedureState {
622        ProcedureState::RollingBack { error }
623    }
624
625    /// Returns a [ProcedureState] with retrying state.
626    pub fn retrying(error: Arc<Error>) -> ProcedureState {
627        ProcedureState::Retrying { error }
628    }
629
630    /// Returns a [ProcedureState] with poisoned state.
631    pub fn poisoned(keys: PoisonKeys, error: Arc<Error>) -> ProcedureState {
632        ProcedureState::Poisoned { keys, error }
633    }
634
635    /// Returns true if the procedure state is running.
636    pub fn is_running(&self) -> bool {
637        matches!(self, ProcedureState::Running)
638    }
639
640    /// Returns true if the procedure state is done.
641    pub fn is_done(&self) -> bool {
642        matches!(self, ProcedureState::Done { .. })
643    }
644
645    /// Returns true if the procedure state is poisoned.
646    pub fn is_poisoned(&self) -> bool {
647        matches!(self, ProcedureState::Poisoned { .. })
648    }
649
650    /// Returns true if the procedure state failed.
651    pub fn is_failed(&self) -> bool {
652        matches!(self, ProcedureState::Failed { .. })
653    }
654
655    /// Returns true if the procedure state is retrying.
656    pub fn is_retrying(&self) -> bool {
657        matches!(self, ProcedureState::Retrying { .. })
658    }
659
660    /// Returns true if the procedure state is rolling back.
661    pub fn is_rolling_back(&self) -> bool {
662        matches!(self, ProcedureState::RollingBack { .. })
663    }
664
665    /// Returns true if the procedure state is prepare rollback.
666    pub fn is_prepare_rollback(&self) -> bool {
667        matches!(self, ProcedureState::PrepareRollback { .. })
668    }
669
670    /// Returns the error.
671    pub fn error(&self) -> Option<&Arc<Error>> {
672        match self {
673            ProcedureState::Failed { error } => Some(error),
674            ProcedureState::Retrying { error } => Some(error),
675            ProcedureState::RollingBack { error } => Some(error),
676            ProcedureState::Poisoned { error, .. } => Some(error),
677            _ => None,
678        }
679    }
680
681    /// Return the string values of the enum field names.
682    pub fn as_str_name(&self) -> &str {
683        match self {
684            ProcedureState::Running => "Running",
685            ProcedureState::Done { .. } => "Done",
686            ProcedureState::Retrying { .. } => "Retrying",
687            ProcedureState::Failed { .. } => "Failed",
688            ProcedureState::PrepareRollback { .. } => "PrepareRollback",
689            ProcedureState::RollingBack { .. } => "RollingBack",
690            ProcedureState::Poisoned { .. } => "Poisoned",
691        }
692    }
693}
694
695/// The initial procedure state.
696#[derive(Debug, Clone)]
697pub enum InitProcedureState {
698    Running,
699    RollingBack,
700}
701
702// TODO(yingwen): Shutdown
703/// `ProcedureManager` executes [Procedure] submitted to it.
704#[async_trait]
705pub trait ProcedureManager: Send + Sync + 'static {
706    /// Registers loader for specific procedure type `name`.
707    fn register_loader(&self, name: &str, loader: BoxedProcedureLoader) -> Result<()>;
708
709    /// Starts the background GC task.
710    ///
711    /// Recovers unfinished procedures and reruns them.
712    ///
713    /// Callers should ensure all loaders are registered.
714    async fn start(&self) -> Result<()>;
715
716    /// Stops the background GC task.
717    async fn stop(&self) -> Result<()>;
718
719    /// Submits a procedure to execute.
720    ///
721    /// Returns a [Watcher] to watch the created procedure.
722    async fn submit(&self, procedure: ProcedureWithId) -> Result<Watcher>;
723
724    /// Query the procedure state.
725    ///
726    /// Returns `Ok(None)` if the procedure doesn't exist.
727    async fn procedure_state(&self, procedure_id: ProcedureId) -> Result<Option<ProcedureState>>;
728
729    /// Returns a [Watcher] to watch [ProcedureState] of specific procedure.
730    fn procedure_watcher(&self, procedure_id: ProcedureId) -> Option<Watcher>;
731
732    /// Returns the details of the procedure.
733    async fn list_procedures(&self) -> Result<Vec<ProcedureInfo>>;
734
735    /// Returns whether persisted unfinished procedures contain any requested type.
736    ///
737    /// This inspects durable state without submitting procedures for execution.
738    async fn has_unfinished_procedure(&self, type_names: &[&str]) -> Result<bool>;
739}
740
741/// Ref-counted pointer to the [ProcedureManager].
742pub type ProcedureManagerRef = Arc<dyn ProcedureManager>;
743
744#[derive(Debug, Clone)]
745pub struct ProcedureInfo {
746    /// Id of this procedure.
747    pub id: ProcedureId,
748    /// Type name of this procedure.
749    pub type_name: String,
750    /// Start execution time of this procedure.
751    pub start_time_ms: i64,
752    /// End execution time of this procedure.
753    pub end_time_ms: i64,
754    /// status of this procedure.
755    pub state: ProcedureState,
756    /// Lock keys of this procedure.
757    pub lock_keys: Vec<String>,
758}
759
760#[cfg(test)]
761mod tests {
762    use async_trait::async_trait;
763    use common_error::mock::MockError;
764    use common_error::status_code::StatusCode;
765
766    use super::*;
767
768    struct DefaultEventProcedure;
769
770    #[async_trait]
771    impl Procedure for DefaultEventProcedure {
772        fn type_name(&self) -> &str {
773            "default_event"
774        }
775
776        async fn execute(&mut self, _: &Context) -> Result<Status> {
777            Ok(Status::done())
778        }
779
780        fn dump(&self) -> Result<String> {
781            Ok(String::new())
782        }
783
784        fn lock_key(&self) -> LockKey {
785            LockKey::default()
786        }
787    }
788
789    #[test]
790    fn test_default_procedure_event_hook() {
791        let state = ProcedureState::Running;
792        let context = EventContext {
793            procedure_id: ProcedureId::random(),
794            lifecycle_state: &state,
795            trigger: EventTrigger::Succeeded,
796            event_type_filter: Arc::new(common_event_recorder::EventTypeFilter::All),
797            event_context: None,
798        };
799
800        assert!(DefaultEventProcedure.event(&context).is_none());
801        assert!(Box::new(DefaultEventProcedure).event(&context).is_none());
802    }
803
804    #[test]
805    fn test_status() {
806        let status = Status::executing(false);
807        assert!(!status.need_persist());
808
809        let status = Status::executing(true);
810        assert!(status.need_persist());
811
812        let status = Status::executing_with_clean_poisons(false);
813        assert!(status.need_clean_poisons());
814
815        let status = Status::executing_with_clean_poisons(true);
816        assert!(status.need_clean_poisons());
817
818        let status = Status::Suspended {
819            recovery: SubprocedureRecovery::Discard,
820            subprocedures: Vec::new(),
821            persist: false,
822        };
823        assert!(!status.need_persist());
824
825        let status = Status::Suspended {
826            recovery: SubprocedureRecovery::Discard,
827            subprocedures: Vec::new(),
828            persist: true,
829        };
830        assert!(status.need_persist());
831
832        let status = Status::done();
833        assert!(!status.need_persist());
834        assert!(status.need_clean_poisons());
835    }
836
837    #[test]
838    fn test_lock_key() {
839        let entity = "catalog.schema.my_table";
840        let key = LockKey::single_exclusive(entity);
841        assert_eq!(
842            vec![&StringKey::Exclusive(entity.to_string())],
843            key.keys_to_lock().collect::<Vec<_>>()
844        );
845
846        let key = LockKey::new_exclusive([
847            "b".to_string(),
848            "c".to_string(),
849            "a".to_string(),
850            "c".to_string(),
851        ]);
852        assert_eq!(
853            vec![
854                &StringKey::Exclusive("a".to_string()),
855                &StringKey::Exclusive("b".to_string()),
856                &StringKey::Exclusive("c".to_string())
857            ],
858            key.keys_to_lock().collect::<Vec<_>>()
859        );
860    }
861
862    #[test]
863    fn test_procedure_id() {
864        let id = ProcedureId::random();
865        let uuid_str = id.to_string();
866        assert_eq!(id.0.to_string(), uuid_str);
867
868        let parsed = ProcedureId::parse_str(&uuid_str).unwrap();
869        assert_eq!(id, parsed);
870        let parsed = uuid_str.parse().unwrap();
871        assert_eq!(id, parsed);
872    }
873
874    #[test]
875    fn test_procedure_id_serialization() {
876        let id = ProcedureId::random();
877        let json = serde_json::to_string(&id).unwrap();
878        assert_eq!(format!("\"{id}\""), json);
879
880        let parsed = serde_json::from_str(&json).unwrap();
881        assert_eq!(id, parsed);
882    }
883
884    #[test]
885    fn test_procedure_state() {
886        assert!(ProcedureState::Running.is_running());
887        assert!(ProcedureState::Running.error().is_none());
888        assert!(ProcedureState::Done { output: None }.is_done());
889
890        let state = ProcedureState::failed(Arc::new(Error::external(MockError::new(
891            StatusCode::Unexpected,
892        ))));
893        assert!(state.is_failed());
894        let _ = state.error().unwrap();
895    }
896}