fix(site-repl): count replicated buckets as in-sync (base64 decode in status path) (#5022)

fix(site-replication): count replicated buckets as in-sync in replicate status

`mc admin replicate status` reported "0/N Buckets in sync" with a cross mark for
every bucket even when replication was healthy and objects had propagated.

build_sr_info stores each bucket's replication_config in the wire form as
base64-encoded XML (raw_config_to_base64), but site_replication_config_mismatch
XML-parsed that string directly without base64-decoding. The parse always
failed, so every bucket with a replication config was reported as a config
mismatch (replication_cfg_mismatch = true), which the status endpoint and mc
render as out-of-sync.

Decode the wire value (tolerant base64 decode, falling back to raw bytes) before
XML-parsing. Adds a regression test that feeds the base64 wire form used in
production; the existing tests only exercised raw XML, which masked the bug.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
This commit is contained in:
abdullahnah92
2026-07-20 13:49:51 +03:00
committed by GitHub
parent 9e4c5e949f
commit 7ddaae397b
+46 -1
View File
@@ -3285,7 +3285,13 @@ fn site_replication_config_mismatch<'a>(values: impl Iterator<Item = Option<&'a
let expected_rules = total_sites.saturating_sub(1);
let replicated = values.iter().all(|raw| {
deserialize::<ReplicationConfiguration>(raw.as_bytes())
// `raw` is the wire form produced by build_sr_info, i.e. base64-encoded XML
// (raw_config_to_base64). Decode it before XML-parsing — parsing the base64 text
// directly always fails, which would falsely report every replicated bucket as
// out-of-sync ("0/N Buckets in sync"). decode_bucket_meta_wire_value falls back to
// the raw bytes when the value is not base64, so plain-XML callers still work.
let xml = decode_bucket_meta_wire_value(raw);
deserialize::<ReplicationConfiguration>(&xml)
.is_ok_and(|config| config.rules.len() == expected_rules && config.rules.iter().all(site_replication_rule_complete))
});
@@ -11003,6 +11009,45 @@ mod tests {
);
}
// Status miscount regression: build_sr_info stores replication_config as base64-encoded XML
// (the wire form). site_replication_config_mismatch must decode it before XML-parsing; before
// the fix it parsed the base64 text directly, always failed, and reported every replicated
// bucket as out-of-sync ("0/N Buckets in sync"). This test feeds the real base64 wire form.
#[test]
fn test_site_replication_config_mismatch_accepts_base64_wire_form() {
let xml = {
let config = ReplicationConfiguration {
role: String::new(),
rules: vec![build_site_replication_rule(
"arn:rustfs:replication::dep-b:bucket",
1,
"site-repl-dep-b",
)],
};
String::from_utf8(serialize(&config).unwrap()).unwrap()
};
let b64 = BASE64_STANDARD.encode(xml.as_bytes());
// Both sites present the complete config in base64 wire form → NOT a mismatch.
assert_eq!(
site_replication_config_mismatch(vec![Some(&b64), Some(&b64)].into_iter(), 2),
(2, false),
"base64-encoded complete configs on both sites must not be reported as a mismatch"
);
// The tolerant decode keeps plain-XML callers working too.
assert_eq!(
site_replication_config_mismatch(vec![Some(&xml), Some(&xml)].into_iter(), 2),
(2, false),
"raw-XML wire form still parses via the base64 fallback"
);
// A base64 config present on only one of two sites is still a mismatch.
assert_eq!(
site_replication_config_mismatch(vec![Some(&b64)].into_iter(), 2),
(1, true),
"config present on only one site is a mismatch regardless of encoding"
);
}
// BUG1: peers persisted on add/join must carry a real sync_state (Enable), not Unknown,
// so `mc admin replicate info` and the console show the correct state for healthy peers.
#[test]