fix(madmin): restore server_info msgpack compatibility across mixed nodes (#2621)

This commit is contained in:
houseme
2026-04-21 09:09:25 +08:00
committed by GitHub
parent 3796b684f0
commit b7a945e453
6 changed files with 141 additions and 9 deletions
Generated
+1
View File
@@ -8344,6 +8344,7 @@ dependencies = [
"chrono",
"humantime",
"hyper",
"rmp-serde",
"serde",
"serde_json",
"time",
+3
View File
@@ -38,3 +38,6 @@ time.workspace = true
[lib]
doctest = false
[dev-dependencies]
rmp-serde.workspace = true
+131 -5
View File
@@ -86,10 +86,6 @@ pub struct Disk {
pub write_latency: f64,
pub utilization: f64,
pub metrics: Option<DiskMetrics>,
#[serde(rename = "runtimeState", default, skip_serializing_if = "Option::is_none")]
pub runtime_state: Option<String>,
#[serde(rename = "offlineDurationSeconds", default, skip_serializing_if = "Option::is_none")]
pub offline_duration_seconds: Option<u64>,
pub heal_info: Option<HealingDisk>,
pub used_inodes: u64,
pub free_inodes: u64,
@@ -97,6 +93,10 @@ pub struct Disk {
pub pool_index: i32,
pub set_index: i32,
pub disk_index: i32,
#[serde(rename = "runtimeState", default, skip_serializing_if = "Option::is_none")]
pub runtime_state: Option<String>,
#[serde(rename = "offlineDurationSeconds", default, skip_serializing_if = "Option::is_none")]
pub offline_duration_seconds: Option<u64>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
@@ -353,10 +353,50 @@ pub struct InfoMessage {
#[cfg(test)]
mod tests {
use super::*;
use rmp_serde::{Deserializer, Serializer};
use serde_json;
use std::collections::HashMap;
use std::{collections::HashMap, io::Cursor};
use time::OffsetDateTime;
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
struct LegacyDiskCompat {
endpoint: String,
#[serde(rename = "rootDisk")]
root_disk: bool,
#[serde(rename = "path")]
drive_path: String,
healing: bool,
scanning: bool,
state: String,
uuid: String,
major: u32,
minor: u32,
model: Option<String>,
#[serde(rename = "totalspace")]
total_space: u64,
#[serde(rename = "usedspace")]
used_space: u64,
#[serde(rename = "availspace")]
available_space: u64,
#[serde(rename = "readthroughput")]
read_throughput: f64,
#[serde(rename = "writethroughput")]
write_throughput: f64,
#[serde(rename = "readlatency")]
read_latency: f64,
#[serde(rename = "writelatency")]
write_latency: f64,
utilization: f64,
metrics: Option<DiskMetrics>,
heal_info: Option<HealingDisk>,
used_inodes: u64,
free_inodes: u64,
local: bool,
pool_index: i32,
set_index: i32,
disk_index: i32,
}
#[test]
fn test_item_state_to_string() {
assert_eq!(ItemState::Offline.to_string(), ITEM_OFFLINE);
@@ -496,6 +536,92 @@ mod tests {
assert!(disk.local);
}
#[test]
fn test_disk_msgpack_backward_compat_from_legacy_layout() {
let legacy = LegacyDiskCompat {
endpoint: "http://legacy-node:9000".to_string(),
root_disk: false,
drive_path: "/data/legacy".to_string(),
healing: false,
scanning: false,
state: ITEM_ONLINE.to_string(),
uuid: "legacy-uuid".to_string(),
major: 8,
minor: 2,
model: Some("legacy".to_string()),
total_space: 42,
used_space: 12,
available_space: 30,
read_throughput: 1.0,
write_throughput: 2.0,
read_latency: 3.0,
write_latency: 4.0,
utilization: 5.0,
metrics: None,
heal_info: None,
used_inodes: 11_125,
free_inodes: 98_000,
local: true,
pool_index: 1,
set_index: 2,
disk_index: 3,
};
let mut encoded = Vec::new();
legacy.serialize(&mut Serializer::new(&mut encoded)).unwrap();
let mut decoder = Deserializer::new(Cursor::new(encoded));
let decoded: Disk = serde::Deserialize::deserialize(&mut decoder).unwrap();
assert_eq!(decoded.used_inodes, 11_125);
assert_eq!(decoded.runtime_state, None);
assert_eq!(decoded.offline_duration_seconds, None);
}
#[test]
fn test_disk_msgpack_forward_compat_to_legacy_layout() {
let current = Disk {
endpoint: "http://current-node:9000".to_string(),
root_disk: false,
drive_path: "/data/current".to_string(),
healing: false,
scanning: false,
state: ITEM_ONLINE.to_string(),
uuid: "current-uuid".to_string(),
major: 8,
minor: 3,
model: Some("current".to_string()),
total_space: 64,
used_space: 20,
available_space: 44,
read_throughput: 1.5,
write_throughput: 2.5,
read_latency: 3.5,
write_latency: 4.5,
utilization: 6.5,
metrics: None,
heal_info: None,
used_inodes: 22_250,
free_inodes: 97_000,
local: true,
pool_index: 1,
set_index: 2,
disk_index: 3,
runtime_state: Some("online".to_string()),
offline_duration_seconds: Some(0),
};
let mut encoded = Vec::new();
current
.serialize(&mut Serializer::new(&mut encoded).with_struct_map())
.unwrap();
let mut decoder = Deserializer::new(Cursor::new(encoded));
let decoded: LegacyDiskCompat = serde::Deserialize::deserialize(&mut decoder).unwrap();
assert_eq!(decoded.used_inodes, 22_250);
assert_eq!(decoded.disk_index, 3);
assert_eq!(decoded.endpoint, "http://current-node:9000");
}
#[test]
fn test_healing_disk_default() {
let healing_disk = HealingDisk::default();
+2 -2
View File
@@ -467,12 +467,12 @@ impl MQTTArgs {
if !self.queue_dir.is_empty() {
let path = std::path::Path::new(&self.queue_dir);
if !path.is_absolute() {
return Err(TargetError::Configuration("mqtt queueDir path should be absolute".to_string()));
return Err(TargetError::Configuration("mqtt queue_dir path should be absolute".to_string()));
}
if self.qos == QoS::AtMostOnce {
return Err(TargetError::Configuration(
"QoS should be AtLeastOnce (1) or ExactlyOnce (2) if queueDir is set".to_string(),
"QoS should be AtLeastOnce (1) or ExactlyOnce (2) if queue_dir is set".to_string(),
));
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ impl WebhookArgs {
if !self.queue_dir.is_empty() {
let path = std::path::Path::new(&self.queue_dir);
if !path.is_absolute() {
return Err(TargetError::Configuration("webhook queueDir path should be absolute".to_string()));
return Err(TargetError::Configuration("webhook queue_dir path should be absolute".to_string()));
}
}
+3 -1
View File
@@ -204,7 +204,9 @@ impl NodeService {
) -> Result<Response<ServerInfoResponse>, Status> {
let info = get_local_server_property().await;
let mut buf = Vec::new();
if let Err(err) = info.serialize(&mut Serializer::new(&mut buf)) {
// Use map encoding for forward/backward compatibility across mixed versions:
// unknown fields can be ignored by older nodes during deserialization.
if let Err(err) = info.serialize(&mut Serializer::new(&mut buf).with_struct_map()) {
return Ok(Response::new(ServerInfoResponse {
success: false,
server_properties: Bytes::new(),