Skip to main content

common_meta/reconciliation/
event.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;
16
17use api::v1::value::ValueData;
18use api::v1::{ColumnSchema, Row};
19use common_event_recorder::Event;
20use common_event_recorder::error::{Result, SerializeEventSnafu};
21use common_event_recorder::event_table::{
22    CATALOG_NAME_COLUMN, PHYSICAL_TABLE_ID_COLUMN, SCHEMA_NAME_COLUMN, TABLE_ID_COLUMN,
23    TABLE_NAME_COLUMN, column_schemas, nullable_string, nullable_value,
24};
25use serde::Serialize;
26use snafu::ResultExt;
27use store_api::storage::TableId;
28
29use crate::reconciliation::ResolveStrategy;
30
31/// Stable event type stored for logical table reconciliation procedures.
32pub(crate) const RECONCILE_LOGICAL_TABLES_EVENT_TYPE: &str = "reconcile_logical_tables";
33/// Stable event type stored for physical table reconciliation procedures.
34pub(crate) const RECONCILE_TABLE_EVENT_TYPE: &str = "reconcile_table";
35const PAYLOAD_VERSION: u8 = 1;
36
37/// Nullable object locators shared by all reconciliation event types.
38#[derive(Debug, Clone, Default, PartialEq, Eq)]
39pub(crate) struct ReconciliationLocator {
40    catalog_name: Option<String>,
41    schema_name: Option<String>,
42    table_name: Option<String>,
43    table_id: Option<TableId>,
44    physical_table_id: Option<TableId>,
45}
46
47impl ReconciliationLocator {
48    /// Creates a locator for a physical table with its fully qualified name and ID.
49    pub(crate) fn physical_table(
50        catalog_name: &str,
51        schema_name: &str,
52        table_name: &str,
53        table_id: TableId,
54    ) -> Self {
55        Self {
56            catalog_name: Some(catalog_name.to_string()),
57            schema_name: Some(schema_name.to_string()),
58            table_name: Some(table_name.to_string()),
59            table_id: Some(table_id),
60            ..Default::default()
61        }
62    }
63
64    /// Creates a locator that links a logical table to its physical table.
65    pub(crate) fn logical_table(
66        catalog_name: &str,
67        schema_name: &str,
68        table_name: &str,
69        table_id: TableId,
70        physical_table_id: TableId,
71    ) -> Self {
72        Self {
73            catalog_name: Some(catalog_name.to_string()),
74            schema_name: Some(schema_name.to_string()),
75            table_name: Some(table_name.to_string()),
76            table_id: Some(table_id),
77            physical_table_id: Some(physical_table_id),
78        }
79    }
80
81    fn schema() -> Vec<ColumnSchema> {
82        column_schemas([
83            &CATALOG_NAME_COLUMN,
84            &SCHEMA_NAME_COLUMN,
85            &TABLE_NAME_COLUMN,
86            &TABLE_ID_COLUMN,
87            &PHYSICAL_TABLE_ID_COLUMN,
88        ])
89    }
90
91    fn row(&self) -> Row {
92        Row {
93            values: vec![
94                nullable_string(self.catalog_name.as_deref()),
95                nullable_string(self.schema_name.as_deref()),
96                nullable_string(self.table_name.as_deref()),
97                nullable_table_id(self.table_id),
98                nullable_table_id(self.physical_table_id),
99            ],
100        }
101    }
102}
103
104#[derive(Debug, Serialize)]
105#[serde(untagged)]
106enum ReconcileTablePayload {
107    Submitted(TableSubmittedPayload),
108    Result(TableResultPayload),
109}
110
111#[derive(Debug, Serialize)]
112struct TableSubmittedPayload {
113    version: u8,
114    resolve_strategy: &'static str,
115    is_subprocedure: bool,
116}
117
118#[derive(Debug, Serialize)]
119struct TableResultPayload {
120    version: u8,
121    complete: bool,
122    metadata_state: Option<&'static str>,
123    resolution_strategy_applied: Option<&'static str>,
124    resolved_column_count: Option<usize>,
125    scanned_region_count: usize,
126    updated_region_count: usize,
127    table_info_updated: bool,
128    last_completed_phase: Option<&'static str>,
129}
130
131/// Event representation for physical table reconciliation.
132#[derive(Debug)]
133pub(crate) struct ReconcileTableEvent {
134    locator: ReconciliationLocator,
135    payload: Option<ReconcileTablePayload>,
136}
137
138impl ReconcileTableEvent {
139    /// Builds the bounded intent event emitted when table reconciliation is submitted.
140    pub(crate) fn table_submitted(
141        locator: ReconciliationLocator,
142        resolve_strategy: ResolveStrategy,
143        is_subprocedure: bool,
144    ) -> Self {
145        Self {
146            locator,
147            payload: Some(ReconcileTablePayload::Submitted(TableSubmittedPayload {
148                version: PAYLOAD_VERSION,
149                resolve_strategy: resolve_strategy_name(resolve_strategy),
150                is_subprocedure,
151            })),
152        }
153    }
154
155    /// Builds a terminal event from the bounded reconciliation result summary.
156    #[allow(clippy::too_many_arguments)]
157    pub(crate) fn table_result(
158        locator: ReconciliationLocator,
159        complete: bool,
160        metadata_state: Option<&'static str>,
161        resolution_strategy_applied: Option<ResolveStrategy>,
162        resolved_column_count: Option<usize>,
163        scanned_region_count: usize,
164        updated_region_count: usize,
165        table_info_updated: bool,
166        last_completed_phase: Option<&'static str>,
167    ) -> Self {
168        Self {
169            locator,
170            payload: Some(ReconcileTablePayload::Result(TableResultPayload {
171                version: PAYLOAD_VERSION,
172                complete,
173                metadata_state,
174                resolution_strategy_applied: resolution_strategy_applied.map(resolve_strategy_name),
175                resolved_column_count,
176                scanned_region_count,
177                updated_region_count,
178                table_info_updated,
179                last_completed_phase,
180            })),
181        }
182    }
183
184    /// Builds a table lifecycle event whose reconciliation payload is null.
185    pub(crate) fn table_lifecycle(locator: ReconciliationLocator) -> Self {
186        Self {
187            locator,
188            payload: None,
189        }
190    }
191}
192
193impl Event for ReconcileTableEvent {
194    fn event_type(&self) -> &str {
195        RECONCILE_TABLE_EVENT_TYPE
196    }
197
198    fn json_payload(&self) -> Result<serde_json::Value> {
199        match &self.payload {
200            Some(payload) => serde_json::to_value(payload).context(SerializeEventSnafu),
201            None => Ok(serde_json::Value::Null),
202        }
203    }
204
205    fn extra_schema(&self) -> Vec<ColumnSchema> {
206        ReconciliationLocator::schema()
207    }
208
209    fn extra_rows(&self) -> Result<Vec<Row>> {
210        Ok(vec![self.locator.row()])
211    }
212
213    fn as_any(&self) -> &dyn Any {
214        self
215    }
216}
217
218#[derive(Debug, Serialize)]
219#[serde(untagged)]
220enum ReconcileLogicalTablesPayload {
221    Submitted(LogicalTablesSubmittedPayload),
222    Result(LogicalTablesResultPayload),
223}
224
225#[derive(Debug, Serialize)]
226struct LogicalTablesSubmittedPayload {
227    version: u8,
228    logical_table_count: usize,
229    is_subprocedure: bool,
230}
231
232#[derive(Debug, Serialize)]
233struct LogicalTablesResultPayload {
234    version: u8,
235    complete: bool,
236    /// Number of logical tables in the request, not a count of completed repairs.
237    processed_table_count: usize,
238    metadata_consistent_table_count: usize,
239    metadata_inconsistent_table_count: usize,
240    /// Tables identified for creation by the existing resolution metrics.
241    create_table_count: usize,
242    update_table_info_count: usize,
243}
244
245/// Event representation for logical table reconciliation.
246#[derive(Debug)]
247pub(crate) struct ReconcileLogicalTablesEvent {
248    locators: Vec<ReconciliationLocator>,
249    payload: Option<ReconcileLogicalTablesPayload>,
250}
251
252impl ReconcileLogicalTablesEvent {
253    /// Builds the bounded intent event emitted when logical table reconciliation is submitted.
254    pub(crate) fn submitted(locators: Vec<ReconciliationLocator>, is_subprocedure: bool) -> Self {
255        let logical_table_count = locators.len();
256        Self {
257            locators,
258            payload: Some(ReconcileLogicalTablesPayload::Submitted(
259                LogicalTablesSubmittedPayload {
260                    version: PAYLOAD_VERSION,
261                    logical_table_count,
262                    is_subprocedure,
263                },
264            )),
265        }
266    }
267
268    /// Builds a terminal event from persistent table IDs and existing volatile metrics.
269    ///
270    /// Metrics are best-effort observations from the current process. They reset on recovery
271    /// and do not account for every partial region or table-info update.
272    pub(crate) fn result(
273        locators: Vec<ReconciliationLocator>,
274        complete: bool,
275        processed_table_count: usize,
276        metadata_consistent_table_count: usize,
277        metadata_inconsistent_table_count: usize,
278        create_table_count: usize,
279        update_table_info_count: usize,
280    ) -> Self {
281        Self {
282            locators,
283            payload: Some(ReconcileLogicalTablesPayload::Result(
284                LogicalTablesResultPayload {
285                    version: PAYLOAD_VERSION,
286                    complete,
287                    processed_table_count,
288                    metadata_consistent_table_count,
289                    metadata_inconsistent_table_count,
290                    create_table_count,
291                    update_table_info_count,
292                },
293            )),
294        }
295    }
296
297    /// Builds a lifecycle event whose reconciliation payload is null.
298    pub(crate) fn lifecycle(locators: Vec<ReconciliationLocator>) -> Self {
299        Self {
300            locators,
301            payload: None,
302        }
303    }
304}
305
306impl Event for ReconcileLogicalTablesEvent {
307    fn event_type(&self) -> &str {
308        RECONCILE_LOGICAL_TABLES_EVENT_TYPE
309    }
310
311    fn json_payload(&self) -> Result<serde_json::Value> {
312        match &self.payload {
313            Some(payload) => serde_json::to_value(payload).context(SerializeEventSnafu),
314            None => Ok(serde_json::Value::Null),
315        }
316    }
317
318    fn extra_schema(&self) -> Vec<ColumnSchema> {
319        ReconciliationLocator::schema()
320    }
321
322    fn extra_rows(&self) -> Result<Vec<Row>> {
323        Ok(self
324            .locators
325            .iter()
326            .map(ReconciliationLocator::row)
327            .collect())
328    }
329
330    fn as_any(&self) -> &dyn Any {
331        self
332    }
333}
334
335fn resolve_strategy_name(strategy: ResolveStrategy) -> &'static str {
336    match strategy {
337        ResolveStrategy::UseLatest => "use_latest",
338        ResolveStrategy::UseMetasrv => "use_metasrv",
339        ResolveStrategy::AbortOnConflict => "abort_on_conflict",
340    }
341}
342
343fn nullable_table_id(value: Option<TableId>) -> api::v1::Value {
344    nullable_value(value.map(ValueData::U32Value))
345}
346
347#[cfg(test)]
348mod tests {
349    use api::v1::value::ValueData;
350    use api::v1::{ColumnDataType, Row, SemanticType, Value};
351    use common_event_recorder::Event;
352    use serde_json::json;
353
354    use super::*;
355
356    #[test]
357    fn reconciliation_events_use_the_shared_locator_contract() {
358        let table = ReconcileTableEvent::table_lifecycle(ReconciliationLocator::physical_table(
359            "greptime", "public", "metrics", 42,
360        ));
361        assert_eq!(table.event_type(), RECONCILE_TABLE_EVENT_TYPE);
362        assert_eq!(
363            table
364                .extra_schema()
365                .into_iter()
366                .map(|column| {
367                    (
368                        column.column_name,
369                        ColumnDataType::try_from(column.datatype).unwrap(),
370                        SemanticType::try_from(column.semantic_type).unwrap(),
371                    )
372                })
373                .collect::<Vec<_>>(),
374            vec![
375                (
376                    "catalog_name".to_string(),
377                    ColumnDataType::String,
378                    SemanticType::Field
379                ),
380                (
381                    "schema_name".to_string(),
382                    ColumnDataType::String,
383                    SemanticType::Field
384                ),
385                (
386                    "table_name".to_string(),
387                    ColumnDataType::String,
388                    SemanticType::Field
389                ),
390                (
391                    "table_id".to_string(),
392                    ColumnDataType::Uint32,
393                    SemanticType::Field
394                ),
395                (
396                    "physical_table_id".to_string(),
397                    ColumnDataType::Uint32,
398                    SemanticType::Field,
399                ),
400            ]
401        );
402        assert_eq!(
403            table.extra_rows().unwrap(),
404            vec![Row {
405                values: vec![
406                    ValueData::StringValue("greptime".to_string()).into(),
407                    ValueData::StringValue("public".to_string()).into(),
408                    ValueData::StringValue("metrics".to_string()).into(),
409                    ValueData::U32Value(42).into(),
410                    Value::default(),
411                ],
412            }]
413        );
414        assert_eq!(table.json_payload().unwrap(), serde_json::Value::Null);
415
416        let logical_tables = ReconcileLogicalTablesEvent::lifecycle(vec![
417            ReconciliationLocator::logical_table("greptime", "public", "cpu", 43, 42),
418            ReconciliationLocator::logical_table("greptime", "public", "memory", 44, 42),
419        ]);
420        assert_eq!(
421            logical_tables.event_type(),
422            RECONCILE_LOGICAL_TABLES_EVENT_TYPE
423        );
424        assert_eq!(logical_tables.extra_schema(), table.extra_schema());
425        assert_eq!(
426            logical_tables.extra_rows().unwrap(),
427            vec![
428                Row {
429                    values: vec![
430                        ValueData::StringValue("greptime".to_string()).into(),
431                        ValueData::StringValue("public".to_string()).into(),
432                        ValueData::StringValue("cpu".to_string()).into(),
433                        ValueData::U32Value(43).into(),
434                        ValueData::U32Value(42).into(),
435                    ],
436                },
437                Row {
438                    values: vec![
439                        ValueData::StringValue("greptime".to_string()).into(),
440                        ValueData::StringValue("public".to_string()).into(),
441                        ValueData::StringValue("memory".to_string()).into(),
442                        ValueData::U32Value(44).into(),
443                        ValueData::U32Value(42).into(),
444                    ],
445                },
446            ]
447        );
448        assert_eq!(
449            logical_tables.json_payload().unwrap(),
450            serde_json::Value::Null
451        );
452    }
453
454    #[test]
455    fn submitted_payloads_are_versioned_and_use_stable_strategy_names() {
456        for (strategy, expected) in [
457            (ResolveStrategy::UseLatest, "use_latest"),
458            (ResolveStrategy::UseMetasrv, "use_metasrv"),
459            (ResolveStrategy::AbortOnConflict, "abort_on_conflict"),
460        ] {
461            let table = ReconcileTableEvent::table_submitted(
462                ReconciliationLocator::physical_table("greptime", "public", "metrics", 42),
463                strategy,
464                true,
465            );
466            assert_eq!(
467                table.json_payload().unwrap(),
468                json!({
469                    "version": 1,
470                    "resolve_strategy": expected,
471                    "is_subprocedure": true,
472                })
473            );
474        }
475
476        let logical_tables = ReconcileLogicalTablesEvent::submitted(
477            vec![
478                ReconciliationLocator::logical_table("greptime", "public", "cpu", 43, 42),
479                ReconciliationLocator::logical_table("greptime", "public", "memory", 44, 42),
480            ],
481            true,
482        );
483        assert_eq!(
484            logical_tables.json_payload().unwrap(),
485            json!({
486                "version": 1,
487                "logical_table_count": 2,
488                "is_subprocedure": true,
489            })
490        );
491    }
492
493    #[test]
494    fn terminal_payloads_distinguish_complete_and_partial_results() {
495        let table = ReconcileTableEvent::table_result(
496            ReconciliationLocator::physical_table("greptime", "public", "metrics", 42),
497            false,
498            Some("inconsistent"),
499            Some(ResolveStrategy::UseMetasrv),
500            Some(4),
501            3,
502            2,
503            true,
504            Some("update_table_info"),
505        );
506        assert_eq!(
507            table.json_payload().unwrap(),
508            json!({
509                "version": 1,
510                "complete": false,
511                "metadata_state": "inconsistent",
512                "resolution_strategy_applied": "use_metasrv",
513                "resolved_column_count": 4,
514                "scanned_region_count": 3,
515                "updated_region_count": 2,
516                "table_info_updated": true,
517                "last_completed_phase": "update_table_info",
518            })
519        );
520
521        let logical_tables = ReconcileLogicalTablesEvent::result(
522            vec![ReconciliationLocator::logical_table(
523                "greptime", "public", "cpu", 43, 42,
524            )],
525            true,
526            1,
527            0,
528            0,
529            1,
530            0,
531        );
532        assert_eq!(
533            logical_tables.json_payload().unwrap(),
534            json!({
535                "version": 1,
536                "complete": true,
537                "processed_table_count": 1,
538                "metadata_consistent_table_count": 0,
539                "metadata_inconsistent_table_count": 0,
540                "create_table_count": 1,
541                "update_table_info_count": 0,
542            })
543        );
544    }
545}