Skip to main content

common_meta/reconciliation/
reconcile_database.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
15pub(crate) mod end;
16pub(crate) mod reconcile_logical_tables;
17pub(crate) mod reconcile_tables;
18pub(crate) mod start;
19
20use std::any::Any;
21use std::collections::HashMap;
22use std::fmt::Debug;
23use std::time::Instant;
24
25use async_trait::async_trait;
26use common_error::ext::ErrorExt;
27use common_procedure::error::{FromJsonSnafu, ToJsonSnafu};
28use common_procedure::{
29    Context as ProcedureContext, Error as ProcedureError, EventContext, EventTrigger, LockKey,
30    Procedure, Result as ProcedureResult, Status,
31};
32use futures::stream::BoxStream;
33use serde::{Deserialize, Serialize};
34use snafu::ResultExt;
35use store_api::storage::TableId;
36use table::table_name::TableName;
37
38use crate::cache_invalidator::CacheInvalidatorRef;
39use crate::error::Result;
40use crate::key::TableMetadataManagerRef;
41use crate::key::table_name::TableNameValue;
42use crate::lock_key::{CatalogLock, SchemaLock};
43use crate::metrics;
44use crate::node_manager::NodeManagerRef;
45use crate::reconciliation::event::{
46    RECONCILE_DATABASE_EVENT_TYPE, ReconcileDatabaseEvent, ReconciliationLocator,
47};
48use crate::reconciliation::reconcile_database::start::ReconcileDatabaseStart;
49use crate::reconciliation::reconcile_table::resolve_column_metadata::ResolveStrategy;
50use crate::reconciliation::utils::{
51    Context, ReconcileDatabaseMetrics, SubprocedureMeta, wait_for_inflight_subprocedures,
52};
53pub(crate) const DEFAULT_PARALLELISM: usize = 64;
54
55pub(crate) struct ReconcileDatabaseContext {
56    pub node_manager: NodeManagerRef,
57    pub table_metadata_manager: TableMetadataManagerRef,
58    pub cache_invalidator: CacheInvalidatorRef,
59    persistent_ctx: PersistentContext,
60    volatile_ctx: VolatileContext,
61}
62
63impl ReconcileDatabaseContext {
64    pub fn new(ctx: Context, persistent_ctx: PersistentContext) -> Self {
65        Self {
66            node_manager: ctx.node_manager,
67            table_metadata_manager: ctx.table_metadata_manager,
68            cache_invalidator: ctx.cache_invalidator,
69            persistent_ctx,
70            volatile_ctx: VolatileContext::default(),
71        }
72    }
73
74    /// Waits for inflight subprocedures to complete.
75    pub(crate) async fn wait_for_inflight_subprocedures(
76        &mut self,
77        procedure_ctx: &ProcedureContext,
78    ) -> Result<()> {
79        if !self.volatile_ctx.inflight_subprocedures.is_empty() {
80            let result = wait_for_inflight_subprocedures(
81                procedure_ctx,
82                &self.volatile_ctx.inflight_subprocedures,
83                self.persistent_ctx.fail_fast,
84            )
85            .await?;
86
87            // Collects result into metrics
88            let metrics = result.into();
89            self.volatile_ctx.inflight_subprocedures.clear();
90            self.volatile_ctx.metrics += metrics;
91        }
92
93        Ok(())
94    }
95
96    /// Returns the immutable metrics.
97    pub(crate) fn metrics(&self) -> &ReconcileDatabaseMetrics {
98        &self.volatile_ctx.metrics
99    }
100}
101
102#[derive(Debug, Serialize, Deserialize)]
103pub(crate) struct PersistentContext {
104    catalog: String,
105    schema: String,
106    fail_fast: bool,
107    parallelism: usize,
108    resolve_strategy: ResolveStrategy,
109    is_subprocedure: bool,
110}
111
112impl PersistentContext {
113    pub fn new(
114        catalog: String,
115        schema: String,
116        fail_fast: bool,
117        parallelism: usize,
118        resolve_strategy: ResolveStrategy,
119        is_subprocedure: bool,
120    ) -> Self {
121        Self {
122            catalog,
123            schema,
124            fail_fast,
125            parallelism,
126            resolve_strategy,
127            is_subprocedure,
128        }
129    }
130}
131
132pub(crate) struct VolatileContext {
133    /// Stores pending physical tables.
134    pending_tables: Vec<(TableId, TableName)>,
135    /// Stores pending logical tables associated with each physical table.
136    ///
137    /// - Key: Table ID of the physical table.
138    /// - Value: Vector of (TableId, TableName) tuples representing logical tables belonging to the physical table.
139    pending_logical_tables: HashMap<TableId, Vec<(TableId, TableName)>>,
140    /// Stores inflight subprocedures.
141    inflight_subprocedures: Vec<SubprocedureMeta>,
142    /// Stores the stream of tables.
143    tables: Option<BoxStream<'static, Result<(String, TableNameValue)>>>,
144    /// The metrics of reconciling database.
145    metrics: ReconcileDatabaseMetrics,
146    /// The start time of the reconciliation.
147    start_time: Instant,
148}
149
150impl Default for VolatileContext {
151    fn default() -> Self {
152        Self {
153            pending_tables: vec![],
154            pending_logical_tables: HashMap::new(),
155            inflight_subprocedures: vec![],
156            tables: None,
157            metrics: ReconcileDatabaseMetrics::default(),
158            start_time: Instant::now(),
159        }
160    }
161}
162
163pub struct ReconcileDatabaseProcedure {
164    pub context: ReconcileDatabaseContext,
165    state: Box<dyn State>,
166}
167
168impl ReconcileDatabaseProcedure {
169    pub const TYPE_NAME: &'static str = "metasrv-procedure::ReconcileDatabase";
170
171    pub fn new(
172        ctx: Context,
173        catalog: String,
174        schema: String,
175        fail_fast: bool,
176        parallelism: usize,
177        resolve_strategy: ResolveStrategy,
178        is_subprocedure: bool,
179    ) -> Self {
180        let persistent_ctx = PersistentContext::new(
181            catalog,
182            schema,
183            fail_fast,
184            parallelism,
185            resolve_strategy,
186            is_subprocedure,
187        );
188        let context = ReconcileDatabaseContext::new(ctx, persistent_ctx);
189        let state = Box::new(ReconcileDatabaseStart);
190        Self { context, state }
191    }
192
193    pub(crate) fn from_json(ctx: Context, json: &str) -> ProcedureResult<Self> {
194        let ProcedureDataOwned {
195            state,
196            persistent_ctx,
197        } = serde_json::from_str(json).context(FromJsonSnafu)?;
198        let context = ReconcileDatabaseContext::new(ctx, persistent_ctx);
199        Ok(Self { context, state })
200    }
201}
202
203#[derive(Debug, Serialize)]
204struct ProcedureData<'a> {
205    state: &'a dyn State,
206    persistent_ctx: &'a PersistentContext,
207}
208
209#[derive(Debug, Deserialize)]
210struct ProcedureDataOwned {
211    state: Box<dyn State>,
212    persistent_ctx: PersistentContext,
213}
214
215#[async_trait]
216impl Procedure for ReconcileDatabaseProcedure {
217    fn type_name(&self) -> &str {
218        Self::TYPE_NAME
219    }
220
221    async fn execute(&mut self, _ctx: &ProcedureContext) -> ProcedureResult<Status> {
222        let state = &mut self.state;
223
224        let procedure_name = Self::TYPE_NAME;
225        let step = state.name();
226        let _timer = metrics::METRIC_META_RECONCILIATION_PROCEDURE
227            .with_label_values(&[procedure_name, step])
228            .start_timer();
229        match state.next(&mut self.context, _ctx).await {
230            Ok((next, status)) => {
231                *state = next;
232                Ok(status)
233            }
234            Err(e) => {
235                if e.retry_hint().is_retryable() {
236                    metrics::METRIC_META_RECONCILIATION_PROCEDURE_ERROR
237                        .with_label_values(&[procedure_name, step, metrics::ERROR_TYPE_RETRYABLE])
238                        .inc();
239                    Err(ProcedureError::retry_later(e))
240                } else {
241                    metrics::METRIC_META_RECONCILIATION_PROCEDURE_ERROR
242                        .with_label_values(&[procedure_name, step, metrics::ERROR_TYPE_EXTERNAL])
243                        .inc();
244                    Err(ProcedureError::external(e))
245                }
246            }
247        }
248    }
249
250    fn dump(&self) -> ProcedureResult<String> {
251        let data = ProcedureData {
252            state: self.state.as_ref(),
253            persistent_ctx: &self.context.persistent_ctx,
254        };
255        serde_json::to_string(&data).context(ToJsonSnafu)
256    }
257
258    fn lock_key(&self) -> LockKey {
259        let catalog = &self.context.persistent_ctx.catalog;
260        let schema = &self.context.persistent_ctx.schema;
261        // If the procedure is a subprocedure, only lock the schema.
262        if self.context.persistent_ctx.is_subprocedure {
263            return LockKey::new(vec![SchemaLock::write(catalog, schema).into()]);
264        }
265
266        LockKey::new(vec![
267            CatalogLock::Read(catalog).into(),
268            SchemaLock::write(catalog, schema).into(),
269        ])
270    }
271
272    fn event(&self, ctx: &EventContext<'_>) -> Option<Box<dyn common_event_recorder::Event>> {
273        if !ctx.event_type_filter.allows(RECONCILE_DATABASE_EVENT_TYPE) {
274            return None;
275        }
276
277        let persistent_ctx = &self.context.persistent_ctx;
278        let locator =
279            ReconciliationLocator::database(&persistent_ctx.catalog, &persistent_ctx.schema);
280        let event = match ctx.trigger {
281            EventTrigger::Submitted => ReconcileDatabaseEvent::submitted(
282                locator,
283                persistent_ctx.resolve_strategy,
284                persistent_ctx.fail_fast,
285                persistent_ctx.parallelism,
286                persistent_ctx.is_subprocedure,
287            ),
288            EventTrigger::Succeeded => self.result_event(locator, true),
289            EventTrigger::Failed | EventTrigger::Poisoned => self.result_event(locator, false),
290            _ => ReconcileDatabaseEvent::lifecycle(locator),
291        };
292        Some(Box::new(event))
293    }
294}
295
296impl ReconcileDatabaseProcedure {
297    fn result_event(
298        &self,
299        locator: ReconciliationLocator,
300        complete: bool,
301    ) -> ReconcileDatabaseEvent {
302        let metrics = self.context.metrics();
303        ReconcileDatabaseEvent::result(
304            locator,
305            complete,
306            metrics.succeeded_tables,
307            metrics.failed_tables,
308            metrics.succeeded_procedures,
309            metrics.failed_procedures,
310        )
311    }
312}
313
314#[async_trait::async_trait]
315#[typetag::serde(tag = "reconcile_database_state")]
316pub(crate) trait State: Sync + Send + Debug {
317    fn name(&self) -> &'static str {
318        let type_name = std::any::type_name::<Self>();
319        // short name
320        type_name.split("::").last().unwrap_or(type_name)
321    }
322
323    async fn next(
324        &mut self,
325        ctx: &mut ReconcileDatabaseContext,
326        procedure_ctx: &ProcedureContext,
327    ) -> Result<(Box<dyn State>, Status)>;
328
329    fn as_any(&self) -> &dyn Any;
330}
331
332#[cfg(test)]
333mod tests {
334    use std::sync::Arc;
335
336    use common_event_recorder::{EventTypeFilter, EventTypeFilterRef};
337    use common_procedure::{
338        ChildSubmissionOutcome, EventContext, EventTrigger, Procedure, ProcedureId, ProcedureState,
339        RetryPhase,
340    };
341    use serde_json::{Value, json};
342
343    use super::*;
344    use crate::reconciliation::event::RECONCILE_CATALOG_EVENT_TYPE;
345    use crate::test_util::{MockDatanodeManager, new_ddl_context};
346
347    struct DatabaseEventHarness {
348        procedure_id: ProcedureId,
349        lifecycle_state: ProcedureState,
350        event_type_filter: EventTypeFilterRef,
351    }
352
353    impl DatabaseEventHarness {
354        fn all() -> Self {
355            Self {
356                procedure_id: ProcedureId::random(),
357                lifecycle_state: ProcedureState::Running,
358                event_type_filter: Arc::new(EventTypeFilter::All),
359            }
360        }
361
362        fn selected(event_types: impl IntoIterator<Item = &'static str>) -> Self {
363            Self {
364                event_type_filter: Arc::new(EventTypeFilter::Only(
365                    event_types.into_iter().map(str::to_string).collect(),
366                )),
367                ..Self::all()
368            }
369        }
370
371        fn event(
372            &self,
373            procedure: &dyn Procedure,
374            trigger: EventTrigger,
375        ) -> Option<Box<dyn common_event_recorder::Event>> {
376            procedure.event(&EventContext {
377                procedure_id: self.procedure_id,
378                lifecycle_state: &self.lifecycle_state,
379                trigger,
380                event_type_filter: self.event_type_filter.clone(),
381                event_context: None,
382            })
383        }
384    }
385
386    #[test]
387    fn database_submitted_events_cover_root_and_child_intent() {
388        let events = DatabaseEventHarness::all();
389        let root = test_procedure(false);
390        let child = test_procedure(true);
391        for (procedure, is_subprocedure) in [(&root, false), (&child, true)] {
392            let submitted = events.event(procedure, EventTrigger::Submitted).unwrap();
393            assert_eq!(submitted.event_type(), RECONCILE_DATABASE_EVENT_TYPE);
394            assert_eq!(
395                submitted.json_payload().unwrap(),
396                json!({
397                    "version": 1,
398                    "resolve_strategy": "use_metasrv",
399                    "fail_fast": false,
400                    "parallelism": 8,
401                    "is_subprocedure": is_subprocedure,
402                })
403            );
404        }
405    }
406
407    #[test]
408    fn database_non_terminal_lifecycle_events_have_null_payloads() {
409        let events = DatabaseEventHarness::all();
410        let mut procedure = test_procedure(true);
411        procedure.context.volatile_ctx.metrics = populated_metrics();
412
413        for trigger in [
414            EventTrigger::Recovered,
415            EventTrigger::ChildSubmitted {
416                procedure_id: ProcedureId::random(),
417                outcome: ChildSubmissionOutcome::Accepted,
418            },
419            EventTrigger::Retrying {
420                phase: RetryPhase::Execute,
421                attempt: 2,
422            },
423            EventTrigger::RollingBack,
424        ] {
425            assert_eq!(
426                events
427                    .event(&procedure, trigger)
428                    .unwrap()
429                    .json_payload()
430                    .unwrap(),
431                Value::Null
432            );
433        }
434    }
435
436    #[test]
437    fn database_terminal_events_report_existing_metrics() {
438        let events = DatabaseEventHarness::all();
439        let mut procedure = test_procedure(true);
440        procedure.context.volatile_ctx.metrics = populated_metrics();
441
442        for (trigger, complete) in [
443            (EventTrigger::Succeeded, true),
444            (EventTrigger::Failed, false),
445            (EventTrigger::Poisoned, false),
446        ] {
447            assert_eq!(
448                events
449                    .event(&procedure, trigger)
450                    .unwrap()
451                    .json_payload()
452                    .unwrap(),
453                json!({
454                    "version": 1,
455                    "complete": complete,
456                    "processed_table_count": 8,
457                    "succeeded_table_count": 5,
458                    "failed_table_count": 3,
459                    "succeeded_subprocedure_count": 3,
460                    "failed_subprocedure_count": 2,
461                })
462            );
463        }
464    }
465
466    #[test]
467    fn database_event_filtering_uses_the_database_event_type() {
468        let procedure = test_procedure(false);
469        assert!(
470            DatabaseEventHarness::selected([RECONCILE_DATABASE_EVENT_TYPE])
471                .event(&procedure, EventTrigger::Submitted)
472                .is_some()
473        );
474        assert!(
475            DatabaseEventHarness::selected([RECONCILE_CATALOG_EVENT_TYPE])
476                .event(&procedure, EventTrigger::Submitted)
477                .is_none()
478        );
479        assert!(
480            DatabaseEventHarness::selected([])
481                .event(&procedure, EventTrigger::Submitted)
482                .is_none()
483        );
484    }
485
486    #[test]
487    fn database_recovery_preserves_locator_and_resets_metrics() {
488        let events = DatabaseEventHarness::all();
489        let mut procedure = test_procedure(false);
490        procedure.context.volatile_ctx.metrics = populated_metrics();
491        let original_dump = procedure.dump().unwrap();
492        procedure.context.volatile_ctx.metrics = ReconcileDatabaseMetrics::default();
493        assert_eq!(procedure.dump().unwrap(), original_dump);
494
495        let loaded = ReconcileDatabaseProcedure::from_json(test_context(), &original_dump).unwrap();
496        assert_eq!(loaded.dump().unwrap(), original_dump);
497        assert_eq!(
498            events
499                .event(&loaded, EventTrigger::Recovered)
500                .unwrap()
501                .extra_rows()
502                .unwrap(),
503            events
504                .event(&procedure, EventTrigger::Submitted)
505                .unwrap()
506                .extra_rows()
507                .unwrap(),
508        );
509        assert_eq!(
510            events
511                .event(&loaded, EventTrigger::Succeeded)
512                .unwrap()
513                .json_payload()
514                .unwrap(),
515            json!({
516                "version": 1,
517                "complete": true,
518                "processed_table_count": 0,
519                "succeeded_table_count": 0,
520                "failed_table_count": 0,
521                "succeeded_subprocedure_count": 0,
522                "failed_subprocedure_count": 0,
523            })
524        );
525    }
526
527    fn populated_metrics() -> ReconcileDatabaseMetrics {
528        ReconcileDatabaseMetrics {
529            succeeded_tables: 5,
530            failed_tables: 3,
531            succeeded_procedures: 3,
532            failed_procedures: 2,
533        }
534    }
535
536    fn test_procedure(is_subprocedure: bool) -> ReconcileDatabaseProcedure {
537        ReconcileDatabaseProcedure::new(
538            test_context(),
539            "greptime".to_string(),
540            "public".to_string(),
541            false,
542            8,
543            ResolveStrategy::UseMetasrv,
544            is_subprocedure,
545        )
546    }
547
548    fn test_context() -> Context {
549        let ddl_context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
550        Context {
551            node_manager: ddl_context.node_manager,
552            table_metadata_manager: ddl_context.table_metadata_manager,
553            cache_invalidator: ddl_context.cache_invalidator,
554        }
555    }
556}