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:
唐小鸭
2026-08-06 08:27:27 +08:00
committed by GitHub
parent ead419451a
commit 733c7b0f67
6 changed files with 163 additions and 15 deletions
+15
View File
@@ -1279,6 +1279,14 @@ mod tests {
.iter()
.any(|field| field.name == "disableProxy" && field.state == super::ReplicationFieldState::Unsupported)
);
assert!(
response
.replication
.remote_targets
.fields
.iter()
.any(|field| field.name == "healthCheckDuration" && field.state == super::ReplicationFieldState::Supported)
);
assert_eq!(response.manual_transition_jobs.contract_version, 1);
assert_eq!(response.manual_transition_jobs.status.state, CapabilityState::Supported);
assert_eq!(response.manual_transition_jobs.modes, ["enqueue_only", "async"]);
@@ -1361,6 +1369,13 @@ mod tests {
.iter()
.any(|field| field["name"] == "disableProxy" && field["state"] == "unsupported")
);
assert!(
value["replication"]["remote_targets"]["fields"]
.as_array()
.expect("remote target fields should be an array")
.iter()
.any(|field| field["name"] == "healthCheckDuration" && field["state"] == "supported")
);
assert_eq!(value["manual_transition_jobs"]["contract_version"], 1);
assert_eq!(value["manual_transition_jobs"]["status"]["state"], "supported");
assert_eq!(value["manual_transition_jobs"]["modes"], json!(["enqueue_only", "async"]));