feat: clean up soft-dropped regions offline (#8458)

* fix(meta): skip reopening dropped tables during purge

Purge soft-dropped tables by dropping stored routes directly instead of
reopening tombstoned regions first. Treat legacy
\`PurgeDroppedTableState::OpenRegions\` snapshots as a compatibility-only
transition to \`DropRegions\`.

Files:
- \`src/common/meta/src/ddl/purge_dropped_table.rs\`
- \`src/common/meta/src/ddl/undrop_table.rs\`
- \`src/common/meta/src/ddl/tests/drop_table.rs\`

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix(meta): guard undrop restore race

Serialize `UNDROP TABLE` with same-name creates by seeding the
original table name before procedure submission, and clean up reopened
regions when metadata restore fails.

Files:
- `src/common/meta/src/ddl/undrop_table.rs`
- `src/common/meta/src/ddl_manager.rs`
- `src/common/meta/src/ddl/tests/drop_table.rs`

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* feat: clean up soft-dropped regions offline

Use an explicit RegionCleanUp request for purge-table cleanup so tombstoned regions can be removed without reopening them.

Route cleanup through datanode, Mito, and metric-engine offline paths, including WAL obsoletion and region directory removal.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix(datanode): reject cleanup for registered regions

Return `RegionBusy` when `RegionCleanUp` targets a region already tracked by the datanode, so offline cleanup only runs for regions without a local mapping.

Add coverage for `OfflineCleanup` engine selection and registered-region rejection.

Files:
- `src/datanode/src/region_server.rs`

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix(meta): require tombstone before undrop

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix(meta): reject file-engine soft drop

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix: harden soft-drop cleanup paths

Reject `RegionCleanUp` for already-open Mito regions instead of turning cleanup into a drop.

Make `UndropTableProcedure` tolerate missing persisted table names and always deregister failure detectors after restore-failure cleanup.

Files:
- `src/common/meta/src/ddl/undrop_table.rs`
- `src/mito2/src/engine/open_test.rs`
- `src/mito2/src/worker/handle_open.rs`

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* chore: bump cleanup proto dependency

Bump \`greptime-proto\` to the reviewed cleanup RPC revision and align cleanup request parsing and dispatch with the renamed \`CleanUpRequest\` payload.

Files:
- \`Cargo.toml\`
- \`Cargo.lock\`
- \`src/store-api/src/region_request.rs\`
- \`src/common/meta/src/ddl/drop_table/executor.rs\`

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* refactor: share region cleanup helpers

Share common region cleanup helpers between normal drop and offline cleanup
while keeping their preconditions separate.

- Extract shared dropped-region runtime cleanup for `handle_drop_request` and `handle_offline_cleanup_request`.
- Share runtime artifact and manifest cache cleanup after full deletion paths.
- Make full-drop directory removal policy explicit: full drop and purge cleanup force physical deletion, while partial drop may defer to global GC.

Files:
- `src/mito2/src/worker/handle_drop.rs`
- `src/mito2/src/worker/handle_open.rs`

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* chore: preserve soft-drop cleanup split state

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* refactor: remove obsolete CleanUp match arm from RegionRequest

The `CleanUp` variant in the `region_request::Body` match is now handled
exclusively by `RegionServer` via a separate path. This arm would have
returned an unexpected error, so removing it eliminates dead code.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix(meta): clean every soft-dropped region replica

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* fix(meta): order soft-drop replica cleanup

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* Revert "fix(meta): order soft-drop replica cleanup"

This reverts commit e77162d3e5ebcf2817e2845a6a5177c328fb2c60.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* Revert "fix(meta): clean every soft-dropped region replica"

This reverts commit 2378e00cc258ca1b6a85a1aafbd68c79c666f43c.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

---------

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>
This commit is contained in:
Lei, HUANG
2026-07-14 08:39:36 +00:00
committed by GitHub
parent 0beeef7fc0
commit 81d8beee3a
18 changed files with 1229 additions and 267 deletions
+135 -4
View File
@@ -1214,6 +1214,24 @@ impl RegionServerInner {
})?
.clone(),
},
RegionChange::OfflineCleanup(attribute) => match current_region_status {
Some(status) => match status.clone() {
RegionEngineWithStatus::Registering(_)
| RegionEngineWithStatus::Deregistering(_)
| RegionEngineWithStatus::Ready(_) => {
return error::RegionBusySnafu { region_id }.fail();
}
},
None => self
.engines
.read()
.unwrap()
.get(attribute.engine())
.with_context(|| RegionEngineNotFoundSnafu {
name: attribute.engine(),
})?
.clone(),
},
RegionChange::Deregisters => match current_region_status {
Some(status) => match status.clone() {
RegionEngineWithStatus::Registering(_) => {
@@ -1559,6 +1577,10 @@ impl RegionServerInner {
let attribute = parse_region_attribute(&open.engine, &open.options)?;
RegionChange::Register(attribute)
}
RegionRequest::CleanUp(clean_up) => {
let attribute = parse_region_attribute(&clean_up.engine, &clean_up.options)?;
RegionChange::OfflineCleanup(attribute)
}
RegionRequest::Close(_) | RegionRequest::Drop(_) => RegionChange::Deregisters,
RegionRequest::Put(_) | RegionRequest::Delete(_) | RegionRequest::BulkInserts(_) => {
RegionChange::Ingest
@@ -1678,7 +1700,7 @@ impl RegionServerInner {
region_change: RegionChange,
) {
match region_change {
RegionChange::None | RegionChange::Ingest => {}
RegionChange::None | RegionChange::Ingest | RegionChange::OfflineCleanup(_) => {}
RegionChange::Register(_) => {
self.region_map.remove(&region_id);
}
@@ -1698,7 +1720,7 @@ impl RegionServerInner {
) -> Result<()> {
let engine_type = engine.name();
match region_change {
RegionChange::None | RegionChange::Ingest => {}
RegionChange::None | RegionChange::Ingest | RegionChange::OfflineCleanup(_) => {}
RegionChange::Register(attribute) => {
info!(
"Region {region_id} is registered to engine {}",
@@ -1883,6 +1905,7 @@ impl RegionServerInner {
enum RegionChange {
None,
Register(RegionAttribute),
OfflineCleanup(RegionAttribute),
Deregisters,
Catchup,
Ingest,
@@ -1947,8 +1970,8 @@ mod tests {
use store_api::metadata::{ColumnMetadata, RegionMetadata, RegionMetadataBuilder};
use store_api::region_engine::RegionEngine;
use store_api::region_request::{
PathType, RegionCompactRequest, RegionDeleteRequest, RegionDropRequest, RegionOpenRequest,
RegionPutRequest, RegionTruncateRequest,
PathType, RegionCleanUpRequest, RegionCompactRequest, RegionDeleteRequest,
RegionDropRequest, RegionOpenRequest, RegionPutRequest, RegionTruncateRequest,
};
use store_api::storage::RegionId;
@@ -2119,6 +2142,77 @@ mod tests {
assert!(pinned.as_ref().get_ref().metrics().is_none());
}
#[tokio::test]
async fn test_offline_cleanup_does_not_register_region() {
let mut mock_region_server = mock_region_server();
let (engine, mut receiver) = MockRegionEngine::new(MITO_ENGINE_NAME);
mock_region_server.register_engine(engine);
let region_id = RegionId::new(1, 1);
let response = mock_region_server
.handle_request(
region_id,
RegionRequest::CleanUp(RegionCleanUpRequest {
engine: MITO_ENGINE_NAME.to_string(),
table_dir: String::new(),
path_type: PathType::Bare,
options: HashMap::new(),
}),
)
.await
.unwrap();
assert_eq!(response.affected_rows, 0);
let (handled_region_id, handled_request) = receiver.try_recv().unwrap();
assert_eq!(handled_region_id, region_id);
assert_matches!(handled_request, RegionRequest::CleanUp(_));
assert!(
mock_region_server
.inner
.region_map
.get(&region_id)
.is_none()
);
}
#[tokio::test]
async fn test_offline_cleanup_rejects_registered_region() {
let mut mock_region_server = mock_region_server();
let (engine, mut receiver) = MockRegionEngine::new(MITO_ENGINE_NAME);
mock_region_server.register_engine(engine.clone());
let region_id = RegionId::new(1, 1);
mock_region_server
.inner
.region_map
.insert(region_id, RegionEngineWithStatus::Ready(engine));
let err = mock_region_server
.handle_request(
region_id,
RegionRequest::CleanUp(RegionCleanUpRequest {
engine: MITO_ENGINE_NAME.to_string(),
table_dir: String::new(),
path_type: PathType::Bare,
options: HashMap::new(),
}),
)
.await
.unwrap_err();
assert_eq!(err.status_code(), StatusCode::RegionBusy);
assert!(receiver.try_recv().is_err());
assert!(matches!(
mock_region_server
.inner
.region_map
.get(&region_id)
.unwrap()
.clone(),
RegionEngineWithStatus::Ready(_)
));
}
#[tokio::test]
async fn test_region_registering() {
common_telemetry::init_default_ut_logging();
@@ -2530,6 +2624,43 @@ mod tests {
assert_matches!(current_engine, CurrentEngine::Engine(_));
}),
},
// RegionChange::OfflineCleanup
CurrentEngineTest {
region_id,
current_region_status: None,
region_change: RegionChange::OfflineCleanup(RegionAttribute::Mito),
assert: Box::new(|result| {
let current_engine = result.unwrap();
assert_matches!(current_engine, CurrentEngine::Engine(_));
}),
},
CurrentEngineTest {
region_id,
current_region_status: Some(RegionEngineWithStatus::Registering(engine.clone())),
region_change: RegionChange::OfflineCleanup(RegionAttribute::Mito),
assert: Box::new(|result| {
let err = result.unwrap_err();
assert_eq!(err.status_code(), StatusCode::RegionBusy);
}),
},
CurrentEngineTest {
region_id,
current_region_status: Some(RegionEngineWithStatus::Deregistering(engine.clone())),
region_change: RegionChange::OfflineCleanup(RegionAttribute::Mito),
assert: Box::new(|result| {
let err = result.unwrap_err();
assert_eq!(err.status_code(), StatusCode::RegionBusy);
}),
},
CurrentEngineTest {
region_id,
current_region_status: Some(RegionEngineWithStatus::Ready(engine.clone())),
region_change: RegionChange::OfflineCleanup(RegionAttribute::Mito),
assert: Box::new(|result| {
let err = result.unwrap_err();
assert_eq!(err.status_code(), StatusCode::RegionBusy);
}),
},
];
for test in tests {