mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 08:49:26 +00:00
fix(ecstore): migrate tier free versions during decommission (#6393)
This commit is contained in:
@@ -90,6 +90,21 @@ fn legacy_data_key_for_version(version_id: Option<Uuid>) -> Option<String> {
|
||||
pub const TRANSITION_COMPLETE: &str = "complete";
|
||||
pub const TRANSITION_PENDING: &str = "pending";
|
||||
|
||||
/// xl.meta key marking a tier free-version record.
|
||||
///
|
||||
/// A free version is a delete-marker-shaped cleanup hint appended by
|
||||
/// [`MetaObject::delete_version`] when a version whose remote transition
|
||||
/// completed is removed from xl.meta; it carries the remote tier identity for
|
||||
/// an idempotent remote delete and is never a user-visible version
|
||||
/// (`num_versions` excludes it). While the record exists it is consumed by the
|
||||
/// lifecycle free-version recovery scan and the usage scanner, which re-enqueue
|
||||
/// the pending remote delete, and by heal metadata walks. On S3 and lifecycle
|
||||
/// delete paths the same obligation is also carried by a committed tier-journal
|
||||
/// entry; deletes without such an entry (for example a removed version whose
|
||||
/// transition state decodes as unknown) rely on this record alone until the
|
||||
/// worker removes it after a successful remote delete. Decommission preserves
|
||||
/// the record and its remote identity on the target pool before source cleanup
|
||||
/// — see docs/architecture/decommission-compatibility.md.
|
||||
pub const FREE_VERSION: &str = "free-version";
|
||||
|
||||
pub const TRANSITION_STATUS: &str = "transition-status";
|
||||
@@ -447,6 +462,10 @@ impl FileMeta {
|
||||
};
|
||||
|
||||
if let Some(fidx) = existing_idx {
|
||||
let existing = self.versions[fidx].parse_version_meta()?;
|
||||
if existing.free_version() != version.free_version() {
|
||||
return Err(Error::other("cannot replace a free version with a non-free version"));
|
||||
}
|
||||
return self.set_idx(fidx, version);
|
||||
}
|
||||
|
||||
@@ -1363,6 +1382,39 @@ mod test {
|
||||
assert!(fm.versions[0].header.sorts_before(&fm.versions[1].header));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_version_filemata_rejects_free_and_ordinary_same_id_replacement() {
|
||||
let version_id = Uuid::new_v4();
|
||||
let mut free_meta_sys = HashMap::new();
|
||||
insert_bytes(&mut free_meta_sys, rustfs_utils::http::SUFFIX_FREE_VERSION, Vec::new());
|
||||
let free_version = FileMetaVersion {
|
||||
version_type: VersionType::Delete,
|
||||
delete_marker: Some(MetaDeleteMarker {
|
||||
version_id: Some(version_id),
|
||||
mod_time: Some(OffsetDateTime::now_utc()),
|
||||
meta_sys: free_meta_sys,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let ordinary_version = valid_object_version(version_id, vec![10, 20]);
|
||||
|
||||
for (existing, replacement) in [
|
||||
(free_version.clone(), ordinary_version.clone()),
|
||||
(ordinary_version, free_version),
|
||||
] {
|
||||
let mut fm = FileMeta::new();
|
||||
fm.add_version_filemata(existing).expect("seed same-id version");
|
||||
let before = fm.marshal_msg().expect("serialize original metadata");
|
||||
|
||||
let err = fm
|
||||
.add_version_filemata(replacement)
|
||||
.expect_err("free and ordinary versions with the same ID must not replace each other");
|
||||
|
||||
assert!(err.to_string().contains("cannot replace a free version"));
|
||||
assert_eq!(fm.marshal_msg().expect("serialize rejected metadata"), before);
|
||||
}
|
||||
}
|
||||
|
||||
/// `add_version_filemata` positions an inserted version with
|
||||
/// `partition_point(sorts_before)`, which only yields the canonical slot when
|
||||
/// `versions` is already canonically ordered. Nothing establishes that
|
||||
|
||||
@@ -2725,6 +2725,15 @@ impl MetaObject {
|
||||
self.meta_sys.retain(|k, _| !k.starts_with("X-Amz-Restore"));
|
||||
}
|
||||
|
||||
/// Builds the free-version cleanup record appended when a transitioned
|
||||
/// version is removed from xl.meta. The record keeps the remote tier
|
||||
/// identity so the lifecycle worker can issue the idempotent remote delete
|
||||
/// and only then remove the record; until then the recovery scan and the
|
||||
/// usage scanner keep re-enqueueing it. S3 and lifecycle deletes also
|
||||
/// persist a committed tier-journal entry for the same remote delete. The
|
||||
/// decommission path copies this record unchanged before source cleanup,
|
||||
/// including when the transition state is unknown — see
|
||||
/// docs/architecture/decommission-compatibility.md.
|
||||
pub fn init_free_version(&self, fi: &FileInfo) -> Result<(FileMetaVersion, bool)> {
|
||||
if fi.skip_tier_free_version() {
|
||||
return Ok((FileMetaVersion::default(), false));
|
||||
|
||||
Reference in New Issue
Block a user