Skip to main content

metric_engine/engine/sync/
region.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::time::Instant;
16
17use common_error::ext::BoxedError;
18use common_telemetry::info;
19use mito2::manifest::action::RegionEdit;
20use snafu::{OptionExt, ResultExt, ensure};
21use store_api::region_engine::{MitoCopyRegionFromRequest, SyncRegionFromResponse};
22use store_api::storage::RegionId;
23
24use crate::engine::MetricEngineInner;
25use crate::error::{
26    MissingFilesSnafu, MitoCopyRegionFromOperationSnafu, MitoEditRegionSnafu,
27    PhysicalRegionNotFoundSnafu, Result,
28};
29use crate::utils;
30
31impl MetricEngineInner {
32    /// Syncs the logical regions from the source region to the target region in the metric engine.
33    ///
34    /// This operation:
35    /// 1. Copies SST files from source metadata region to target metadata region
36    /// 2. Transforms logical region metadata (updates region numbers to match target)
37    /// 3. Edits target manifest to remove old file entries (copied files)
38    /// 4. Recovers states and returns newly opened logical region IDs
39    ///
40    /// **Note**: Only the metadata region is synced. The data region is not affected.
41    pub(crate) async fn sync_region_from_region(
42        &self,
43        region_id: RegionId,
44        source_region_id: RegionId,
45        parallelism: usize,
46    ) -> Result<SyncRegionFromResponse> {
47        let source_metadata_region_id = utils::to_metadata_region_id(source_region_id);
48        let target_metadata_region_id = utils::to_metadata_region_id(region_id);
49        let target_data_region_id = utils::to_data_region_id(region_id);
50        let source_data_region_id = utils::to_data_region_id(source_region_id);
51        info!(
52            "Syncing region from region {} to region {}, parallelism: {}",
53            source_region_id, region_id, parallelism
54        );
55
56        let res = self
57            .mito
58            .copy_region_from(
59                target_metadata_region_id,
60                MitoCopyRegionFromRequest {
61                    source_region_id: source_metadata_region_id,
62                    parallelism,
63                },
64            )
65            .await
66            .map_err(BoxedError::new)
67            .context(MitoCopyRegionFromOperationSnafu {
68                source_region_id: source_metadata_region_id,
69                target_region_id: target_metadata_region_id,
70            })?;
71
72        if res.copied_file_ids.is_empty() {
73            info!(
74                "No files were copied from source region {} to target region {}, copied file ids are empty",
75                source_metadata_region_id, target_metadata_region_id
76            );
77            return Ok(SyncRegionFromResponse::Metric {
78                metadata_synced: false,
79                data_synced: false,
80                new_opened_logical_region_ids: vec![],
81            });
82        }
83
84        let target_region = self.mito.find_region(target_metadata_region_id).context(
85            PhysicalRegionNotFoundSnafu {
86                region_id: target_metadata_region_id,
87            },
88        )?;
89        let files_to_remove = target_region.file_metas(&res.copied_file_ids).await;
90        let missing_file_ids = res
91            .copied_file_ids
92            .iter()
93            .zip(&files_to_remove)
94            .filter_map(|(file_id, maybe_meta)| {
95                if maybe_meta.is_none() {
96                    Some(*file_id)
97                } else {
98                    None
99                }
100            })
101            .collect::<Vec<_>>();
102        // `copy_region_from` does not trigger compaction,
103        // so there should be no files removed and thus no missing files.
104        ensure!(
105            missing_file_ids.is_empty(),
106            MissingFilesSnafu {
107                region_id: target_metadata_region_id,
108                file_ids: missing_file_ids,
109            }
110        );
111        let files_to_remove = files_to_remove.into_iter().flatten().collect::<Vec<_>>();
112        // Transform the logical region metadata of the target data region.
113        self.metadata_region
114            .transform_logical_region_metadata(target_data_region_id, source_data_region_id)
115            .await?;
116
117        let edit = RegionEdit {
118            files_to_add: vec![],
119            files_to_remove: files_to_remove.clone(),
120            timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
121            compaction_time_window: None,
122            flushed_entry_id: None,
123            flushed_sequence: None,
124            committed_sequence: None,
125            wal_recovery: Default::default(),
126        };
127        self.mito
128            .edit_region(target_metadata_region_id, edit)
129            .await
130            .map_err(BoxedError::new)
131            .context(MitoEditRegionSnafu {
132                region_id: target_metadata_region_id,
133            })?;
134        info!(
135            "Successfully edit metadata region: {} after syncing from source metadata region: {}, files to remove: {:?}",
136            target_metadata_region_id,
137            source_metadata_region_id,
138            files_to_remove
139                .iter()
140                .map(|meta| meta.file_id)
141                .collect::<Vec<_>>(),
142        );
143
144        let now = Instant::now();
145        // Always recover states from the target metadata region after syncing
146        // from the source metadata region.
147        let physical_region_options = *self
148            .state
149            .read()
150            .unwrap()
151            .physical_region_states()
152            .get(&target_data_region_id)
153            .context(PhysicalRegionNotFoundSnafu {
154                region_id: target_data_region_id,
155            })?
156            .options();
157        let new_opened_logical_region_ids = self
158            .recover_states(target_data_region_id, physical_region_options)
159            .await?;
160        info!(
161            "Sync metadata region from source region {} to target region {}, recover states cost: {:?}, new opened logical region ids: {:?}",
162            source_metadata_region_id,
163            target_metadata_region_id,
164            now.elapsed(),
165            new_opened_logical_region_ids
166        );
167
168        Ok(SyncRegionFromResponse::Metric {
169            metadata_synced: true,
170            data_synced: false,
171            new_opened_logical_region_ids,
172        })
173    }
174}
175
176#[cfg(test)]
177mod tests {
178
179    use common_error::ext::ErrorExt;
180    use common_error::status_code::StatusCode;
181    use common_telemetry::debug;
182    use store_api::metric_engine_consts::{METRIC_ENGINE_NAME, PHYSICAL_TABLE_METADATA_KEY};
183    use store_api::region_engine::{RegionEngine, SyncRegionFromRequest};
184    use store_api::region_request::{
185        BatchRegionDdlRequest, PathType, RegionCloseRequest, RegionFlushRequest, RegionOpenRequest,
186        RegionRequest,
187    };
188    use store_api::storage::RegionId;
189
190    use crate::metadata_region::MetadataRegion;
191    use crate::test_util::{TestEnv, create_logical_region_request};
192
193    async fn assert_logical_table_columns(
194        metadata_region: &MetadataRegion,
195        physical_region_id: RegionId,
196        logical_region_id: RegionId,
197        expected_columns: &[&str],
198    ) {
199        let mut columns = metadata_region
200            .logical_columns(physical_region_id, logical_region_id)
201            .await
202            .unwrap()
203            .into_iter()
204            .map(|(n, _)| n)
205            .collect::<Vec<_>>();
206        columns.sort_unstable();
207        assert_eq!(columns, expected_columns);
208    }
209
210    #[tokio::test]
211    async fn test_sync_region_from_region() {
212        common_telemetry::init_default_ut_logging();
213        let env = TestEnv::new().await;
214        let metric_engine = env.metric();
215        let source_physical_region_id = RegionId::new(1024, 0);
216        let logical_region_id1 = RegionId::new(1025, 0);
217        let logical_region_id2 = RegionId::new(1026, 0);
218        env.create_physical_region(source_physical_region_id, "/test_dir1", vec![])
219            .await;
220        let region_create_request1 =
221            create_logical_region_request(&["job"], source_physical_region_id, "logical1");
222        let region_create_request2 =
223            create_logical_region_request(&["host"], source_physical_region_id, "logical2");
224        metric_engine
225            .handle_batch_ddl_requests(BatchRegionDdlRequest::Create(vec![
226                (logical_region_id1, region_create_request1),
227                (logical_region_id2, region_create_request2),
228            ]))
229            .await
230            .unwrap();
231        debug!("Flushing source physical region");
232        metric_engine
233            .handle_request(
234                source_physical_region_id,
235                RegionRequest::Flush(RegionFlushRequest::default()),
236            )
237            .await
238            .unwrap();
239        let logical_regions = metric_engine
240            .logical_regions(source_physical_region_id)
241            .await
242            .unwrap();
243        assert!(logical_regions.contains(&logical_region_id1));
244        assert!(logical_regions.contains(&logical_region_id2));
245
246        let target_physical_region_id = RegionId::new(1024, 1);
247        let target_logical_region_id1 = RegionId::new(1025, 1);
248        let target_logical_region_id2 = RegionId::new(1026, 1);
249        // Prepare target physical region
250        env.create_physical_region(target_physical_region_id, "/test_dir1", vec![])
251            .await;
252        let r = metric_engine
253            .sync_region(
254                target_physical_region_id,
255                SyncRegionFromRequest::FromRegion {
256                    source_region_id: source_physical_region_id,
257                    parallelism: 1,
258                },
259            )
260            .await
261            .unwrap();
262        let new_opened_logical_region_ids = r.new_opened_logical_region_ids().unwrap();
263        assert_eq!(new_opened_logical_region_ids.len(), 2);
264        assert!(new_opened_logical_region_ids.contains(&target_logical_region_id1));
265        assert!(new_opened_logical_region_ids.contains(&target_logical_region_id2));
266        debug!("Sync region from again");
267        assert_logical_table_columns(
268            &env.metadata_region(),
269            target_physical_region_id,
270            target_logical_region_id1,
271            &["greptime_timestamp", "greptime_value", "job"],
272        )
273        .await;
274        assert_logical_table_columns(
275            &env.metadata_region(),
276            target_physical_region_id,
277            target_logical_region_id2,
278            &["greptime_timestamp", "greptime_value", "host"],
279        )
280        .await;
281        let logical_regions = env
282            .metadata_region()
283            .logical_regions(target_physical_region_id)
284            .await
285            .unwrap();
286        assert_eq!(logical_regions.len(), 2);
287        assert!(logical_regions.contains(&target_logical_region_id1));
288        assert!(logical_regions.contains(&target_logical_region_id2));
289
290        // Should be ok to sync region from again.
291        let r = metric_engine
292            .sync_region(
293                target_physical_region_id,
294                SyncRegionFromRequest::FromRegion {
295                    source_region_id: source_physical_region_id,
296                    parallelism: 1,
297                },
298            )
299            .await
300            .unwrap();
301        let new_opened_logical_region_ids = r.new_opened_logical_region_ids().unwrap();
302        assert!(new_opened_logical_region_ids.is_empty());
303
304        // Try to close region and reopen it, should be ok.
305        metric_engine
306            .handle_request(
307                target_physical_region_id,
308                RegionRequest::Close(RegionCloseRequest::default()),
309            )
310            .await
311            .unwrap();
312        let physical_region_option = [(PHYSICAL_TABLE_METADATA_KEY.to_string(), String::new())]
313            .into_iter()
314            .collect();
315        metric_engine
316            .handle_request(
317                target_physical_region_id,
318                RegionRequest::Open(RegionOpenRequest {
319                    engine: METRIC_ENGINE_NAME.to_string(),
320                    table_dir: "/test_dir1".to_string(),
321                    path_type: PathType::Bare,
322                    options: physical_region_option,
323                    skip_wal_replay: false,
324                    checkpoint: None,
325                    requirements: Default::default(),
326                }),
327            )
328            .await
329            .unwrap();
330        let logical_regions = env
331            .metadata_region()
332            .logical_regions(target_physical_region_id)
333            .await
334            .unwrap();
335        assert_eq!(logical_regions.len(), 2);
336        assert!(logical_regions.contains(&target_logical_region_id1));
337        assert!(logical_regions.contains(&target_logical_region_id2));
338    }
339
340    #[tokio::test]
341    async fn test_sync_region_from_region_with_no_files() {
342        common_telemetry::init_default_ut_logging();
343        let env = TestEnv::new().await;
344        let metric_engine = env.metric();
345        let source_physical_region_id = RegionId::new(1024, 0);
346        env.create_physical_region(source_physical_region_id, "/test_dir1", vec![])
347            .await;
348        let target_physical_region_id = RegionId::new(1024, 1);
349        env.create_physical_region(target_physical_region_id, "/test_dir1", vec![])
350            .await;
351        let r = metric_engine
352            .sync_region(
353                target_physical_region_id,
354                SyncRegionFromRequest::FromRegion {
355                    source_region_id: source_physical_region_id,
356                    parallelism: 1,
357                },
358            )
359            .await
360            .unwrap();
361        let new_opened_logical_region_ids = r.new_opened_logical_region_ids().unwrap();
362        assert!(new_opened_logical_region_ids.is_empty());
363    }
364
365    #[tokio::test]
366    async fn test_sync_region_from_region_source_not_exist() {
367        common_telemetry::init_default_ut_logging();
368        let env = TestEnv::new().await;
369        let metric_engine = env.metric();
370        let source_physical_region_id = RegionId::new(1024, 0);
371        let target_physical_region_id = RegionId::new(1024, 1);
372        env.create_physical_region(target_physical_region_id, "/test_dir1", vec![])
373            .await;
374        let err = metric_engine
375            .sync_region(
376                target_physical_region_id,
377                SyncRegionFromRequest::FromRegion {
378                    source_region_id: source_physical_region_id,
379                    parallelism: 1,
380                },
381            )
382            .await
383            .unwrap_err();
384        assert_eq!(err.status_code(), StatusCode::InvalidArguments);
385    }
386}