fix(tier): recover (#3182)

* fix(tier): stop sending nil/garbage versionId to warm backend S3

Three bugs caused NoSuchVersion errors when reading tiered objects:

1. warm_backend_s3sdk: GET and DELETE ignored rv/range opts entirely —
   fixed to forward version_id and byte-range to the SDK request.

2. version.rs (MetaObject + MetaDeleteMarker): transition_version_id was
   parsed with unwrap_or_default(), turning invalid/wrong-length bytes
   into Uuid::nil(). The nil UUID was then serialized and sent as
   ?versionId=00000000-... to the tier backend -> NoSuchVersion.
   Fixed: .and_then(.ok()).filter(!is_nil()) so only valid non-nil UUIDs
   are forwarded as versionId.

3. bucket_lifecycle_ops: add debug/error logs in
   get_transitioned_object_reader to record tier, tier_object, and
   tier_version_id before and on failure of the tier GET.

Also adds tier transition fields to dump_fileinfo example for offline
xl.meta inspection, and fixes Docker build (cargo path + entrypoint).
Adds CLAUDE.md with tier architecture and debugging notes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* more fixes for versionId

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Marcelo Bartsch <marcelo@bartsch.cl>

* remove branch

* Add tests and fix cargo path, add load to build-docker

* update documentation (CLAUDE.md)

* more fixes for recover

* More fixes to ILM recover

* final fix

* chore: add missing-shard first-scene diagnostics (#3213)

chore(ecstore): add missing-shard first-scene diagnostics

Log rename_data quorum context behind RUSTFS_ISSUE3031_DIAG_ENABLE so partial-disk success can be correlated with later missing shard reads.

Also log put_object commit success and tmp cleanup boundaries to capture when successful quorum writes are followed by tmp_dir cleanup.

* fix test anmd fmt

* fix cargo path
fix test

* fix(tier): format copy_object self-copy guard

---------

Signed-off-by: Marcelo Bartsch <marcelo@bartsch.cl>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
Marcelo Bartsch
2026-06-07 11:38:28 +02:00
committed by GitHub
parent 069d1e5a75
commit f00898d070
10 changed files with 506 additions and 34 deletions
+80 -3
View File
@@ -2052,8 +2052,9 @@ impl MetaObject {
let transitioned_objname = get_bytes(&self.meta_sys, SUFFIX_TRANSITIONED_OBJECTNAME)
.map(|v| String::from_utf8_lossy(&v).to_string())
.unwrap_or_default();
let transition_version_id =
get_bytes(&self.meta_sys, SUFFIX_TRANSITIONED_VERSION_ID).map(|v| Uuid::from_slice(v.as_slice()).unwrap_or_default());
let transition_version_id = get_bytes(&self.meta_sys, SUFFIX_TRANSITIONED_VERSION_ID)
.and_then(|v| Uuid::from_slice(v.as_slice()).ok())
.filter(|u| !u.is_nil());
let transition_tier = get_bytes(&self.meta_sys, SUFFIX_TRANSITION_TIER)
.map(|v| String::from_utf8_lossy(&v).to_string())
.unwrap_or_default();
@@ -2359,7 +2360,8 @@ impl MetaDeleteMarker {
.unwrap_or_default();
fi.transition_version_id = get_bytes(&self.meta_sys, SUFFIX_TRANSITIONED_VERSION_ID)
.map(|v| Uuid::from_slice(v.as_slice()).unwrap_or_default());
.and_then(|v| Uuid::from_slice(v.as_slice()).ok())
.filter(|u| !u.is_nil());
}
fi
@@ -3314,4 +3316,79 @@ mod tests {
let decoded = FileMetaVersion::decode_data_dir_from_meta(&encoded).expect("decode data_dir");
assert_eq!(decoded, Some(data_dir));
}
fn make_meta_object_with_sys(meta_sys: HashMap<String, Vec<u8>>) -> MetaObject {
MetaObject {
erasure_algorithm: ErasureAlgo::ReedSolomon,
erasure_m: 2,
erasure_n: 4,
erasure_block_size: 1_048_576,
erasure_index: 1,
erasure_dist: vec![1, 2, 3, 4, 5, 6],
bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
meta_sys,
..Default::default()
}
}
#[test]
fn meta_object_transition_version_id_absent_yields_none() {
let fi = make_meta_object_with_sys(HashMap::new()).into_fileinfo("b", "k", false);
assert_eq!(fi.transition_version_id, None);
}
#[test]
fn meta_object_transition_version_id_empty_bytes_yields_none() {
let mut sys = HashMap::new();
insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, vec![]);
let fi = make_meta_object_with_sys(sys).into_fileinfo("b", "k", false);
assert_eq!(fi.transition_version_id, None);
}
#[test]
fn meta_object_transition_version_id_nil_uuid_yields_none() {
// Regression: old code used unwrap_or_default() which turned nil bytes into Some(Uuid::nil())
let mut sys = HashMap::new();
insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, Uuid::nil().as_bytes().to_vec());
let fi = make_meta_object_with_sys(sys).into_fileinfo("b", "k", false);
assert_eq!(fi.transition_version_id, None);
}
#[test]
fn meta_object_transition_version_id_valid_uuid_round_trips() {
let id = sample_version_id();
let mut sys = HashMap::new();
insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, id.as_bytes().to_vec());
let fi = make_meta_object_with_sys(sys).into_fileinfo("b", "k", false);
assert_eq!(fi.transition_version_id, Some(id));
}
#[test]
fn delete_marker_free_version_transition_version_id_nil_uuid_yields_none() {
let mut sys = HashMap::new();
insert_bytes(&mut sys, SUFFIX_FREE_VERSION, vec![]);
insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, Uuid::nil().as_bytes().to_vec());
let fi = MetaDeleteMarker {
version_id: None,
mod_time: None,
meta_sys: sys,
}
.into_fileinfo("b", "k", false);
assert_eq!(fi.transition_version_id, None);
}
#[test]
fn delete_marker_free_version_transition_version_id_valid_uuid_round_trips() {
let id = sample_version_id();
let mut sys = HashMap::new();
insert_bytes(&mut sys, SUFFIX_FREE_VERSION, vec![]);
insert_bytes(&mut sys, SUFFIX_TRANSITIONED_VERSION_ID, id.as_bytes().to_vec());
let fi = MetaDeleteMarker {
version_id: None,
mod_time: None,
meta_sys: sys,
}
.into_fileinfo("b", "k", false);
assert_eq!(fi.transition_version_id, Some(id));
}
}