Skip to content

Commit 978f295

Browse files
authored
fix: Handle concurrent gc_dropped_db_by_id and undrop_database operations safely (#17901)
* fix: Handle concurrent `gc_dropped_db_by_id` and `undrop_database` operations safely Add transaction condition to ensure proper behavior when `gc_dropped_db_by_id` and `undrop_database` are executed concurrently on the same database, by checking that that database meta being cleaned is marked as dropped. * tweak comments * tweak UT
1 parent 12af1b0 commit 978f295

File tree

2 files changed

+85
-0
lines changed

2 files changed

+85
-0
lines changed

src/meta/api/src/schema_api_impl.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3725,6 +3725,14 @@ async fn gc_dropped_db_by_id(
37253725
return Ok(());
37263726
};
37273727

3728+
if seq_db_meta.drop_on.is_none() {
3729+
// If db is not marked as dropped, just ignore the gc request and return directly.
3730+
// In subsequent KV transactions, we also verify that db_meta hasn't changed
3731+
// to ensure we don't reclaim metadata of the given database that might have been
3732+
// successfully undropped in a parallel operation.
3733+
return Ok(());
3734+
}
3735+
37283736
// TODO: enable this when gc_in_progress is set.
37293737
// if !seq_db_meta.gc_in_progress {
37303738
// let err = UnknownDatabaseId::new(
@@ -3778,6 +3786,9 @@ async fn gc_dropped_db_by_id(
37783786
.push(txn_put_pb(&db_id_history_ident, &db_id_list)?);
37793787
}
37803788

3789+
// Verify db_meta hasn't changed since we started this operation.
3790+
// This establishes a condition for the transaction that will prevent it from committing
3791+
// if the database metadata was modified by another concurrent operation (like un-dropping).
37813792
txn.condition.push(txn_cond_eq_seq(&dbid, seq_db_meta.seq));
37823793
txn.if_then.push(txn_op_del(&dbid));
37833794
txn.condition

src/meta/api/src/schema_api_test_suite.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ impl SchemaApiTestSuite {
337337
suite.table_index_create_drop(&b.build().await).await?;
338338
suite.index_create_list_drop(&b.build().await).await?;
339339
suite.table_lock_revision(&b.build().await).await?;
340+
suite.gc_dropped_db_after_undrop(&b.build().await).await?;
340341
suite.catalog_create_get_list_drop(&b.build().await).await?;
341342
suite.table_least_visible_time(&b.build().await).await?;
342343
suite
@@ -6946,6 +6947,79 @@ impl SchemaApiTestSuite {
69466947
Ok(())
69476948
}
69486949

6950+
async fn gc_dropped_db_after_undrop<MT: SchemaApi + kvapi::AsKVApi<Error = MetaError>>(
6951+
self,
6952+
mt: &MT,
6953+
) -> anyhow::Result<()> {
6954+
let tenant_name = "tenant1_gc_dropped_db_after_undrop";
6955+
let db_name = "db1_gc_dropped_db_after_undrop";
6956+
let mut util = Util::new(mt, tenant_name, db_name, "", "eng");
6957+
6958+
let tenant = Tenant::new_or_err(tenant_name, func_name!())?;
6959+
let db_name_ident = DatabaseNameIdent::new(&tenant, db_name);
6960+
6961+
// 1. Create database
6962+
util.create_db().await?;
6963+
let db_id = util.db_id;
6964+
6965+
info!("Created database with ID: {}", db_id);
6966+
6967+
// 2. Drop database
6968+
util.drop_db().await?;
6969+
6970+
// 2.1. Check database is marked as dropped
6971+
let req = ListDroppedTableReq::new(&tenant);
6972+
let resp = mt.get_drop_table_infos(req).await?;
6973+
6974+
// Filter for our specific database ID
6975+
let drop_ids: Vec<DroppedId> = resp
6976+
.drop_ids
6977+
.into_iter()
6978+
.filter(|id| {
6979+
if let DroppedId::Db { db_id: id, .. } = id {
6980+
*id == db_id
6981+
} else {
6982+
false
6983+
}
6984+
})
6985+
.collect();
6986+
6987+
assert!(
6988+
!drop_ids.is_empty(),
6989+
"Database being tested should be dropped"
6990+
);
6991+
6992+
// 3. Undrop the database
6993+
//
6994+
// A more rigorous test would verify the race condition protection, but difficult to implement as an integration test:
6995+
// Ideally, we would undrop the database precisely after the `gc_drop_tables` process has verified
6996+
// that the database is marked as dropped, but before committing the kv transaction that removes the database metadata.
6997+
6998+
let undrop_req = UndropDatabaseReq {
6999+
name_ident: db_name_ident.clone(),
7000+
};
7001+
mt.undrop_database(undrop_req).await?;
7002+
7003+
let req = GcDroppedTableReq {
7004+
tenant: tenant.clone(),
7005+
drop_ids,
7006+
};
7007+
7008+
// 4. Check that gc_drop_tables operation has NOT removed database's meta data
7009+
mt.gc_drop_tables(req.clone()).await?;
7010+
7011+
// 5. Verify the database is still accessible
7012+
let get_req = GetDatabaseReq::new(tenant.clone(), db_name.to_string());
7013+
let db_info = mt.get_database(get_req).await?;
7014+
assert_eq!(db_info.database_id.db_id, db_id, "Database ID should match");
7015+
assert!(
7016+
db_info.meta.drop_on.is_none(),
7017+
"Database should not be marked as dropped"
7018+
);
7019+
7020+
Ok(())
7021+
}
7022+
69497023
// pub async fn share_create_get_drop<MT: SchemaApi>(&self, mt: &MT) -> anyhow::Result<()> {
69507024
// let tenant1 = "tenant1";
69517025
// let share_name1 = "share1";

0 commit comments

Comments
 (0)