mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-09 22:59:59 +00:00
fix(replication): accept remote target healthCheckDuration nanoseconds (#5754)
* test(replication): accept madmin nanosecond healthCheckDuration payloads Red-phase TDD tests for P0-7: mc 'replicate add' sends the madmin default healthCheckDuration=60s as a Go time.Duration nanosecond integer (60000000000), which RustFS currently rejects as an unsupported field and would misread as seconds. Also pins the defensive seconds-or-nanos read for persisted bucket-targets metadata and the capability contract listing healthCheckDuration as writable. Currently failing (red): - remote_target_request_accepts_go_duration_wire_values - remote_target_request_accepts_legacy_seconds_health_check - remote_target_health_check_duration_is_declared_writable - bucket_target_reads_go_nanosecond_durations_defensively - runtime_capabilities_response_reports_missing_topology_before_storage_init * fix(replication): accept remote target healthCheckDuration nanoseconds mc 'replicate add' always sends the madmin default healthcheck-seconds=60 serialized as a Go time.Duration nanosecond integer (60000000000), so the default mc link-creation path (and 'mc replicate update') failed with InvalidRequest. Move healthCheckDuration from the unsupported to the writable remote-target field list; the capability contract in the runtime capabilities response follows the constants automatically. Fix the unit mismatch in both directions: - Request parsing and persisted bucket-targets reads decode the value defensively: below 10^7 it is legacy RustFS seconds, otherwise Go time.Duration nanoseconds (also covers MinIO-written metadata). totalDowntime shares the same wire shape and gets the same handling. - The list-remote-targets admin response re-encodes only these two fields as nanoseconds via a dedicated serialization path, leaving the persisted seconds-based wire format untouched for existing readers. The per-target health-check interval is accepted for mc compatibility but not yet applied; the heartbeat keeps its global env-configured interval, and the explicit 'healthcheck' update op stays rejected. disableProxy, edge, and edgeSyncBeforeExpiry remain explicitly rejected.
This commit is contained in:
@@ -206,7 +206,9 @@ pub mod bucket {
|
||||
}
|
||||
|
||||
pub mod target {
|
||||
pub use crate::bucket::target::{ARN, BucketTarget, BucketTargetType, BucketTargets, Credentials, LatencyStat};
|
||||
pub use crate::bucket::target::{
|
||||
ARN, BucketTarget, BucketTargetType, BucketTargets, Credentials, LatencyStat, duration_from_secs_or_nanos,
|
||||
};
|
||||
}
|
||||
|
||||
pub mod utils {
|
||||
|
||||
@@ -94,6 +94,21 @@ mod duration_milliseconds {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defensive decode for the two integer wire encodings of these duration
|
||||
/// fields: RustFS persists (and legacy RustFS clients sent) plain seconds,
|
||||
/// while Go `time.Duration` JSON — madmin/mc requests and MinIO-written
|
||||
/// bucket-targets metadata — is nanoseconds. No meaningful interval lies
|
||||
/// between 10^7 seconds (~115 days) and 10^7 nanoseconds (10ms), so the
|
||||
/// magnitude disambiguates the unit.
|
||||
pub fn duration_from_secs_or_nanos(value: u64) -> Duration {
|
||||
const NANOS_THRESHOLD: u64 = 10_000_000;
|
||||
if value < NANOS_THRESHOLD {
|
||||
Duration::from_secs(value)
|
||||
} else {
|
||||
Duration::from_nanos(value)
|
||||
}
|
||||
}
|
||||
|
||||
mod duration_seconds {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
use std::time::Duration;
|
||||
@@ -109,8 +124,8 @@ mod duration_seconds {
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let secs = u64::deserialize(deserializer)?;
|
||||
Ok(Duration::from_secs(secs))
|
||||
let value = u64::deserialize(deserializer)?;
|
||||
Ok(super::duration_from_secs_or_nanos(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,6 +504,29 @@ mod tests {
|
||||
assert_eq!(original.offline_count, deserialized.offline_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_target_reads_go_nanosecond_durations_defensively() {
|
||||
// MinIO-written bucket-targets metadata and madmin clients encode
|
||||
// these fields as Go `time.Duration` nanoseconds; RustFS has always
|
||||
// persisted seconds. Both encodings must decode to the same interval.
|
||||
let target: BucketTarget = serde_json::from_value(serde_json::json!({
|
||||
"endpoint": "localhost:9000",
|
||||
"targetbucket": "target",
|
||||
"type": "replication",
|
||||
"healthCheckDuration": 60_000_000_000u64,
|
||||
"totalDowntime": 90_000_000_000u64
|
||||
}))
|
||||
.expect("nanosecond durations should deserialize");
|
||||
|
||||
assert_eq!(target.health_check_duration, Duration::from_secs(60));
|
||||
assert_eq!(target.total_downtime, Duration::from_secs(90));
|
||||
|
||||
// The persisted wire format stays seconds for existing RustFS readers.
|
||||
let value = serde_json::to_value(&target).expect("target should serialize");
|
||||
assert_eq!(value["healthCheckDuration"], 60);
|
||||
assert_eq!(value["totalDowntime"], 90);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bucket_target_debug_redacts_credentials() {
|
||||
let target = BucketTarget {
|
||||
|
||||
@@ -73,9 +73,13 @@ pub const REMOTE_TARGET_WRITABLE_FIELDS: &[&str] = &[
|
||||
"storage_class",
|
||||
"skipTlsVerify",
|
||||
"caCertPem",
|
||||
// Accepted for mc compatibility (mc `replicate add` always sends the
|
||||
// madmin default of 60s); the per-target health-check interval is not
|
||||
// yet applied — the heartbeat keeps its global env-configured interval.
|
||||
"healthCheckDuration",
|
||||
];
|
||||
|
||||
pub const REMOTE_TARGET_UNSUPPORTED_FIELDS: &[&str] = &["disableProxy", "healthCheckDuration", "edge", "edgeSyncBeforeExpiry"];
|
||||
pub const REMOTE_TARGET_UNSUPPORTED_FIELDS: &[&str] = &["disableProxy", "edge", "edgeSyncBeforeExpiry"];
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ObjectOpts {
|
||||
|
||||
Reference in New Issue
Block a user