mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-18 10:43:15 +00:00
feat: harden runtime capability snapshots (#3784)
* feat(admin): expose runtime capability snapshots * feat(runtime): refine workload admission snapshots * test(ci): align architecture migration checks * build(deps): bump memmap2 for RUSTSEC-2026-0186 * build(deps): refresh cargo deny lockfile
This commit is contained in:
@@ -14,6 +14,7 @@ categories = ["concurrency", "filesystem"]
|
||||
# Internal crates
|
||||
rustfs-io-core = { workspace = true }
|
||||
rustfs-io-metrics = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
# Async runtime
|
||||
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
||||
@@ -26,6 +27,7 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["test-util","macros","rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
|
||||
//! Runtime workload admission contract types.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Stable workload classes used by future runtime admission snapshots.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkloadClass {
|
||||
/// Foreground read requests.
|
||||
ForegroundRead,
|
||||
@@ -62,7 +65,8 @@ impl std::fmt::Display for WorkloadClass {
|
||||
}
|
||||
|
||||
/// Read-only admission state for a workload class.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AdmissionState {
|
||||
/// Admission is open for this workload class.
|
||||
Open,
|
||||
@@ -85,7 +89,8 @@ impl AdmissionState {
|
||||
}
|
||||
|
||||
/// Read-only admission snapshot for one workload class.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkloadAdmissionSnapshot {
|
||||
/// Workload class described by this snapshot.
|
||||
pub class: WorkloadClass,
|
||||
@@ -130,7 +135,8 @@ impl WorkloadAdmissionSnapshot {
|
||||
}
|
||||
|
||||
/// Read-only admission registry snapshot.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkloadAdmissionRegistrySnapshot {
|
||||
entries: Vec<WorkloadAdmissionSnapshot>,
|
||||
}
|
||||
@@ -175,6 +181,7 @@ pub trait WorkloadAdmissionSnapshotProvider {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn workload_contract_covers_required_classes() {
|
||||
@@ -249,4 +256,39 @@ mod tests {
|
||||
Some(AdmissionState::Open)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workload_admission_snapshot_round_trips_with_snake_case_enums() {
|
||||
let snapshot = WorkloadAdmissionSnapshot::new(WorkloadClass::ForegroundWrite, AdmissionState::Disabled)
|
||||
.with_counts(Some(0), Some(0), Some(0))
|
||||
.with_reason("foreground write admission not exposed");
|
||||
let encoded = serde_json::to_value(&snapshot).expect("serialize workload admission snapshot");
|
||||
|
||||
assert_eq!(encoded["class"], json!("foreground_write"));
|
||||
assert_eq!(encoded["state"], json!("disabled"));
|
||||
|
||||
let decoded: WorkloadAdmissionSnapshot =
|
||||
serde_json::from_value(encoded).expect("deserialize workload admission snapshot");
|
||||
assert_eq!(decoded, snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workload_admission_registry_rejects_unknown_fields() {
|
||||
let raw = json!({
|
||||
"entries": [{
|
||||
"class": "foreground_read",
|
||||
"state": "open",
|
||||
"active": 1,
|
||||
"queued": 0,
|
||||
"limit": 8,
|
||||
"reason": null,
|
||||
"unexpected": true
|
||||
}]
|
||||
});
|
||||
|
||||
let err = serde_json::from_value::<WorkloadAdmissionRegistrySnapshot>(raw)
|
||||
.expect_err("unknown workload admission fields must be rejected");
|
||||
|
||||
assert!(err.to_string().contains("unexpected"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +387,7 @@ fn endpoint_labels(endpoint: &Endpoint) -> TopologyLabels {
|
||||
additional.insert(LOCAL_ENDPOINT_LABEL.to_owned(), endpoint.is_local.to_string());
|
||||
|
||||
TopologyLabels {
|
||||
node: Some(endpoint_node_id(endpoint)),
|
||||
additional,
|
||||
..TopologyLabels::default()
|
||||
}
|
||||
@@ -453,6 +454,7 @@ mod tests {
|
||||
.map(String::as_str),
|
||||
Some("path")
|
||||
);
|
||||
assert_eq!(snapshot.pools[0].sets[0].disks[0].labels.node.as_deref(), Some(LOCAL_NODE_ID));
|
||||
|
||||
let encoded = serde_json::to_string(&snapshot).expect("serialize topology snapshot");
|
||||
assert!(!encoded.contains("/tmp/rustfs-cluster-control-plane"));
|
||||
@@ -464,6 +466,7 @@ mod tests {
|
||||
let snapshot = topology_snapshot_from_endpoint_pools(&endpoint_pools);
|
||||
|
||||
assert_eq!(snapshot.pools[0].sets[0].disks[0].disk_id.as_deref(), Some("node1.example:9000"));
|
||||
assert_eq!(snapshot.pools[0].sets[0].disks[0].labels.node.as_deref(), Some("node1.example:9000"));
|
||||
assert_eq!(
|
||||
snapshot.pools[0].sets[0].disks[0]
|
||||
.labels
|
||||
|
||||
Reference in New Issue
Block a user