1use 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 schemas: Option<BoxStream<'static, Result<String>>>,
112 inflight_subprocedure: Option<SubprocedureMeta>,
114 metrics: ReconcileCatalogMetrics,
116 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 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}