mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 07:57:01 +00:00
feat(table-catalog): retain protected ref metadata (#3394)
This commit is contained in:
@@ -875,6 +875,7 @@ where
|
|||||||
let current_metadata_location = catalog.table.metadata_location.clone();
|
let current_metadata_location = catalog.table.metadata_location.clone();
|
||||||
|
|
||||||
let mut retained = BTreeSet::new();
|
let mut retained = BTreeSet::new();
|
||||||
|
let mut current_metadata_for_refs = None;
|
||||||
let current_metadata_status =
|
let current_metadata_status =
|
||||||
if is_valid_table_metadata_location(&parsed_namespace, &parsed_table, ¤t_metadata_location) {
|
if is_valid_table_metadata_location(&parsed_namespace, &parsed_table, ¤t_metadata_location) {
|
||||||
retained.insert(current_metadata_location.clone());
|
retained.insert(current_metadata_location.clone());
|
||||||
@@ -883,6 +884,7 @@ where
|
|||||||
match serde_json::from_slice::<serde_json::Value>(¤t_metadata_object.data) {
|
match serde_json::from_slice::<serde_json::Value>(¤t_metadata_object.data) {
|
||||||
Ok(current_metadata) if current_metadata.is_object() => {
|
Ok(current_metadata) if current_metadata.is_object() => {
|
||||||
retained.extend(metadata_log_locations(¤t_metadata, &parsed_namespace, &parsed_table));
|
retained.extend(metadata_log_locations(¤t_metadata, &parsed_namespace, &parsed_table));
|
||||||
|
current_metadata_for_refs = Some(current_metadata);
|
||||||
TableMetadataPointerStatus::Valid
|
TableMetadataPointerStatus::Valid
|
||||||
}
|
}
|
||||||
Ok(_) | Err(_) => TableMetadataPointerStatus::InvalidJson,
|
Ok(_) | Err(_) => TableMetadataPointerStatus::InvalidJson,
|
||||||
@@ -908,6 +910,19 @@ where
|
|||||||
for metadata_location in metadata_locations.iter().rev().take(retain_recent_metadata_files) {
|
for metadata_location in metadata_locations.iter().rev().take(retain_recent_metadata_files) {
|
||||||
retained.insert(metadata_location.clone());
|
retained.insert(metadata_location.clone());
|
||||||
}
|
}
|
||||||
|
if let Some(current_metadata) = current_metadata_for_refs.as_ref() {
|
||||||
|
retained.extend(
|
||||||
|
metadata_locations_for_protected_snapshot_refs(
|
||||||
|
&self.backend,
|
||||||
|
table_bucket,
|
||||||
|
&parsed_namespace,
|
||||||
|
&parsed_table,
|
||||||
|
current_metadata,
|
||||||
|
&metadata_locations,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let orphan_metadata_candidate_locations = metadata_locations
|
let orphan_metadata_candidate_locations = metadata_locations
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -977,6 +992,17 @@ where
|
|||||||
for metadata_location in metadata_locations.iter().rev().take(retain_recent_metadata_files) {
|
for metadata_location in metadata_locations.iter().rev().take(retain_recent_metadata_files) {
|
||||||
retained.insert(metadata_location.clone());
|
retained.insert(metadata_location.clone());
|
||||||
}
|
}
|
||||||
|
retained.extend(
|
||||||
|
metadata_locations_for_protected_snapshot_refs(
|
||||||
|
&self.backend,
|
||||||
|
table_bucket,
|
||||||
|
&namespace,
|
||||||
|
&table,
|
||||||
|
¤t_metadata,
|
||||||
|
&metadata_locations,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
let cleanup_candidate_locations = metadata_locations
|
let cleanup_candidate_locations = metadata_locations
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -1080,6 +1106,17 @@ where
|
|||||||
let mut protected = metadata_log_locations(¤t_metadata, &namespace, &table);
|
let mut protected = metadata_log_locations(¤t_metadata, &namespace, &table);
|
||||||
protected.insert(entry.metadata_location.clone());
|
protected.insert(entry.metadata_location.clone());
|
||||||
protected.extend(report.retained_metadata_locations.iter().cloned());
|
protected.extend(report.retained_metadata_locations.iter().cloned());
|
||||||
|
protected.extend(
|
||||||
|
metadata_locations_for_protected_snapshot_refs(
|
||||||
|
&self.backend,
|
||||||
|
table_bucket,
|
||||||
|
&namespace,
|
||||||
|
&table,
|
||||||
|
¤t_metadata,
|
||||||
|
&report.cleanup_candidate_locations,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
let mut cleanup_candidate_locations = BTreeSet::new();
|
let mut cleanup_candidate_locations = BTreeSet::new();
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
@@ -1645,6 +1682,79 @@ fn metadata_log_locations(
|
|||||||
locations
|
locations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn metadata_locations_for_protected_snapshot_refs<B>(
|
||||||
|
backend: &B,
|
||||||
|
table_bucket: &str,
|
||||||
|
namespace: &Namespace,
|
||||||
|
table: &IdentifierSegment,
|
||||||
|
current_metadata: &serde_json::Value,
|
||||||
|
metadata_locations: &[String],
|
||||||
|
) -> TableCatalogStoreResult<BTreeSet<String>>
|
||||||
|
where
|
||||||
|
B: TableCatalogObjectBackend,
|
||||||
|
{
|
||||||
|
let protected_snapshot_ids = protected_ref_snapshot_ids(current_metadata);
|
||||||
|
if protected_snapshot_ids.is_empty() {
|
||||||
|
return Ok(BTreeSet::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut retained = BTreeSet::new();
|
||||||
|
for metadata_location in metadata_locations {
|
||||||
|
if !is_valid_table_metadata_location(namespace, table, metadata_location) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(metadata_object) = backend.read_object(table_bucket, metadata_location).await? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(metadata) = serde_json::from_slice::<serde_json::Value>(&metadata_object.data) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if metadata_contains_protected_snapshot_ref(&metadata, &protected_snapshot_ids) {
|
||||||
|
retained.insert(metadata_location.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(retained)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn protected_ref_snapshot_ids(current_metadata: &serde_json::Value) -> BTreeSet<i64> {
|
||||||
|
let mut snapshot_ids = BTreeSet::new();
|
||||||
|
let current_snapshot_id = current_metadata
|
||||||
|
.get("current-snapshot-id")
|
||||||
|
.and_then(serde_json::Value::as_i64);
|
||||||
|
let Some(refs) = current_metadata.get("refs").and_then(serde_json::Value::as_object) else {
|
||||||
|
return snapshot_ids;
|
||||||
|
};
|
||||||
|
|
||||||
|
for reference in refs.values() {
|
||||||
|
if let Some(snapshot_id) = reference.get("snapshot-id").and_then(serde_json::Value::as_i64)
|
||||||
|
&& Some(snapshot_id) != current_snapshot_id
|
||||||
|
{
|
||||||
|
snapshot_ids.insert(snapshot_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_contains_protected_snapshot_ref(metadata: &serde_json::Value, protected_snapshot_ids: &BTreeSet<i64>) -> bool {
|
||||||
|
let current_snapshot_matches = metadata
|
||||||
|
.get("current-snapshot-id")
|
||||||
|
.and_then(serde_json::Value::as_i64)
|
||||||
|
.is_some_and(|snapshot_id| protected_snapshot_ids.contains(&snapshot_id));
|
||||||
|
if current_snapshot_matches {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(refs) = metadata.get("refs").and_then(serde_json::Value::as_object) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
refs.values().any(|reference| {
|
||||||
|
reference
|
||||||
|
.get("snapshot-id")
|
||||||
|
.and_then(serde_json::Value::as_i64)
|
||||||
|
.is_some_and(|snapshot_id| protected_snapshot_ids.contains(&snapshot_id))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn metadata_candidate_is_past_safety_window(mod_time: Option<OffsetDateTime>, now: OffsetDateTime) -> bool {
|
fn metadata_candidate_is_past_safety_window(mod_time: Option<OffsetDateTime>, now: OffsetDateTime) -> bool {
|
||||||
let Some(mod_time) = mod_time else {
|
let Some(mod_time) = mod_time else {
|
||||||
return false;
|
return false;
|
||||||
@@ -2853,6 +2963,71 @@ mod tests {
|
|||||||
assert_eq!(report.cleanup_candidate_locations, vec![v1, v3]);
|
assert_eq!(report.cleanup_candidate_locations, vec![v1, v3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn maintenance_dry_run_keeps_metadata_for_protected_snapshot_refs() {
|
||||||
|
let backend = TestCatalogObjectBackend::default();
|
||||||
|
let store = ObjectTableCatalogStore::new(backend.clone());
|
||||||
|
let bucket = "analytics";
|
||||||
|
let namespace = Namespace::parse("sales").unwrap();
|
||||||
|
let table = IdentifierSegment::parse("orders").unwrap();
|
||||||
|
let orphan = default_table_metadata_file_path(&namespace, &table, "00001.metadata.json");
|
||||||
|
let tagged = default_table_metadata_file_path(&namespace, &table, "00002.metadata.json");
|
||||||
|
let unreferenced = default_table_metadata_file_path(&namespace, &table, "00003.metadata.json");
|
||||||
|
let current = default_table_metadata_file_path(&namespace, &table, "00004.metadata.json");
|
||||||
|
|
||||||
|
seed_table_for_metadata_maintenance(&store, bucket, &namespace, &table, current.clone()).await;
|
||||||
|
backend.seed_object(bucket, &orphan, b"{}".to_vec()).await;
|
||||||
|
backend
|
||||||
|
.seed_object(
|
||||||
|
bucket,
|
||||||
|
&tagged,
|
||||||
|
serde_json::to_vec(&serde_json::json!({
|
||||||
|
"current-snapshot-id": 10
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
backend
|
||||||
|
.seed_object(
|
||||||
|
bucket,
|
||||||
|
&unreferenced,
|
||||||
|
serde_json::to_vec(&serde_json::json!({
|
||||||
|
"current-snapshot-id": 20
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
backend
|
||||||
|
.seed_object(
|
||||||
|
bucket,
|
||||||
|
¤t,
|
||||||
|
serde_json::to_vec(&serde_json::json!({
|
||||||
|
"current-snapshot-id": 30,
|
||||||
|
"metadata-log": [],
|
||||||
|
"refs": {
|
||||||
|
"main": {
|
||||||
|
"snapshot-id": 30,
|
||||||
|
"type": "branch"
|
||||||
|
},
|
||||||
|
"audit": {
|
||||||
|
"snapshot-id": 10,
|
||||||
|
"type": "tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let report = store
|
||||||
|
.plan_table_metadata_maintenance(bucket, "sales", "orders", 0)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(report.retained_metadata_locations.contains(&tagged));
|
||||||
|
assert_eq!(report.cleanup_candidate_locations, vec![orphan, unreferenced]);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn maintenance_dry_run_keeps_recent_metadata_files_and_ignores_non_metadata_objects() {
|
async fn maintenance_dry_run_keeps_recent_metadata_files_and_ignores_non_metadata_objects() {
|
||||||
let backend = TestCatalogObjectBackend::default();
|
let backend = TestCatalogObjectBackend::default();
|
||||||
@@ -3103,6 +3278,53 @@ mod tests {
|
|||||||
assert_eq!(report.orphan_metadata_candidate_locations, vec![uncommitted]);
|
assert_eq!(report.orphan_metadata_candidate_locations, vec![uncommitted]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn orphan_metadata_scan_keeps_metadata_for_protected_snapshot_refs() {
|
||||||
|
let backend = TestCatalogObjectBackend::default();
|
||||||
|
let store = ObjectTableCatalogStore::new(backend.clone());
|
||||||
|
let bucket = "analytics";
|
||||||
|
let namespace = Namespace::parse("sales").unwrap();
|
||||||
|
let table = IdentifierSegment::parse("orders").unwrap();
|
||||||
|
let orphan = default_table_metadata_file_path(&namespace, &table, "00001.metadata.json");
|
||||||
|
let tagged = default_table_metadata_file_path(&namespace, &table, "00002.metadata.json");
|
||||||
|
let current = default_table_metadata_file_path(&namespace, &table, "00003.metadata.json");
|
||||||
|
|
||||||
|
seed_table_for_metadata_maintenance(&store, bucket, &namespace, &table, current.clone()).await;
|
||||||
|
backend.seed_object(bucket, &orphan, b"{}".to_vec()).await;
|
||||||
|
backend
|
||||||
|
.seed_object(
|
||||||
|
bucket,
|
||||||
|
&tagged,
|
||||||
|
serde_json::to_vec(&serde_json::json!({
|
||||||
|
"current-snapshot-id": 10
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
backend
|
||||||
|
.seed_object(
|
||||||
|
bucket,
|
||||||
|
¤t,
|
||||||
|
serde_json::to_vec(&serde_json::json!({
|
||||||
|
"current-snapshot-id": 20,
|
||||||
|
"metadata-log": [],
|
||||||
|
"refs": {
|
||||||
|
"audit": {
|
||||||
|
"snapshot-id": 10,
|
||||||
|
"type": "tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let report = store.diagnose_table_catalog(bucket, "sales", "orders", 0).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(report.current_metadata_status, TableMetadataPointerStatus::Valid);
|
||||||
|
assert_eq!(report.orphan_metadata_candidate_locations, vec![orphan]);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn object_table_catalog_store_commits_with_token_match_and_writes_log() {
|
async fn object_table_catalog_store_commits_with_token_match_and_writes_log() {
|
||||||
let backend = TestCatalogObjectBackend::default();
|
let backend = TestCatalogObjectBackend::default();
|
||||||
|
|||||||
Reference in New Issue
Block a user