refactor(metrics): migrate scanner report timestamps to jiff (#5710)

* refactor(metrics): migrate scanner report timestamps to jiff

Co-Authored-By: heihutu <heihutu@gmail.com>

* refactor(madmin): migrate admin timestamps to jiff (#5712)

Co-authored-by: heihutu <heihutu@gmail.com>

* refactor(storage): migrate RPC DTO timestamps to jiff (#5713)

Co-authored-by: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-08-05 02:35:38 +08:00
committed by GitHub
parent 3dabac4a09
commit 16c2928965
14 changed files with 458 additions and 115 deletions
+22 -3
View File
@@ -39,6 +39,7 @@ use crate::server::{ADMIN_PREFIX, RemoteAddr};
use crate::storage::storage_api::lock_bucket_targets_metadata;
use http::{HeaderMap, HeaderValue, Uri};
use hyper::{Method, StatusCode};
use jiff::Timestamp;
use matchit::Params;
use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE;
use rustfs_credentials::Credentials;
@@ -91,7 +92,7 @@ struct RemoteTargetCredentialsRequest {
#[serde(rename = "secretKey")]
secret_key: String,
session_token: Option<String>,
expiration: Option<chrono::DateTime<chrono::Utc>>,
expiration: Option<Timestamp>,
}
impl From<RemoteTargetCredentialsRequest> for TargetCredentials {
@@ -1050,8 +1051,9 @@ impl Operation for ReplicationMrfHandler {
#[cfg(test)]
mod tests {
use super::{
REMOTE_TARGET_UNSUPPORTED_FIELDS, REMOTE_TARGET_WRITABLE_FIELDS, RemoteTargetRequest, SUPPORTED_REMOTE_TARGET_API,
build_mrf_response, extract_query_params, unique_replication_peers, validate_remote_target_tls_settings,
REMOTE_TARGET_UNSUPPORTED_FIELDS, REMOTE_TARGET_WRITABLE_FIELDS, RemoteTargetCredentialsRequest, RemoteTargetRequest,
SUPPORTED_REMOTE_TARGET_API, build_mrf_response, extract_query_params, unique_replication_peers,
validate_remote_target_tls_settings,
};
use crate::admin::storage_api::bucket::target::BucketTarget;
use crate::admin::storage_api::replication::{BucketStats, DurableMrfBacklog, MrfOpKind, MrfReplicateEntry};
@@ -1350,6 +1352,23 @@ mod tests {
assert!(err.to_string().contains("credentials.secretKey is required"));
}
#[test]
fn remote_target_credentials_expiration_json_remains_rfc3339() {
let credentials: RemoteTargetCredentialsRequest = serde_json::from_value(serde_json::json!({
"accessKey": "access",
"secretKey": "secret",
"expiration": "2026-01-01T00:00:00Z"
}))
.expect("credentials expiration should deserialize from RFC3339 JSON");
let credentials = crate::admin::storage_api::bucket::target::Credentials::from(credentials);
let expiration = credentials.expiration.expect("expiration should be preserved");
assert_eq!(
serde_json::to_value(expiration).expect("expiration should serialize to JSON"),
serde_json::json!("2026-01-01T00:00:00Z")
);
}
#[test]
fn remote_target_request_rejects_unimplemented_fields() {
for (field, value) in [
+79 -4
View File
@@ -14,6 +14,7 @@
use crate::startup_background::{heal_enabled_from_env, scanner_enabled_from_env};
use crate::storage::storage_api::runtime_sources_consumer::EndpointServerPools;
use jiff::Timestamp;
use rmp_serde::Deserializer;
use rustfs_common::heal_channel::HealScanMode;
use rustfs_heal::HealOperationsSnapshot;
@@ -28,6 +29,31 @@ const NODE_HEAL_STATUS_VERSION: u8 = 1;
const NODE_HEAL_STATUS_MAX_SIZE: usize = 64 * 1024;
const HEAL_TOPOLOGY_FINGERPRINT_DOMAIN: &[u8] = b"rustfs-heal-topology-v1\0";
fn chrono_to_jiff_timestamp(timestamp: chrono::DateTime<chrono::Utc>) -> Timestamp {
let seconds = timestamp.timestamp();
let nanoseconds = match i32::try_from(timestamp.timestamp_subsec_nanos()) {
Ok(nanoseconds) => nanoseconds,
Err(_) => {
return if seconds < 0 { Timestamp::MIN } else { Timestamp::MAX };
}
};
match Timestamp::new(seconds, nanoseconds) {
Ok(timestamp) => timestamp,
Err(_) => {
if seconds < 0 {
Timestamp::MIN
} else {
Timestamp::MAX
}
}
}
}
fn jiff_to_chrono_datetime(timestamp: Timestamp) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::<chrono::Utc>::from(std::time::SystemTime::from(timestamp))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct HealControlCoordinator {
pub grid_host: String,
@@ -156,7 +182,7 @@ pub(crate) struct NodeHealProgress {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct NodeHealInfo {
bitrot_start_time: Option<chrono::DateTime<chrono::Utc>>,
bitrot_start_time: Option<Timestamp>,
bitrot_start_cycle: u64,
current_scan_mode: HealScanMode,
}
@@ -164,7 +190,7 @@ struct NodeHealInfo {
impl From<BackgroundHealInfo> for NodeHealInfo {
fn from(info: BackgroundHealInfo) -> Self {
Self {
bitrot_start_time: info.bitrot_start_time,
bitrot_start_time: info.bitrot_start_time.map(chrono_to_jiff_timestamp),
bitrot_start_cycle: info.bitrot_start_cycle,
current_scan_mode: info.current_scan_mode,
}
@@ -174,7 +200,7 @@ impl From<BackgroundHealInfo> for NodeHealInfo {
impl From<NodeHealInfo> for BackgroundHealInfo {
fn from(info: NodeHealInfo) -> Self {
Self {
bitrot_start_time: info.bitrot_start_time,
bitrot_start_time: info.bitrot_start_time.map(jiff_to_chrono_datetime),
bitrot_start_cycle: info.bitrot_start_cycle,
current_scan_mode: info.current_scan_mode,
}
@@ -213,7 +239,7 @@ impl NodeHealStatusSnapshot {
pub(crate) fn info(&self) -> BackgroundHealInfo {
BackgroundHealInfo {
bitrot_start_time: self.info.bitrot_start_time,
bitrot_start_time: self.info.bitrot_start_time.map(jiff_to_chrono_datetime),
bitrot_start_cycle: self.info.bitrot_start_cycle,
current_scan_mode: self.info.current_scan_mode,
}
@@ -270,6 +296,7 @@ mod tests {
Endpoint,
ecstore_layout::{EndpointServerPools, Endpoints, PoolEndpoints},
};
use chrono::SecondsFormat;
use rustfs_heal::HealOperationsSnapshot;
use rustfs_scanner::scanner::BackgroundHealInfo;
@@ -461,6 +488,54 @@ mod tests {
assert_eq!(decoded.operations.queue_length, 2);
}
#[test]
fn node_heal_status_timestamp_json_remains_rfc3339() {
let fixture = serde_json::json!({
"version": 1,
"servicesEnabled": true,
"initialized": true,
"info": {"bitrotStartTime": "2023-11-14T22:13:20.123456Z", "bitrotStartCycle": 9, "currentScanMode": 1},
"operations": {
"queueLength": 2, "activeTasks": 1, "retryingTasks": 0,
"queuedByPriority": {"low": 0, "normal": 2, "high": 0, "urgent": 0},
"activeByPriority": {"low": 0, "normal": 0, "high": 1, "urgent": 0},
"retryingByPriority": {"low": 0, "normal": 0, "high": 0, "urgent": 0},
"queuedBySource": {"scanner": 2, "admin": 0, "autoHeal": 0, "internal": 0, "readRepair": 0},
"activeBySource": {"scanner": 0, "admin": 1, "autoHeal": 0, "internal": 0, "readRepair": 0},
"retryingBySource": {"scanner": 0, "admin": 0, "autoHeal": 0, "internal": 0, "readRepair": 0}
},
"progress": null
});
let encoded = rmp_serde::to_vec_named(&fixture).expect("fixture should encode");
let decoded = decode_node_heal_status(&encoded).expect("timestamp fixture should decode");
let info = decoded.info();
assert_eq!(
info.bitrot_start_time
.expect("bitrot start time should be preserved")
.to_rfc3339_opts(SecondsFormat::Micros, true),
"2023-11-14T22:13:20.123456Z"
);
let started_at = chrono::DateTime::parse_from_rfc3339("2023-11-14T22:13:20.123456Z")
.expect("fixture timestamp should parse")
.with_timezone(&chrono::Utc);
let snapshot = NodeHealStatusSnapshot::for_test(
true,
true,
BackgroundHealInfo {
bitrot_start_time: Some(started_at),
bitrot_start_cycle: 9,
current_scan_mode: rustfs_common::heal_channel::HealScanMode::Deep,
},
HealOperationsSnapshot::default(),
None,
);
let encoded = encode_node_heal_status(&snapshot).expect("snapshot should encode");
let encoded_json: serde_json::Value = rmp_serde::from_slice(&encoded).expect("encoded snapshot should decode as JSON");
assert_eq!(encoded_json["info"]["bitrotStartTime"], serde_json::json!("2023-11-14T22:13:20.123456Z"));
}
#[test]
fn node_heal_status_rejects_nested_unknown_trailing_truncated_and_oversized_data() {
let mut fixture = serde_json::json!({