Skip to main content

relational_engine/
engine.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::collections::HashMap;
17use std::sync::{Arc, RwLock};
18use std::time::Instant;
19
20use api::region::RegionResponse;
21use async_trait::async_trait;
22use common_catalog::consts::RELATIONAL_ENGINE;
23use common_error::ext::BoxedError;
24use common_telemetry::{error, info};
25use object_store::ObjectStore;
26use snafu::{OptionExt, ensure};
27use store_api::metadata::RegionMetadataRef;
28use store_api::region_engine::{
29    RegionEngine, RegionRole, RegionScannerRef, RegionStatistic, RemapManifestsRequest,
30    RemapManifestsResponse, SetRegionRoleStateResponse, SetRegionRoleStateSuccess,
31    SettableRegionRoleState, SyncRegionFromRequest, SyncRegionFromResponse,
32};
33use store_api::region_request::{
34    AffectedRows, RegionCloseRequest, RegionCreateRequest, RegionDropRequest, RegionOpenRequest,
35    RegionRequest, RegionRequirements,
36};
37use store_api::storage::{RegionId, ScanRequest, SequenceNumber};
38use tokio::sync::Mutex;
39
40use crate::error::{
41    RegionNotFoundSnafu, Result as EngineResult, UnexpectedEngineSnafu, UnsupportedSnafu,
42};
43use crate::manifest::RelationalRegionManifest;
44use crate::region::{RelationalRegion, RelationalRegionRef};
45
46pub struct RelationalEngine {
47    inner: EngineInnerRef,
48}
49
50impl RelationalEngine {
51    pub fn new(object_store: ObjectStore) -> Self {
52        Self {
53            inner: Arc::new(EngineInner::new(object_store)),
54        }
55    }
56}
57
58#[async_trait]
59impl RegionEngine for RelationalEngine {
60    fn name(&self) -> &str {
61        RELATIONAL_ENGINE
62    }
63
64    async fn handle_request(
65        &self,
66        region_id: RegionId,
67        request: RegionRequest,
68    ) -> Result<RegionResponse, BoxedError> {
69        self.inner
70            .handle_request(region_id, request)
71            .await
72            .map_err(BoxedError::new)
73    }
74
75    async fn handle_query(
76        &self,
77        _region_id: RegionId,
78        _request: ScanRequest,
79    ) -> Result<RegionScannerRef, BoxedError> {
80        Err(BoxedError::new(
81            UnsupportedSnafu { operation: "scan" }.build(),
82        ))
83    }
84
85    async fn get_metadata(&self, region_id: RegionId) -> Result<RegionMetadataRef, BoxedError> {
86        self.inner
87            .get_region(region_id)
88            .await
89            .map(|r| r.metadata())
90            .context(RegionNotFoundSnafu { region_id })
91            .map_err(BoxedError::new)
92    }
93
94    async fn stop(&self) -> Result<(), BoxedError> {
95        self.inner.stop().await.map_err(BoxedError::new)
96    }
97
98    fn region_statistic(&self, _: RegionId) -> Option<RegionStatistic> {
99        None
100    }
101
102    async fn get_committed_sequence(&self, _: RegionId) -> Result<SequenceNumber, BoxedError> {
103        Ok(Default::default())
104    }
105
106    fn set_region_role(&self, region_id: RegionId, role: RegionRole) -> Result<(), BoxedError> {
107        self.inner
108            .set_region_role(region_id, role)
109            .map_err(BoxedError::new)
110    }
111
112    /// Regions of this engine take no writes, so the lease is not needed.
113    fn set_region_role_with_lease(
114        &self,
115        region_id: RegionId,
116        role: RegionRole,
117        _sent_at: Instant,
118        _deadline: Instant,
119    ) -> Result<bool, BoxedError> {
120        self.set_region_role(region_id, role).map(|_| true)
121    }
122
123    async fn set_region_role_state_gracefully(
124        &self,
125        region_id: RegionId,
126        _region_role_state: SettableRegionRoleState,
127    ) -> Result<SetRegionRoleStateResponse, BoxedError> {
128        let exists = self.inner.get_region(region_id).await.is_some();
129
130        if exists {
131            Ok(SetRegionRoleStateResponse::success(
132                SetRegionRoleStateSuccess::relational(),
133            ))
134        } else {
135            Ok(SetRegionRoleStateResponse::NotFound)
136        }
137    }
138
139    async fn sync_region(
140        &self,
141        _region_id: RegionId,
142        _request: SyncRegionFromRequest,
143    ) -> Result<SyncRegionFromResponse, BoxedError> {
144        // Relational engine doesn't need to sync region manifest.
145        Ok(SyncRegionFromResponse::NotSupported)
146    }
147
148    async fn remap_manifests(
149        &self,
150        _request: RemapManifestsRequest,
151    ) -> Result<RemapManifestsResponse, BoxedError> {
152        Err(BoxedError::new(
153            UnsupportedSnafu {
154                operation: "remap_manifests",
155            }
156            .build(),
157        ))
158    }
159
160    fn role(&self, region_id: RegionId) -> Option<RegionRole> {
161        self.inner.state(region_id)
162    }
163
164    fn as_any(&self) -> &dyn Any {
165        self
166    }
167}
168
169struct EngineInner {
170    /// All regions opened by the engine.
171    ///
172    /// Writing to `regions` should also hold the `region_mutex`.
173    regions: RwLock<HashMap<RegionId, RelationalRegionRef>>,
174
175    /// Region mutex is used to protect the operations such as creating/opening/closing
176    /// a region, to avoid things like opening the same region simultaneously.
177    region_mutex: Mutex<()>,
178
179    object_store: ObjectStore,
180}
181
182type EngineInnerRef = Arc<EngineInner>;
183
184fn ensure_region_requirements(
185    requirements: RegionRequirements,
186    object_store: &ObjectStore,
187) -> EngineResult<()> {
188    if !requirements.object_storage {
189        return Ok(());
190    }
191
192    ensure!(
193        object_store::util::is_object_storage(object_store),
194        UnsupportedSnafu {
195            operation: "create or open region with object storage requirement on non-object storage"
196        }
197    );
198
199    Ok(())
200}
201
202impl EngineInner {
203    fn new(object_store: ObjectStore) -> Self {
204        Self {
205            regions: RwLock::new(HashMap::new()),
206            region_mutex: Mutex::new(()),
207            object_store,
208        }
209    }
210
211    async fn handle_request(
212        &self,
213        region_id: RegionId,
214        request: RegionRequest,
215    ) -> EngineResult<RegionResponse> {
216        let result = match request {
217            RegionRequest::Create(req) => self.handle_create(region_id, req).await,
218            RegionRequest::Drop(req) => self.handle_drop(region_id, req).await,
219            RegionRequest::Open(req) => self.handle_open(region_id, req).await,
220            RegionRequest::Close(req) => self.handle_close(region_id, req).await,
221            _ => UnsupportedSnafu {
222                operation: request.to_string(),
223            }
224            .fail(),
225        };
226        result.map(RegionResponse::new)
227    }
228
229    async fn stop(&self) -> EngineResult<()> {
230        let _lock = self.region_mutex.lock().await;
231        self.regions.write().unwrap().clear();
232        Ok(())
233    }
234
235    fn set_region_role(&self, _region_id: RegionId, _region_role: RegionRole) -> EngineResult<()> {
236        // TODO(zhongzc): Improve the semantics and implementation of this API.
237        Ok(())
238    }
239
240    fn state(&self, region_id: RegionId) -> Option<RegionRole> {
241        if self.regions.read().unwrap().get(&region_id).is_some() {
242            Some(RegionRole::Leader)
243        } else {
244            None
245        }
246    }
247}
248
249impl EngineInner {
250    async fn handle_create(
251        &self,
252        region_id: RegionId,
253        request: RegionCreateRequest,
254    ) -> EngineResult<AffectedRows> {
255        ensure!(
256            request.engine == RELATIONAL_ENGINE,
257            UnexpectedEngineSnafu {
258                engine: request.engine
259            }
260        );
261
262        // If the region is already open, validate the definition before treating the
263        // create as idempotent: a mismatched create must fail, not silently succeed.
264        if let Some(region) = self.get_region(region_id).await {
265            region.check_create_request(region_id, &request)?;
266            return Ok(0);
267        }
268
269        info!("Try to create region, region_id: {}", region_id);
270
271        let _lock = self.region_mutex.lock().await;
272        // Check again after acquiring the lock
273        if let Some(region) = self.get_region(region_id).await {
274            region.check_create_request(region_id, &request)?;
275            return Ok(0);
276        }
277
278        ensure_region_requirements(request.requirements, &self.object_store)?;
279
280        // The manifest is the source of truth for whether a region already
281        // exists. The in-memory `regions` map only tracks regions opened in
282        // this process, so after a restart it is empty even though the
283        // manifest was already persisted. Recover the region from the manifest
284        // instead of failing with `ManifestExists`, keeping `CREATE EXTERNAL
285        // TABLE` idempotent.
286        let region_dir = RelationalRegion::region_dir(&request.table_dir, region_id);
287        if RelationalRegionManifest::exists(&region_dir, &self.object_store).await? {
288            let manifest =
289                RelationalRegionManifest::load(region_id, &region_dir, &self.object_store).await?;
290            let region = RelationalRegion::from_manifest(&region_dir, manifest)?;
291            // The persisted definition must match the incoming request, otherwise a
292            // retried CREATE with a different schema would silently reuse the old
293            // manifest. Reject the mismatch instead.
294            region.check_create_request(region_id, &request)?;
295            self.regions.write().unwrap().insert(region_id, region);
296            info!(
297                "Region recovered from existing manifest, region_id: {}",
298                region_id
299            );
300            return Ok(0);
301        }
302
303        let res = RelationalRegion::create(region_id, request, &self.object_store).await;
304        let region = res.inspect_err(|err| {
305            error!(
306                err;
307                "Failed to create region, region_id: {}",
308                region_id
309            );
310        })?;
311        self.regions.write().unwrap().insert(region_id, region);
312
313        info!("A new region is created, region_id: {}", region_id);
314        Ok(0)
315    }
316
317    async fn handle_open(
318        &self,
319        region_id: RegionId,
320        request: RegionOpenRequest,
321    ) -> EngineResult<AffectedRows> {
322        if self.exists(region_id).await {
323            return Ok(0);
324        }
325
326        info!("Try to open region, region_id: {}", region_id);
327
328        let _lock = self.region_mutex.lock().await;
329        // Check again after acquiring the lock
330        if self.exists(region_id).await {
331            return Ok(0);
332        }
333
334        ensure_region_requirements(request.requirements, &self.object_store)?;
335
336        let res = RelationalRegion::open(region_id, request, &self.object_store).await;
337        let region = res.inspect_err(|err| {
338            error!(
339                err;
340                "Failed to open region, region_id: {}",
341                region_id
342            );
343        })?;
344        self.regions.write().unwrap().insert(region_id, region);
345
346        info!("Region opened, region_id: {}", region_id);
347        Ok(0)
348    }
349
350    async fn handle_close(
351        &self,
352        region_id: RegionId,
353        _request: RegionCloseRequest,
354    ) -> EngineResult<AffectedRows> {
355        let _lock = self.region_mutex.lock().await;
356
357        let mut regions = self.regions.write().unwrap();
358        if regions.remove(&region_id).is_some() {
359            info!("Region closed, region_id: {}", region_id);
360        }
361
362        Ok(0)
363    }
364
365    async fn handle_drop(
366        &self,
367        region_id: RegionId,
368        _request: RegionDropRequest,
369    ) -> EngineResult<AffectedRows> {
370        // Serialize against create/open/close so the region can't be closed
371        // between the existence check and the manifest deletion. A region that
372        // is no longer in memory (e.g. closed concurrently) is reported as not
373        // found instead of silently returning success with a leaked manifest.
374        let _lock = self.region_mutex.lock().await;
375
376        let region = self.get_region(region_id).await;
377        let Some(region) = region else {
378            return RegionNotFoundSnafu { region_id }.fail();
379        };
380
381        info!("Try to drop region, region_id: {}", region_id);
382
383        let res = RelationalRegion::drop(&region, &self.object_store).await;
384        res.inspect_err(|err| {
385            error!(
386                err;
387                "Failed to drop region, region_id: {}",
388                region_id
389            );
390        })?;
391
392        let _ = self.regions.write().unwrap().remove(&region_id);
393
394        info!("Region dropped, region_id: {}", region_id);
395        Ok(0)
396    }
397
398    async fn get_region(&self, region_id: RegionId) -> Option<RelationalRegionRef> {
399        self.regions.read().unwrap().get(&region_id).cloned()
400    }
401
402    async fn exists(&self, region_id: RegionId) -> bool {
403        self.regions.read().unwrap().contains_key(&region_id)
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use std::assert_matches;
410
411    use object_store::ObjectStore;
412    use object_store::services::{Fs, S3};
413    use store_api::region_request::{
414        PathType, RegionCreateRequest, RegionDropRequest, RegionRequirements,
415    };
416    use store_api::storage::RegionId;
417
418    use super::{EngineInner, ensure_region_requirements};
419    use crate::error::Error;
420    use crate::test_util::{new_test_column_metadata, new_test_object_store, new_test_options};
421
422    fn build_fs_object_store() -> ObjectStore {
423        ObjectStore::new(Fs::default().root("/tmp")).unwrap()
424    }
425
426    fn build_s3_object_store() -> ObjectStore {
427        ObjectStore::new(
428            S3::default()
429                .bucket("test-bucket")
430                .region("us-east-1")
431                .disable_ec2_metadata(),
432        )
433        .unwrap()
434    }
435
436    #[test]
437    fn test_empty_region_requirements_are_supported() {
438        ensure_region_requirements(RegionRequirements::empty(), &build_fs_object_store()).unwrap();
439    }
440
441    #[test]
442    fn test_object_storage_region_requirement_rejects_fs_object_store() {
443        let err = ensure_region_requirements(
444            RegionRequirements::object_storage(),
445            &build_fs_object_store(),
446        )
447        .unwrap_err();
448
449        assert!(matches!(err, Error::Unsupported { .. }));
450    }
451
452    #[test]
453    fn test_object_storage_region_requirement_accepts_s3_object_store() {
454        ensure_region_requirements(
455            RegionRequirements::object_storage(),
456            &build_s3_object_store(),
457        )
458        .unwrap();
459    }
460
461    #[tokio::test]
462    async fn test_drop_returns_not_found_when_region_absent() {
463        let (_dir, object_store) = new_test_object_store("test_drop_absent");
464        let inner = EngineInner::new(object_store);
465
466        let err = inner
467            .handle_drop(
468                RegionId::new(1, 0),
469                RegionDropRequest {
470                    fast_path: false,
471                    force: false,
472                    partial_drop: false,
473                },
474            )
475            .await
476            .unwrap_err();
477
478        assert_matches!(err, Error::RegionNotFound { .. });
479    }
480
481    #[tokio::test]
482    async fn test_drop_removes_manifest() {
483        let (_dir, object_store) = new_test_object_store("test_drop_removes_manifest");
484        let inner = EngineInner::new(object_store.clone());
485
486        let region_id = RegionId::new(1, 0);
487        let request = RegionCreateRequest {
488            engine: "relational".to_string(),
489            column_metadatas: new_test_column_metadata(),
490            primary_key: vec![1],
491            options: new_test_options(),
492            table_dir: "drop_dir/".to_string(),
493            path_type: PathType::Bare,
494            partition_expr_json: Some("".to_string()),
495            requirements: Default::default(),
496        };
497
498        inner.handle_create(region_id, request).await.unwrap();
499
500        let manifest_path = "drop_dir/1_0000000000/manifest/_relational_manifest";
501        assert!(object_store.exists(manifest_path).await.unwrap());
502
503        inner
504            .handle_drop(
505                region_id,
506                RegionDropRequest {
507                    fast_path: false,
508                    force: false,
509                    partial_drop: false,
510                },
511            )
512            .await
513            .unwrap();
514
515        assert!(!object_store.exists(manifest_path).await.unwrap());
516        assert!(!inner.exists(region_id).await);
517    }
518
519    #[tokio::test]
520    async fn test_create_is_idempotent_across_engine_rebuild() {
521        let (_dir, object_store) = new_test_object_store("test_create_idempotent");
522        let region_id = RegionId::new(1, 0);
523        let request = RegionCreateRequest {
524            engine: "relational".to_string(),
525            column_metadatas: new_test_column_metadata(),
526            primary_key: vec![1],
527            options: new_test_options(),
528            table_dir: "idempotent_dir/".to_string(),
529            path_type: PathType::Bare,
530            partition_expr_json: Some("".to_string()),
531            requirements: Default::default(),
532        };
533
534        // First create persists the manifest to object storage.
535        {
536            let inner = EngineInner::new(object_store.clone());
537            inner
538                .handle_create(region_id, request.clone())
539                .await
540                .unwrap();
541            assert!(inner.exists(region_id).await);
542        }
543
544        // Rebuild the engine on the same object store: the in-memory region
545        // map is empty, but the manifest is still persisted. Retrying the
546        // same create must recover from the manifest instead of failing.
547        let rebuilt = EngineInner::new(object_store.clone());
548        let affected = rebuilt.handle_create(region_id, request).await.unwrap();
549        assert_eq!(affected, 0);
550        assert!(rebuilt.exists(region_id).await);
551
552        let manifest_path = "idempotent_dir/1_0000000000/manifest/_relational_manifest";
553        assert!(object_store.exists(manifest_path).await.unwrap());
554    }
555
556    #[tokio::test]
557    async fn test_create_rejects_mismatched_definition() {
558        let (_dir, object_store) = new_test_object_store("test_create_mismatch");
559        let region_id = RegionId::new(1, 0);
560
561        let request = RegionCreateRequest {
562            engine: "relational".to_string(),
563            column_metadatas: new_test_column_metadata(),
564            primary_key: vec![1],
565            options: new_test_options(),
566            table_dir: "mismatch_dir/".to_string(),
567            path_type: PathType::Bare,
568            partition_expr_json: Some("".to_string()),
569            requirements: Default::default(),
570        };
571
572        // First create persists the manifest.
573        let inner = EngineInner::new(object_store.clone());
574        inner.handle_create(region_id, request).await.unwrap();
575
576        // Rebuild the engine and retry the create with a different primary key:
577        // the persisted definition no longer matches, so this must fail instead of
578        // silently reusing the old manifest.
579        let rebuilt = EngineInner::new(object_store.clone());
580        let mismatched = RegionCreateRequest {
581            engine: "relational".to_string(),
582            column_metadatas: new_test_column_metadata(),
583            primary_key: vec![2],
584            options: new_test_options(),
585            table_dir: "mismatch_dir/".to_string(),
586            path_type: PathType::Bare,
587            partition_expr_json: Some("".to_string()),
588            requirements: Default::default(),
589        };
590        let err = rebuilt
591            .handle_create(region_id, mismatched)
592            .await
593            .unwrap_err();
594        assert_matches!(err, Error::RegionExists { .. });
595    }
596
597    #[tokio::test]
598    async fn test_create_rejects_mismatched_definition_in_memory() {
599        let (_dir, object_store) = new_test_object_store("test_create_mismatch_in_memory");
600        let inner = EngineInner::new(object_store);
601        let region_id = RegionId::new(1, 0);
602
603        let request = RegionCreateRequest {
604            engine: "relational".to_string(),
605            column_metadatas: new_test_column_metadata(),
606            primary_key: vec![1],
607            options: new_test_options(),
608            table_dir: "mismatch_in_memory_dir/".to_string(),
609            path_type: PathType::Bare,
610            partition_expr_json: Some("".to_string()),
611            requirements: Default::default(),
612        };
613
614        inner
615            .handle_create(region_id, request.clone())
616            .await
617            .unwrap();
618
619        // An identical create is idempotent while the region stays in memory.
620        assert_eq!(inner.handle_create(region_id, request).await.unwrap(), 0);
621
622        // A mismatched create must fail, not silently return success.
623        let mismatched = RegionCreateRequest {
624            engine: "relational".to_string(),
625            column_metadatas: new_test_column_metadata(),
626            primary_key: vec![2],
627            options: new_test_options(),
628            table_dir: "mismatch_in_memory_dir/".to_string(),
629            path_type: PathType::Bare,
630            partition_expr_json: Some("".to_string()),
631            requirements: Default::default(),
632        };
633        let err = inner
634            .handle_create(region_id, mismatched)
635            .await
636            .unwrap_err();
637        assert_matches!(
638            err,
639            Error::RegionExists { reason, .. } if reason.contains("primary key")
640        );
641    }
642}