1pub(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 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 let metrics = result.into();
89 self.volatile_ctx.inflight_subprocedures.clear();
90 self.volatile_ctx.metrics += metrics;
91 }
92
93 Ok(())
94 }
95
96 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 pending_tables: Vec<(TableId, TableName)>,
135 pending_logical_tables: HashMap<TableId, Vec<(TableId, TableName)>>,
140 inflight_subprocedures: Vec<SubprocedureMeta>,
142 tables: Option<BoxStream<'static, Result<(String, TableNameValue)>>>,
144 metrics: ReconcileDatabaseMetrics,
146 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 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 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}