mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-23 20:59:05 +00:00
fix(scanner): preserve exact overflow heal versions
This commit is contained in:
@@ -111,6 +111,9 @@ pub struct MetaCacheHealDiscovery {
|
|||||||
/// healing disabled; this is an explicit bounded continuation, not a
|
/// healing disabled; this is an explicit bounded continuation, not a
|
||||||
/// version claim.
|
/// version claim.
|
||||||
pub truncated_objects: Vec<String>,
|
pub truncated_objects: Vec<String>,
|
||||||
|
/// Validated candidates beyond the main cap, retained with exact version
|
||||||
|
/// identities so callers never fall back to a latest-version request.
|
||||||
|
pub truncated_candidates: Vec<MetaCacheHealCandidate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetaCacheEntry {
|
impl MetaCacheEntry {
|
||||||
@@ -442,6 +445,7 @@ impl MetaCacheEntries {
|
|||||||
unverified_count: 0,
|
unverified_count: 0,
|
||||||
truncated: false,
|
truncated: false,
|
||||||
truncated_objects: Vec::with_capacity(MAX_META_CACHE_HEAL_TRUNCATED_OBJECTS.min(limit)),
|
truncated_objects: Vec::with_capacity(MAX_META_CACHE_HEAL_TRUNCATED_OBJECTS.min(limit)),
|
||||||
|
truncated_candidates: Vec::new(),
|
||||||
};
|
};
|
||||||
let mut seen: HashMap<(String, Option<Uuid>, MetaCacheHealCandidateKind), usize> =
|
let mut seen: HashMap<(String, Option<Uuid>, MetaCacheHealCandidateKind), usize> =
|
||||||
HashMap::with_capacity(limit.min(self.0.len()));
|
HashMap::with_capacity(limit.min(self.0.len()));
|
||||||
@@ -580,10 +584,10 @@ impl MetaCacheEntries {
|
|||||||
{
|
{
|
||||||
discovery.truncated_objects.push(candidate.object.clone());
|
discovery.truncated_objects.push(candidate.object.clone());
|
||||||
}
|
}
|
||||||
// The remaining versions in this raw entry cannot add a
|
if discovery.truncated_objects.iter().any(|object| object == &candidate.object) {
|
||||||
// bounded candidate; avoid parsing a very long history
|
discovery.truncated_candidates.push(candidate);
|
||||||
// after the safe continuation has been recorded.
|
}
|
||||||
break;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
entry_seen.insert(key.clone());
|
entry_seen.insert(key.clone());
|
||||||
seen.insert(key, discovery.candidates.len());
|
seen.insert(key, discovery.candidates.len());
|
||||||
@@ -775,7 +779,7 @@ fn valid_heal_candidate_name(bucket: &str, entry: &MetaCacheEntry) -> bool {
|
|||||||
if bucket.is_empty()
|
if bucket.is_empty()
|
||||||
|| entry.name.is_empty()
|
|| entry.name.is_empty()
|
||||||
|| entry.is_dir()
|
|| entry.is_dir()
|
||||||
|| entry.name.contains('\\')
|
|| (cfg!(windows) && entry.name.contains('\\'))
|
||||||
|| entry.name.chars().any(char::is_control)
|
|| entry.name.chars().any(char::is_control)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -2130,6 +2134,13 @@ mod tests {
|
|||||||
let discovery = entries.discover_heal_candidates("bucket", 5);
|
let discovery = entries.discover_heal_candidates("bucket", 5);
|
||||||
assert!(discovery.candidates.len() <= 5);
|
assert!(discovery.candidates.len() <= 5);
|
||||||
assert!(discovery.truncated, "bounded discovery must expose dropped candidates");
|
assert!(discovery.truncated, "bounded discovery must expose dropped candidates");
|
||||||
|
assert!(
|
||||||
|
discovery
|
||||||
|
.truncated_candidates
|
||||||
|
.iter()
|
||||||
|
.all(|candidate| candidate.version_id.is_some()),
|
||||||
|
"overflow candidates must retain exact version identities"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
discovery.truncated_objects.iter().any(|object| object == "object"),
|
discovery.truncated_objects.iter().any(|object| object == "object"),
|
||||||
"bounded discovery must expose an object-level safe continuation"
|
"bounded discovery must expose an object-level safe continuation"
|
||||||
@@ -2165,7 +2176,6 @@ mod tests {
|
|||||||
"./object",
|
"./object",
|
||||||
"object/../other",
|
"object/../other",
|
||||||
"object//name",
|
"object//name",
|
||||||
"object\\name",
|
|
||||||
"object\u{0001}name",
|
"object\u{0001}name",
|
||||||
"object\0name",
|
"object\0name",
|
||||||
] {
|
] {
|
||||||
@@ -2178,6 +2188,33 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let mut entry = metacache_entry_single_version(400, now, "object\\name");
|
||||||
|
entry.name = "object\\name".to_string();
|
||||||
|
assert!(
|
||||||
|
MetaCacheEntries(vec![Some(entry)])
|
||||||
|
.discover_heal_candidates("bucket", 5)
|
||||||
|
.candidates
|
||||||
|
.is_empty(),
|
||||||
|
"backslash is a path separator on Windows"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let mut entry = metacache_entry_single_version(400, now, "object\\name");
|
||||||
|
entry.name = "object\\name".to_string();
|
||||||
|
assert_eq!(
|
||||||
|
MetaCacheEntries(vec![Some(entry)])
|
||||||
|
.discover_heal_candidates("bucket", 5)
|
||||||
|
.candidates
|
||||||
|
.len(),
|
||||||
|
1,
|
||||||
|
"backslash is object-key data on Unix"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for valid_name in ["trailing/", "prefix/object"] {
|
for valid_name in ["trailing/", "prefix/object"] {
|
||||||
let mut entry = metacache_entry_single_version(401, now, valid_name);
|
let mut entry = metacache_entry_single_version(401, now, valid_name);
|
||||||
entry.name = valid_name.to_string();
|
entry.name = valid_name.to_string();
|
||||||
|
|||||||
@@ -1977,40 +1977,35 @@ impl FolderScanner {
|
|||||||
found_objects = true;
|
found_objects = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A bounded candidate union may overflow for an
|
// Candidates beyond the main cap remain exact
|
||||||
// object with a very long version history. Keep
|
// version requests; never downgrade them to a
|
||||||
// that overflow explicit and issue one safe,
|
// latest-version (version_id=None) heal.
|
||||||
// versionless inspection request per object so
|
for candidate in discovery.truncated_candidates {
|
||||||
// the dropped versions are not silently treated
|
let version_id = candidate.validated_version().map(|id| id.to_string());
|
||||||
// as absent. This continuation is deliberately
|
let identity = (candidate.object.clone(), version_id.clone(), candidate.kind.clone());
|
||||||
// outside the versioned candidate cap and always
|
|
||||||
// disables destructive cleanup.
|
|
||||||
for object in discovery.truncated_objects {
|
|
||||||
if seen_truncated_objects.len() >= MAX_META_CACHE_HEAL_TRUNCATED_OBJECTS
|
if seen_truncated_objects.len() >= MAX_META_CACHE_HEAL_TRUNCATED_OBJECTS
|
||||||
&& !seen_truncated_objects.contains(&object)
|
&& !seen_truncated_objects.contains(&candidate.object)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !seen_truncated_objects.insert(object.clone()) {
|
seen_truncated_objects.insert(candidate.object.clone());
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let identity = (object.clone(), None, MetaCacheHealCandidateKind::UnversionedObject);
|
|
||||||
if !seen_heal_candidates.insert(identity) {
|
if !seen_heal_candidates.insert(identity) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let request = build_non_destructive_object_heal_request(
|
let request = build_object_heal_request(
|
||||||
bucket.clone(),
|
bucket.clone(),
|
||||||
object.clone(),
|
candidate.object.clone(),
|
||||||
|
version_id.clone(),
|
||||||
self.scan_mode,
|
self.scan_mode,
|
||||||
HealChannelPriority::High,
|
HealChannelPriority::High,
|
||||||
);
|
);
|
||||||
(self.update_current_path)(&object).await;
|
(self.update_current_path)(&candidate.object).await;
|
||||||
let admission = self
|
let admission = self
|
||||||
.send_required_scanner_heal_request(
|
.send_required_scanner_heal_request(
|
||||||
PendingScannerHealKind::Object,
|
PendingScannerHealKind::Object,
|
||||||
bucket.clone(),
|
bucket.clone(),
|
||||||
Some(object.clone()),
|
Some(candidate.object.clone()),
|
||||||
None,
|
version_id,
|
||||||
request,
|
request,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user