Skip to main content

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