refactor: route cluster control plane readiness (#3935)

This commit is contained in:
Zhengchao An
2026-06-27 09:47:30 +08:00
committed by GitHub
parent 0a5b1b1b3a
commit 3fb4dcd52e
13 changed files with 343 additions and 58 deletions
+3 -2
View File
@@ -49,9 +49,10 @@ pub mod cluster {
pub use crate::cluster::{ pub use crate::cluster::{
ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage,
ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth,
ClusterPeerHealthSnapshot, ClusterPoolState, ClusterPoolStateSnapshot, local_node_storage_snapshot_from_membership, ClusterPeerHealthSnapshot, ClusterPoolState, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot,
ClusterRpcChannelSnapshot, ClusterRpcPlane, ClusterRpcTransport, local_node_storage_snapshot_from_membership,
membership_snapshot_from_endpoint_pools, peer_health_snapshot_from_membership, pool_state_snapshot_from_endpoint_pools, membership_snapshot_from_endpoint_pools, peer_health_snapshot_from_membership, pool_state_snapshot_from_endpoint_pools,
topology_snapshot_from_endpoint_pools, topology_snapshot_from_endpoint_pools_with_capabilities, rpc_boundary_snapshot, topology_snapshot_from_endpoint_pools, topology_snapshot_from_endpoint_pools_with_capabilities,
}; };
} }
@@ -32,6 +32,8 @@ const FAILURE_DOMAIN_NOT_REPORTED: &str = "failure domain labels not reported by
const NUMA_NOT_WIRED: &str = "NUMA topology not wired into runtime"; const NUMA_NOT_WIRED: &str = "NUMA topology not wired into runtime";
const PROFILING_NOT_WIRED: &str = "profiling capability not wired into ECStore"; const PROFILING_NOT_WIRED: &str = "profiling capability not wired into ECStore";
const PEER_HEALTH_NOT_REPORTED: &str = "peer health not reported by endpoints"; const PEER_HEALTH_NOT_REPORTED: &str = "peer health not reported by endpoints";
const CONTROL_RPC_SEPARATED: &str = "control RPC remains on the gRPC control plane";
const DATA_STREAM_RPC_SEPARATED: &str = "remote disk data streams remain on the internode data transport";
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ClusterControlPlane { pub struct ClusterControlPlane {
@@ -65,6 +67,10 @@ impl ClusterControlPlane {
peer_health_snapshot_from_membership(&membership) peer_health_snapshot_from_membership(&membership)
} }
pub fn rpc_boundary_snapshot(&self) -> ClusterRpcBoundarySnapshot {
rpc_boundary_snapshot()
}
pub fn read_snapshot(&self) -> ClusterControlPlaneSnapshot { pub fn read_snapshot(&self) -> ClusterControlPlaneSnapshot {
let membership = self.membership_snapshot(); let membership = self.membership_snapshot();
@@ -73,6 +79,7 @@ impl ClusterControlPlane {
pool_state: self.pool_state_snapshot(), pool_state: self.pool_state_snapshot(),
local_storage: local_node_storage_snapshot_from_membership(&membership), local_storage: local_node_storage_snapshot_from_membership(&membership),
peer_health: peer_health_snapshot_from_membership(&membership), peer_health: peer_health_snapshot_from_membership(&membership),
rpc_boundary: self.rpc_boundary_snapshot(),
membership, membership,
} }
} }
@@ -84,6 +91,7 @@ pub struct ClusterControlPlaneSnapshot {
pub pool_state: ClusterPoolStateSnapshot, pub pool_state: ClusterPoolStateSnapshot,
pub local_storage: ClusterLocalNodeStorageSnapshot, pub local_storage: ClusterLocalNodeStorageSnapshot,
pub peer_health: ClusterPeerHealthSnapshot, pub peer_health: ClusterPeerHealthSnapshot,
pub rpc_boundary: ClusterRpcBoundarySnapshot,
pub membership: ClusterMembershipSnapshot, pub membership: ClusterMembershipSnapshot,
} }
@@ -154,6 +162,32 @@ pub struct ClusterPeerHealth {
pub status: CapabilityStatus, pub status: CapabilityStatus,
} }
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ClusterRpcBoundarySnapshot {
pub control_channels: Vec<ClusterRpcChannelSnapshot>,
pub data_channels: Vec<ClusterRpcChannelSnapshot>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClusterRpcChannelSnapshot {
pub name: String,
pub plane: ClusterRpcPlane,
pub transport: ClusterRpcTransport,
pub status: CapabilityStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClusterRpcPlane {
Control,
Data,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClusterRpcTransport {
Grpc,
InternodeDataTransport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ClusterEndpointType { pub enum ClusterEndpointType {
Path, Path,
@@ -313,6 +347,35 @@ pub fn peer_health_snapshot_from_membership(membership: &ClusterMembershipSnapsh
} }
} }
pub fn rpc_boundary_snapshot() -> ClusterRpcBoundarySnapshot {
ClusterRpcBoundarySnapshot {
control_channels: ["metadata", "lock", "health", "administrative"]
.into_iter()
.map(|name| rpc_channel_snapshot(name, ClusterRpcPlane::Control, ClusterRpcTransport::Grpc, CONTROL_RPC_SEPARATED))
.collect(),
data_channels: vec![rpc_channel_snapshot(
"remote_disk_stream",
ClusterRpcPlane::Data,
ClusterRpcTransport::InternodeDataTransport,
DATA_STREAM_RPC_SEPARATED,
)],
}
}
fn rpc_channel_snapshot(
name: &str,
plane: ClusterRpcPlane,
transport: ClusterRpcTransport,
reason: &'static str,
) -> ClusterRpcChannelSnapshot {
ClusterRpcChannelSnapshot {
name: name.to_owned(),
plane,
transport,
status: CapabilityStatus::supported().with_reason(reason),
}
}
fn topology_sets_from_endpoints( fn topology_sets_from_endpoints(
pool_index: usize, pool_index: usize,
drives_per_set: usize, drives_per_set: usize,
@@ -552,9 +615,25 @@ mod tests {
assert_eq!(snapshot.pool_state.pools[0].endpoint_count, 4); assert_eq!(snapshot.pool_state.pools[0].endpoint_count, 4);
assert_eq!(snapshot.local_storage.nodes.len(), 2); assert_eq!(snapshot.local_storage.nodes.len(), 2);
assert_eq!(snapshot.peer_health.peers.len(), 3); assert_eq!(snapshot.peer_health.peers.len(), 3);
assert_eq!(snapshot.rpc_boundary.control_channels.len(), 4);
assert_eq!(snapshot.rpc_boundary.data_channels.len(), 1);
assert_eq!(node_ids, BTreeSet::from([LOCAL_NODE_ID, "node1.example:9000", "node2.example:9000"])); assert_eq!(node_ids, BTreeSet::from([LOCAL_NODE_ID, "node1.example:9000", "node2.example:9000"]));
} }
#[test]
fn rpc_boundary_snapshot_keeps_control_rpc_separate_from_data_streams() {
let snapshot = rpc_boundary_snapshot();
assert!(
snapshot
.control_channels
.iter()
.all(|channel| channel.plane == ClusterRpcPlane::Control && channel.transport == ClusterRpcTransport::Grpc)
);
assert_eq!(snapshot.data_channels[0].plane, ClusterRpcPlane::Data);
assert_eq!(snapshot.data_channels[0].transport, ClusterRpcTransport::InternodeDataTransport);
}
fn sample_path_endpoint_pools() -> EndpointServerPools { fn sample_path_endpoint_pools() -> EndpointServerPools {
let endpoints = (0..4) let endpoints = (0..4)
.map(|index| { .map(|index| {
+3 -2
View File
@@ -18,7 +18,8 @@ pub(crate) mod rpc;
pub use control_plane::{ pub use control_plane::{
ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage,
ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth,
ClusterPeerHealthSnapshot, ClusterPoolState, ClusterPoolStateSnapshot, local_node_storage_snapshot_from_membership, ClusterPeerHealthSnapshot, ClusterPoolState, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot, ClusterRpcChannelSnapshot,
membership_snapshot_from_endpoint_pools, peer_health_snapshot_from_membership, pool_state_snapshot_from_endpoint_pools, ClusterRpcPlane, ClusterRpcTransport, local_node_storage_snapshot_from_membership, membership_snapshot_from_endpoint_pools,
peer_health_snapshot_from_membership, pool_state_snapshot_from_endpoint_pools, rpc_boundary_snapshot,
topology_snapshot_from_endpoint_pools, topology_snapshot_from_endpoint_pools_with_capabilities, topology_snapshot_from_endpoint_pools, topology_snapshot_from_endpoint_pools_with_capabilities,
}; };
+67 -15
View File
@@ -5,23 +5,31 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
## Current Context ## Current Context
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660) - Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
- Branch: `overtrue/arch-ecstore-services-domain-batch` - Branch: `overtrue/arch-cluster-readonly-control-plane-batch`
- Baseline: completed `C-011/C-012/C-013/API-055/API-059/API-079/API-080/API-081/API-082/API-083/API-084/API-085/API-086/API-087/API-088/API-089/API-090/API-091/API-092/API-093/API-094/API-095/API-096/API-097/API-098/API-099/API-100/API-101/API-102/API-103/API-104/API-105/API-106/API-107/API-108/API-109/API-110/API-111/API-112/API-113/API-114/API-115/API-116/API-117/API-118/API-119/API-120/API-121/API-122/API-123/API-124/API-125/API-126/API-127/API-128/API-129/API-130/API-131/API-132/API-133/API-134/API-135/API-136/API-137/API-138/API-139/API-140/API-141/API-142/API-143/API-144/API-145/API-146/API-147/API-148/API-149/API-150/API-151/API-152/API-153/API-154/API-155/API-156/API-157/API-158/API-159/API-160/API-161/API-162/API-163/API-164/API-165/API-166/API-167/API-168/API-169/API-170/API-171/API-172/API-173/API-174/API-175/API-176/API-177/API-178/API-179/API-180/API-181/API-182/API-183/API-184/API-185/API-186/API-187/API-188/API-189/API-190/API-191/API-192/API-193/API-194/API-195/API-196/API-197/API-198/API-199/API-200/API-201/API-202/API-203/API-204/API-205/API-206/API-207/API-208/API-209/API-210/API-211/API-212/API-213/API-214/API-215/API-216/API-217/API-218/API-219/API-220/API-221/API-222/API-223/API-224/API-225/API-226/API-227/API-228/API-229/API-230/API-231/API-232/API-233/API-234/API-235/API-236/API-237/API-238/API-239/API-240/API-241/API-242/API-243/API-244/API-245/API-246/API-247/API-248/API-249/API-250/API-251/API-252/API-253/API-254/CTX-002`. - Baseline: completed `C-011/C-012/C-013/API-055/API-059/API-079/API-080/API-081/API-082/API-083/API-084/API-085/API-086/API-087/API-088/API-089/API-090/API-091/API-092/API-093/API-094/API-095/API-096/API-097/API-098/API-099/API-100/API-101/API-102/API-103/API-104/API-105/API-106/API-107/API-108/API-109/API-110/API-111/API-112/API-113/API-114/API-115/API-116/API-117/API-118/API-119/API-120/API-121/API-122/API-123/API-124/API-125/API-126/API-127/API-128/API-129/API-130/API-131/API-132/API-133/API-134/API-135/API-136/API-137/API-138/API-139/API-140/API-141/API-142/API-143/API-144/API-145/API-146/API-147/API-148/API-149/API-150/API-151/API-152/API-153/API-154/API-155/API-156/API-157/API-158/API-159/API-160/API-161/API-162/API-163/API-164/API-165/API-166/API-167/API-168/API-169/API-170/API-171/API-172/API-173/API-174/API-175/API-176/API-177/API-178/API-179/API-180/API-181/API-182/API-183/API-184/API-185/API-186/API-187/API-188/API-189/API-190/API-191/API-192/API-193/API-194/API-195/API-196/API-197/API-198/API-199/API-200/API-201/API-202/API-203/API-204/API-205/API-206/API-207/API-208/API-209/API-210/API-211/API-212/API-213/API-214/API-215/API-216/API-217/API-218/API-219/API-220/API-221/API-222/API-223/API-224/API-225/API-226/API-227/API-228/API-229/API-230/API-231/API-232/API-233/API-234/API-235/API-236/API-237/API-238/API-239/API-240/API-241/API-242/API-243/API-244/API-245/API-246/API-247/API-248/API-249/API-250/API-251/API-252/API-253/API-254/CTX-002`.
- Current baseline also includes API-255 from PR #3923, API-256 from PR - Current baseline also includes API-255 from PR #3923, API-256 from PR
#3925, and CFG-009 from PR #3927. #3925, and CFG-009 from PR #3927.
- Current phase PR: E-035/E-008 ECStore services domain batch. - Current phase PR: C-007/C-009 cluster read-only control-plane consumer, RPC
- Based on: E-034 branch while PR #3932 and follow-up ECStore owner PRs are boundary, and readiness invariant batch.
pending; rebase onto `origin/main` after E-019 through E-034 merge before - Based on: E-035/E-008 branch while PR #3933 and the E-031 through E-035
opening this PR. follow-up ECStore owner batch are pending; rebase onto `origin/main` after
- PR type for this branch: `pure-move`. those PRs merge before opening this PR.
- Runtime behavior changes: none expected for E-035/E-008; production changes - PR type for this branch: `contract`.
only move the ECStore rebalance and tier owner modules under `services` while - Runtime behavior changes: admin readiness reads keep the existing readiness
retaining the public `api::rebalance` and `api::tier` facades. report collector and, when endpoint context is available, project that same
- Rust code changes: move the ECStore root `rebalance` and `tier` directories report through the read-only cluster snapshot runtime status before returning
into `services`, remove the root `lib.rs` declarations, and migrate readiness; `FullReady` publication now uses the existing storage, IAM, and
crate-internal imports to `services::{rebalance,tier}` without function-body lock quorum readiness invariant.
changes. - Rust code changes: add a read-only cluster RPC boundary snapshot that models
- CI/script changes: reject restoring ECStore root `store.rs`, `set_disk.rs`, metadata/lock/health/admin control RPC as gRPC and remote-disk streams as the
internode data transport, expose it through the ECStore public cluster facade,
include it in RustFS cluster snapshots and admin JSON views, route admin
readiness usecase reads through the snapshot runtime status using the existing
readiness report collector, and require lock quorum before publishing
`FullReady`.
- CI/script changes: guard that the ECStore RPC boundary snapshot and
internode data-transport control-plane separation note remain present; reject
restoring ECStore root `store.rs`, `set_disk.rs`,
`store_list_objects.rs`, `store_utils.rs`, `store_init.rs`, `store_list_objects.rs`, `store_utils.rs`, `store_init.rs`,
`disks_layout.rs`, `endpoints.rs`, `storage_api_contracts.rs`, `disks_layout.rs`, `endpoints.rs`, `storage_api_contracts.rs`,
`batch_processor.rs`, `event_notification.rs`, `metrics_realtime.rs`, or `batch_processor.rs`, `event_notification.rs`, `metrics_realtime.rs`, or
@@ -92,7 +100,8 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
codec or coding modules after E-033/E-004, and reject restoring ECStore root codec or coding modules after E-033/E-004, and reject restoring ECStore root
RPC facade modules or ECStore-internal `crate::rpc` consumers after E-034, RPC facade modules or ECStore-internal `crate::rpc` consumers after E-034,
and reject restoring ECStore root rebalance/tier service-domain modules or and reject restoring ECStore root rebalance/tier service-domain modules or
ECStore-internal `crate::rebalance`/`crate::tier` consumers after E-035/E-008. ECStore-internal `crate::rebalance`/`crate::tier` consumers after E-035/E-008,
and reject losing the C-009 cluster RPC boundary model.
## Phase 0 Tasks ## Phase 0 Tasks
@@ -1386,6 +1395,34 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
- Verification: ECStore peer-health tests, compile coverage, formatting, diff - Verification: ECStore peer-health tests, compile coverage, formatting, diff
hygiene, risk scan, pre-commit quality gate, and three-expert review. hygiene, risk scan, pre-commit quality gate, and three-expert review.
- [x] `C-007` Route admin readiness reads to snapshot.
- Completed slice: route the admin readiness usecase through the read-only
cluster snapshot runtime status when endpoint context is available, while
retaining the existing readiness report collector.
- Acceptance: admin readiness reads use the same runtime status that the
cluster snapshot exposes; systems without endpoint context keep the old
fallback.
- Must preserve: health endpoint semantics, readiness metrics, startup
readiness publication, storage/IAM/lock readiness calculations, and all
S3/RPC gate behavior.
- Verification: focused RustFS cluster snapshot/admin usecase tests, compile
coverage, migration guard, formatting, diff hygiene, risk scan, PR quality
gate, and three-expert review.
- [x] `C-009` Model control RPC separately.
- Completed slice: add a read-only RPC boundary snapshot that keeps
metadata, lock, health, and administrative control RPC on the gRPC control
plane while modeling remote-disk streams as internode data transport.
- Acceptance: cluster snapshots expose separate control and data channel
groups, and migration rules fail if the RPC boundary model or
data-transport separation note is removed.
- Must preserve: internode data stream behavior, remote lock behavior, peer
health behavior, metadata/admin RPC behavior, and all transport selection
defaults.
- Verification: ECStore cluster tests, RustFS cluster snapshot handler tests,
compile coverage, migration guard, formatting, diff hygiene, risk scan, PR
quality gate, and three-expert review.
- [x] `TEST-PRTYPE-001` Check PR type enum consistency. - [x] `TEST-PRTYPE-001` Check PR type enum consistency.
- Acceptance: `./scripts/check_architecture_migration_rules.sh` parses the - Acceptance: `./scripts/check_architecture_migration_rules.sh` parses the
allowed PR types from [`crate-boundaries.md`](crate-boundaries.md) and fails allowed PR types from [`crate-boundaries.md`](crate-boundaries.md) and fails
@@ -9346,6 +9383,21 @@ Notes:
public APIs, boxed public errors, production println/eprintln, or relaxed public APIs, boxed public errors, production println/eprintln, or relaxed
ordering introduced in changed Rust files. ordering introduced in changed Rust files.
- Issue #660 C-007/C-009 current slice:
- `cargo check -p rustfs-ecstore --lib`: passed.
- `cargo check -p rustfs --lib`: passed.
- `cargo test -p rustfs-ecstore --lib cluster`: passed, 98 passed.
- `cargo test -p rustfs --lib cluster_snapshot`: passed, 9 passed including
admin cluster snapshot handler coverage.
- `cargo fmt --all --check`: passed.
- `bash -n scripts/check_architecture_migration_rules.sh`: passed.
- `./scripts/check_architecture_migration_rules.sh`: passed.
- `git diff --check overtrue/arch-ecstore-services-domain-batch...HEAD`:
passed.
- Rust risk scan: passed; no new unwrap/expect, numeric casts, string error
public APIs, boxed public errors, production println/eprintln, or relaxed
ordering introduced in changed Rust files.
## Handoff Notes ## Handoff Notes
- Continue with larger consumer-migration batches outside the cleaned - Continue with larger consumer-migration batches outside the cleaned
+97 -2
View File
@@ -20,7 +20,7 @@ use crate::admin::{
storage_api::cluster::{ storage_api::cluster::{
ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot,
ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterPeerHealthSnapshot, ClusterPoolState, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterPeerHealthSnapshot, ClusterPoolState,
ClusterPoolStateSnapshot, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot, ClusterRpcChannelSnapshot, ClusterRpcPlane, ClusterRpcTransport,
}, },
system, system,
}; };
@@ -60,6 +60,7 @@ pub(crate) struct ClusterSnapshotDiscoveryResponse {
pub summary: Option<CapabilityStatus>, pub summary: Option<CapabilityStatus>,
pub topology: Option<CapabilityStatus>, pub topology: Option<CapabilityStatus>,
pub peer_health: Option<CapabilityStatus>, pub peer_health: Option<CapabilityStatus>,
pub rpc_boundary: Option<CapabilityStatus>,
pub workload_admission: Option<CapabilityStatus>, pub workload_admission: Option<CapabilityStatus>,
pub runtime: Option<CapabilityStatus>, pub runtime: Option<CapabilityStatus>,
} }
@@ -124,6 +125,7 @@ pub(crate) async fn build_cluster_snapshot_discovery_response() -> ClusterSnapsh
summary: Some(summary.actionable_pressure.clone()), summary: Some(summary.actionable_pressure.clone()),
topology: Some(summary.topology), topology: Some(summary.topology),
peer_health: Some(summary.peer_health), peer_health: Some(summary.peer_health),
rpc_boundary: Some(summary.rpc_boundary),
workload_admission: Some(summary.workload_admission), workload_admission: Some(summary.workload_admission),
runtime: Some(summary.runtime), runtime: Some(summary.runtime),
} }
@@ -133,6 +135,7 @@ pub(crate) async fn build_cluster_snapshot_discovery_response() -> ClusterSnapsh
summary: None, summary: None,
topology: None, topology: None,
peer_health: None, peer_health: None,
rpc_boundary: None,
workload_admission: None, workload_admission: None,
runtime: None, runtime: None,
}, },
@@ -149,6 +152,7 @@ pub(crate) struct ClusterSnapshotView {
pub pool_state: ClusterPoolStateView, pub pool_state: ClusterPoolStateView,
pub local_storage: ClusterLocalStorageView, pub local_storage: ClusterLocalStorageView,
pub peer_health: ClusterPeerHealthView, pub peer_health: ClusterPeerHealthView,
pub rpc_boundary: ClusterRpcBoundaryView,
pub observability: ObservabilitySnapshot, pub observability: ObservabilitySnapshot,
pub workload_admission: Vec<WorkloadAdmissionView>, pub workload_admission: Vec<WorkloadAdmissionView>,
pub runtime_status: ClusterRuntimeStatusView, pub runtime_status: ClusterRuntimeStatusView,
@@ -167,6 +171,7 @@ impl From<ClusterReadOnlySnapshot> for ClusterSnapshotView {
pool_state: ClusterPoolStateView::from(snapshot.pool_state), pool_state: ClusterPoolStateView::from(snapshot.pool_state),
local_storage: ClusterLocalStorageView::from(snapshot.local_storage), local_storage: ClusterLocalStorageView::from(snapshot.local_storage),
peer_health: ClusterPeerHealthView::from(snapshot.peer_health), peer_health: ClusterPeerHealthView::from(snapshot.peer_health),
rpc_boundary: ClusterRpcBoundaryView::from(snapshot.rpc_boundary),
observability: snapshot.observability, observability: snapshot.observability,
workload_admission: workload_admission_views(snapshot.workload_admission), workload_admission: workload_admission_views(snapshot.workload_admission),
runtime_status: ClusterRuntimeStatusView::from(snapshot.runtime_status), runtime_status: ClusterRuntimeStatusView::from(snapshot.runtime_status),
@@ -181,6 +186,7 @@ pub(crate) struct ClusterSnapshotSummary {
pub topology: CapabilityStatus, pub topology: CapabilityStatus,
pub membership: CapabilityStatus, pub membership: CapabilityStatus,
pub peer_health: CapabilityStatus, pub peer_health: CapabilityStatus,
pub rpc_boundary: CapabilityStatus,
pub observability: CapabilityStatus, pub observability: CapabilityStatus,
pub workload_admission: CapabilityStatus, pub workload_admission: CapabilityStatus,
pub actionable_pressure: CapabilityStatus, pub actionable_pressure: CapabilityStatus,
@@ -191,6 +197,7 @@ impl From<&ClusterReadOnlySnapshot> for ClusterSnapshotSummary {
let topology = summarize_topology(snapshot); let topology = summarize_topology(snapshot);
let membership = summarize_membership(snapshot); let membership = summarize_membership(snapshot);
let peer_health = summarize_peer_health(snapshot); let peer_health = summarize_peer_health(snapshot);
let rpc_boundary = summarize_rpc_boundary(snapshot);
let observability = summarize_observability(snapshot); let observability = summarize_observability(snapshot);
let workload_admission = summarize_workload_admission(snapshot); let workload_admission = summarize_workload_admission(snapshot);
let actionable_pressure = if cluster_has_actionable_pressure(snapshot) { let actionable_pressure = if cluster_has_actionable_pressure(snapshot) {
@@ -204,6 +211,7 @@ impl From<&ClusterReadOnlySnapshot> for ClusterSnapshotSummary {
topology, topology,
membership, membership,
peer_health, peer_health,
rpc_boundary,
observability, observability,
workload_admission, workload_admission,
actionable_pressure, actionable_pressure,
@@ -372,6 +380,44 @@ impl From<ClusterPeerHealth> for ClusterPeerHealthItemView {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct ClusterRpcBoundaryView {
pub control_channels: Vec<ClusterRpcChannelView>,
pub data_channels: Vec<ClusterRpcChannelView>,
}
impl From<ClusterRpcBoundarySnapshot> for ClusterRpcBoundaryView {
fn from(snapshot: ClusterRpcBoundarySnapshot) -> Self {
Self {
control_channels: snapshot
.control_channels
.into_iter()
.map(ClusterRpcChannelView::from)
.collect(),
data_channels: snapshot.data_channels.into_iter().map(ClusterRpcChannelView::from).collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct ClusterRpcChannelView {
pub name: String,
pub plane: &'static str,
pub transport: &'static str,
pub status: CapabilityStatus,
}
impl From<ClusterRpcChannelSnapshot> for ClusterRpcChannelView {
fn from(channel: ClusterRpcChannelSnapshot) -> Self {
Self {
name: channel.name,
plane: rpc_plane_label(channel.plane),
transport: rpc_transport_label(channel.transport),
status: channel.status,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct WorkloadAdmissionView { pub(crate) struct WorkloadAdmissionView {
pub class: &'static str, pub class: &'static str,
@@ -427,6 +473,20 @@ fn endpoint_type_label(endpoint_type: ClusterEndpointType) -> &'static str {
} }
} }
fn rpc_plane_label(plane: ClusterRpcPlane) -> &'static str {
match plane {
ClusterRpcPlane::Control => "control",
ClusterRpcPlane::Data => "data",
}
}
fn rpc_transport_label(transport: ClusterRpcTransport) -> &'static str {
match transport {
ClusterRpcTransport::Grpc => "grpc",
ClusterRpcTransport::InternodeDataTransport => "internode_data_transport",
}
}
fn workload_class_label(class: WorkloadClass) -> &'static str { fn workload_class_label(class: WorkloadClass) -> &'static str {
class.as_str() class.as_str()
} }
@@ -509,6 +569,16 @@ fn summarize_peer_health(snapshot: &ClusterReadOnlySnapshot) -> CapabilityStatus
} }
} }
fn summarize_rpc_boundary(snapshot: &ClusterReadOnlySnapshot) -> CapabilityStatus {
let has_control = !snapshot.rpc_boundary.control_channels.is_empty();
let has_data = !snapshot.rpc_boundary.data_channels.is_empty();
if has_control && has_data {
CapabilityStatus::supported().with_reason("cluster control RPC and data streams are modeled as separate planes")
} else {
CapabilityStatus::unknown().with_reason("cluster RPC boundary snapshot is incomplete")
}
}
fn summarize_workload_admission(snapshot: &ClusterReadOnlySnapshot) -> CapabilityStatus { fn summarize_workload_admission(snapshot: &ClusterReadOnlySnapshot) -> CapabilityStatus {
let entries = snapshot.workload_admission.entries(); let entries = snapshot.workload_admission.entries();
if entries.is_empty() { if entries.is_empty() {
@@ -595,7 +665,7 @@ mod tests {
use crate::admin::storage_api::cluster::{ use crate::admin::storage_api::cluster::{
ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot,
ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterPeerHealthSnapshot, ClusterPoolState, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterPeerHealthSnapshot, ClusterPoolState,
ClusterPoolStateSnapshot, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot, ClusterRpcChannelSnapshot, ClusterRpcPlane, ClusterRpcTransport,
}; };
use crate::cluster_snapshot::{ClusterReadOnlySnapshot, ClusterRuntimeReadinessState, ClusterRuntimeStatusSnapshot}; use crate::cluster_snapshot::{ClusterReadOnlySnapshot, ClusterRuntimeReadinessState, ClusterRuntimeStatusSnapshot};
use crate::server::{DependencyReadiness, ReadinessDegradedReason}; use crate::server::{DependencyReadiness, ReadinessDegradedReason};
@@ -632,6 +702,7 @@ mod tests {
assert_eq!(response.summary, None); assert_eq!(response.summary, None);
assert_eq!(response.topology, None); assert_eq!(response.topology, None);
assert_eq!(response.peer_health, None); assert_eq!(response.peer_health, None);
assert_eq!(response.rpc_boundary, None);
assert_eq!(response.workload_admission, None); assert_eq!(response.workload_admission, None);
assert_eq!(response.runtime, None); assert_eq!(response.runtime, None);
} }
@@ -684,6 +755,7 @@ mod tests {
status: CapabilityStatus::unknown().with_reason("peer state unavailable"), status: CapabilityStatus::unknown().with_reason("peer state unavailable"),
}], }],
}, },
rpc_boundary: sample_rpc_boundary_snapshot(),
observability: ObservabilitySnapshot::default(), observability: ObservabilitySnapshot::default(),
workload_admission: WorkloadAdmissionRegistrySnapshot::new(vec![ workload_admission: WorkloadAdmissionRegistrySnapshot::new(vec![
WorkloadAdmissionSnapshot::new(WorkloadClass::Repair, AdmissionState::Unknown) WorkloadAdmissionSnapshot::new(WorkloadClass::Repair, AdmissionState::Unknown)
@@ -707,8 +779,12 @@ mod tests {
assert_eq!(value["membership"]["drives"][0]["endpoint_type"], "url"); assert_eq!(value["membership"]["drives"][0]["endpoint_type"], "url");
assert_eq!(value["workload_admission"][0]["class"], "repair"); assert_eq!(value["workload_admission"][0]["class"], "repair");
assert_eq!(value["workload_admission"][0]["state"], "unknown"); assert_eq!(value["workload_admission"][0]["state"], "unknown");
assert_eq!(value["rpc_boundary"]["control_channels"][0]["name"], "metadata");
assert_eq!(value["rpc_boundary"]["control_channels"][0]["transport"], "grpc");
assert_eq!(value["rpc_boundary"]["data_channels"][0]["transport"], "internode_data_transport");
assert_eq!(value["runtime_status"]["state"], "degraded"); assert_eq!(value["runtime_status"]["state"], "degraded");
assert_eq!(value["summary"]["runtime"]["state"], "unknown"); assert_eq!(value["summary"]["runtime"]["state"], "unknown");
assert_eq!(value["summary"]["rpc_boundary"]["state"], "supported");
assert_eq!(value["runtime_status"]["degraded_reasons"][0], "storage_and_lock_unavailable"); assert_eq!(value["runtime_status"]["degraded_reasons"][0], "storage_and_lock_unavailable");
assert_eq!(value["actionable_pressure"], true); assert_eq!(value["actionable_pressure"], true);
} }
@@ -721,6 +797,7 @@ mod tests {
pool_state: ClusterPoolStateSnapshot::default(), pool_state: ClusterPoolStateSnapshot::default(),
local_storage: ClusterLocalNodeStorageSnapshot::default(), local_storage: ClusterLocalNodeStorageSnapshot::default(),
peer_health: ClusterPeerHealthSnapshot::default(), peer_health: ClusterPeerHealthSnapshot::default(),
rpc_boundary: sample_rpc_boundary_snapshot(),
observability: ObservabilitySnapshot::default(), observability: ObservabilitySnapshot::default(),
workload_admission: WorkloadAdmissionRegistrySnapshot::new(vec![WorkloadAdmissionSnapshot::new( workload_admission: WorkloadAdmissionRegistrySnapshot::new(vec![WorkloadAdmissionSnapshot::new(
WorkloadClass::ForegroundRead, WorkloadClass::ForegroundRead,
@@ -741,10 +818,28 @@ mod tests {
assert_eq!(summary.runtime.state, CapabilityState::Supported); assert_eq!(summary.runtime.state, CapabilityState::Supported);
assert_eq!(summary.membership.state, CapabilityState::Unknown); assert_eq!(summary.membership.state, CapabilityState::Unknown);
assert_eq!(summary.peer_health.state, CapabilityState::Unknown); assert_eq!(summary.peer_health.state, CapabilityState::Unknown);
assert_eq!(summary.rpc_boundary.state, CapabilityState::Supported);
assert_eq!(summary.workload_admission.state, CapabilityState::Supported); assert_eq!(summary.workload_admission.state, CapabilityState::Supported);
assert_eq!(summary.actionable_pressure.state, CapabilityState::Disabled); assert_eq!(summary.actionable_pressure.state, CapabilityState::Disabled);
} }
fn sample_rpc_boundary_snapshot() -> ClusterRpcBoundarySnapshot {
ClusterRpcBoundarySnapshot {
control_channels: vec![ClusterRpcChannelSnapshot {
name: "metadata".to_string(),
plane: ClusterRpcPlane::Control,
transport: ClusterRpcTransport::Grpc,
status: CapabilityStatus::supported().with_reason("control RPC remains on gRPC"),
}],
data_channels: vec![ClusterRpcChannelSnapshot {
name: "remote_disk_stream".to_string(),
plane: ClusterRpcPlane::Data,
transport: ClusterRpcTransport::InternodeDataTransport,
status: CapabilityStatus::supported().with_reason("remote disk data streams remain separate"),
}],
}
}
fn extract_block_between_markers(src: &str, start: &str, end: &str) -> String { fn extract_block_between_markers(src: &str, start: &str, end: &str) -> String {
let start_index = src.find(start).expect("start marker should exist"); let start_index = src.find(start).expect("start marker should exist");
let end_index = src[start_index..] let end_index = src[start_index..]
+2 -2
View File
@@ -36,7 +36,7 @@ pub(crate) mod ecstore_cluster {
pub(crate) use crate::storage::storage_api::ecstore_cluster::{ pub(crate) use crate::storage::storage_api::ecstore_cluster::{
ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot,
ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterPeerHealthSnapshot, ClusterPoolState, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterPeerHealthSnapshot, ClusterPoolState,
ClusterPoolStateSnapshot, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot, ClusterRpcChannelSnapshot, ClusterRpcPlane, ClusterRpcTransport,
}; };
} }
@@ -447,7 +447,7 @@ pub(crate) mod cluster {
pub(crate) use super::ecstore_cluster::{ pub(crate) use super::ecstore_cluster::{
ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot,
ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterPeerHealthSnapshot, ClusterPoolState, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterPeerHealthSnapshot, ClusterPoolState,
ClusterPoolStateSnapshot, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot, ClusterRpcChannelSnapshot, ClusterRpcPlane, ClusterRpcTransport,
}; };
pub(crate) use super::storage_contracts::{ pub(crate) use super::storage_contracts::{
CapabilitySnapshotError, CapabilityState, CapabilityStatus, ObservabilitySnapshot, ObservabilitySnapshotProvider, CapabilitySnapshotError, CapabilityState, CapabilityStatus, ObservabilitySnapshot, ObservabilitySnapshotProvider,
+13 -3
View File
@@ -28,9 +28,12 @@ use crate::app::runtime_sources::{
AppContext, current_app_context, resolve_endpoints_handle, resolve_object_store_handle_for_context, AppContext, current_app_context, resolve_endpoints_handle, resolve_object_store_handle_for_context,
}; };
use crate::capacity::resolve_admin_used_capacity; use crate::capacity::resolve_admin_used_capacity;
use crate::cluster_snapshot::{ClusterReadOnlySnapshot, collect_cluster_read_only_snapshot}; use crate::cluster_snapshot::{
ClusterReadOnlySnapshot, ClusterRuntimeStatusSnapshot, cluster_read_only_snapshot_from_endpoint_pools,
collect_cluster_read_only_snapshot,
};
use crate::error::ApiError; use crate::error::ApiError;
use crate::server::{DependencyReadiness, collect_dependency_readiness as collect_runtime_dependency_readiness}; use crate::server::{DependencyReadiness, collect_dependency_readiness_report as collect_runtime_dependency_readiness_report};
use rustfs_data_usage::DataUsageInfo; use rustfs_data_usage::DataUsageInfo;
use rustfs_madmin::{InfoMessage, StorageInfo}; use rustfs_madmin::{InfoMessage, StorageInfo};
use s3s::S3ErrorCode; use s3s::S3ErrorCode;
@@ -579,7 +582,14 @@ impl DefaultAdminUsecase {
} }
pub async fn execute_collect_dependency_readiness(&self) -> DependencyReadiness { pub async fn execute_collect_dependency_readiness(&self) -> DependencyReadiness {
collect_runtime_dependency_readiness().await let report = collect_runtime_dependency_readiness_report().await;
if let Some(endpoint_pools) = resolve_endpoints_handle() {
let runtime_status = ClusterRuntimeStatusSnapshot::from_readiness_report(report);
return cluster_read_only_snapshot_from_endpoint_pools(&endpoint_pools, runtime_status)
.runtime_status
.readiness;
}
report.readiness
} }
pub async fn execute_collect_cluster_read_only_snapshot(&self) -> Option<ClusterReadOnlySnapshot> { pub async fn execute_collect_cluster_read_only_snapshot(&self) -> Option<ClusterReadOnlySnapshot> {
+6 -1
View File
@@ -21,7 +21,7 @@ use crate::storage_api::cluster::contract::observability::ObservabilitySnapshot;
use crate::storage_api::cluster::contract::topology::TopologySnapshot; use crate::storage_api::cluster::contract::topology::TopologySnapshot;
use crate::storage_api::cluster::control_plane::{ use crate::storage_api::cluster::control_plane::{
ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot, ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot,
ClusterPeerHealthSnapshot, ClusterPoolStateSnapshot, ClusterPeerHealthSnapshot, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot,
}; };
use crate::workload_admission::workload_admission_registry_snapshot; use crate::workload_admission::workload_admission_registry_snapshot;
use rustfs_concurrency::{AdmissionState, WorkloadAdmissionRegistrySnapshot}; use rustfs_concurrency::{AdmissionState, WorkloadAdmissionRegistrySnapshot};
@@ -33,6 +33,7 @@ pub struct ClusterReadOnlySnapshot {
pub pool_state: ClusterPoolStateSnapshot, pub pool_state: ClusterPoolStateSnapshot,
pub local_storage: ClusterLocalNodeStorageSnapshot, pub local_storage: ClusterLocalNodeStorageSnapshot,
pub peer_health: ClusterPeerHealthSnapshot, pub peer_health: ClusterPeerHealthSnapshot,
pub rpc_boundary: ClusterRpcBoundarySnapshot,
pub observability: ObservabilitySnapshot, pub observability: ObservabilitySnapshot,
pub workload_admission: WorkloadAdmissionRegistrySnapshot, pub workload_admission: WorkloadAdmissionRegistrySnapshot,
pub runtime_status: ClusterRuntimeStatusSnapshot, pub runtime_status: ClusterRuntimeStatusSnapshot,
@@ -94,6 +95,7 @@ pub fn cluster_read_only_snapshot_from_control_plane(
pool_state: control_plane.pool_state, pool_state: control_plane.pool_state,
local_storage: control_plane.local_storage, local_storage: control_plane.local_storage,
peer_health: control_plane.peer_health, peer_health: control_plane.peer_health,
rpc_boundary: control_plane.rpc_boundary,
observability: runtime_observability_snapshot(), observability: runtime_observability_snapshot(),
workload_admission: workload_admission_registry_snapshot(), workload_admission: workload_admission_registry_snapshot(),
runtime_status, runtime_status,
@@ -165,6 +167,8 @@ mod tests {
assert_eq!(snapshot.pool_state.pools[0].endpoint_count, 4); assert_eq!(snapshot.pool_state.pools[0].endpoint_count, 4);
assert_eq!(snapshot.local_storage.nodes.len(), 1); assert_eq!(snapshot.local_storage.nodes.len(), 1);
assert_eq!(snapshot.peer_health.peers.len(), 2); assert_eq!(snapshot.peer_health.peers.len(), 2);
assert_eq!(snapshot.rpc_boundary.control_channels.len(), 4);
assert_eq!(snapshot.rpc_boundary.data_channels.len(), 1);
assert_eq!(snapshot.runtime_status.state, ClusterRuntimeReadinessState::Degraded); assert_eq!(snapshot.runtime_status.state, ClusterRuntimeReadinessState::Degraded);
assert_eq!( assert_eq!(
snapshot.runtime_status.degraded_reasons, snapshot.runtime_status.degraded_reasons,
@@ -193,6 +197,7 @@ mod tests {
pool_state: ClusterPoolStateSnapshot::default(), pool_state: ClusterPoolStateSnapshot::default(),
local_storage: ClusterLocalNodeStorageSnapshot::default(), local_storage: ClusterLocalNodeStorageSnapshot::default(),
peer_health: ClusterPeerHealthSnapshot::default(), peer_health: ClusterPeerHealthSnapshot::default(),
rpc_boundary: ClusterRpcBoundarySnapshot::default(),
observability: ObservabilitySnapshot::default(), observability: ObservabilitySnapshot::default(),
workload_admission: WorkloadAdmissionRegistrySnapshot::new(vec![WorkloadAdmissionSnapshot::new( workload_admission: WorkloadAdmissionRegistrySnapshot::new(vec![WorkloadAdmissionSnapshot::new(
WorkloadClass::ForegroundRead, WorkloadClass::ForegroundRead,
+1 -1
View File
@@ -67,7 +67,7 @@ pub(crate) use readiness::DependencyReadiness;
pub(crate) use readiness::DependencyReadinessReport; pub(crate) use readiness::DependencyReadinessReport;
pub(crate) use readiness::ReadinessDegradedReason; pub(crate) use readiness::ReadinessDegradedReason;
pub(crate) use readiness::ReadinessGateLayer; pub(crate) use readiness::ReadinessGateLayer;
pub(crate) use readiness::collect_dependency_readiness; pub(crate) use readiness::collect_dependency_readiness_report;
pub(crate) use readiness::collect_node_readiness_report; pub(crate) use readiness::collect_node_readiness_report;
pub use readiness::publish_ready_when_runtime_ready; pub use readiness::publish_ready_when_runtime_ready;
pub(crate) use readiness::snapshot_dependency_readiness_report; pub(crate) use readiness::snapshot_dependency_readiness_report;
+56 -27
View File
@@ -222,6 +222,7 @@ pub async fn publish_ready_when_runtime_ready(
target: "rustfs::server::readiness", target: "rustfs::server::readiness",
storage_ready = dependency_readiness.storage_ready, storage_ready = dependency_readiness.storage_ready,
iam_ready = dependency_readiness.iam_ready, iam_ready = dependency_readiness.iam_ready,
lock_quorum_ready = dependency_readiness.lock_quorum_ready,
"Runtime node readiness reached; publishing ready state" "Runtime node readiness reached; publishing ready state"
); );
}, },
@@ -558,10 +559,6 @@ fn dependency_readiness_report_from_readiness(readiness: DependencyReadiness) ->
} }
} }
pub async fn collect_dependency_readiness() -> DependencyReadiness {
collect_dependency_readiness_report().await.readiness
}
pub async fn collect_dependency_readiness_report() -> DependencyReadinessReport { pub async fn collect_dependency_readiness_report() -> DependencyReadinessReport {
let iam_ready_raw = runtime_sources::iam_ready(); let iam_ready_raw = runtime_sources::iam_ready();
let storage_ready = if let Some(cached) = load_cached_storage_readiness().await { let storage_ready = if let Some(cached) = load_cached_storage_readiness().await {
@@ -595,7 +592,7 @@ pub async fn collect_node_readiness_report() -> DependencyReadinessReport {
let readiness = DependencyReadiness { let readiness = DependencyReadiness {
storage_ready: runtime_sources::object_store_handle().is_some(), storage_ready: runtime_sources::object_store_handle().is_some(),
iam_ready: runtime_sources::iam_ready(), iam_ready: runtime_sources::iam_ready(),
lock_quorum_ready: true, lock_quorum_ready: collect_lock_quorum_status().await.ready,
}; };
let report = dependency_readiness_report_from_readiness(readiness); let report = dependency_readiness_report_from_readiness(readiness);
record_readiness_report(&report); record_readiness_report(&report);
@@ -813,7 +810,7 @@ where
loop { loop {
let readiness = load_readiness().await; let readiness = load_readiness().await;
if readiness.storage_ready && readiness.iam_ready { if readiness.storage_ready && readiness.iam_ready && readiness.lock_quorum_ready {
on_ready(readiness); on_ready(readiness);
return Ok(()); return Ok(());
} }
@@ -970,11 +967,11 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn wait_for_runtime_readiness_with_publishes_ready_without_lock_quorum() { async fn wait_for_runtime_readiness_with_does_not_publish_ready_without_lock_quorum() {
let readiness = GlobalReadiness::new(); let readiness = GlobalReadiness::new();
let state_manager = ServiceStateManager::new(); let state_manager = ServiceStateManager::new();
let result = wait_for_runtime_readiness_with( let err = wait_for_runtime_readiness_with(
Duration::ZERO, Duration::ZERO,
Duration::from_millis(1), Duration::from_millis(1),
|| { || {
@@ -989,9 +986,37 @@ mod tests {
state_manager.update(ServiceState::Ready); state_manager.update(ServiceState::Ready);
}, },
) )
.await
.expect_err("startup readiness should require lock quorum");
assert!(err.to_string().contains("lock_quorum_ready=false"));
assert!(!readiness.is_ready());
assert_eq!(state_manager.current_state(), ServiceState::Starting);
}
#[tokio::test]
async fn wait_for_runtime_readiness_with_publishes_ready_when_dependencies_are_ready() {
let readiness = GlobalReadiness::new();
let state_manager = ServiceStateManager::new();
let result = wait_for_runtime_readiness_with(
Duration::ZERO,
Duration::from_millis(1),
|| {
future::ready(DependencyReadiness {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
})
},
|_| {
readiness.mark_stage(rustfs_common::SystemStage::FullReady);
state_manager.update(ServiceState::Ready);
},
)
.await; .await;
assert!(result.is_ok(), "lock quorum must not block node readiness publication"); assert!(result.is_ok(), "all runtime dependencies should publish readiness");
assert!(readiness.is_ready()); assert!(readiness.is_ready());
assert_eq!(state_manager.current_state(), ServiceState::Ready); assert_eq!(state_manager.current_state(), ServiceState::Ready);
} }
@@ -1264,30 +1289,34 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[serial]
async fn lock_quorum_status_cache_roundtrip() { async fn lock_quorum_status_cache_roundtrip() {
let cache = lock_quorum_status_cache(); async_with_vars([(rustfs_config::ENV_HEALTH_READINESS_CACHE_TTL_MS, Some("60000"))], async {
{ let cache = lock_quorum_status_cache();
let mut guard = cache.lock().await; {
*guard = None; let mut guard = cache.lock().await;
} *guard = None;
}
update_lock_quorum_status_cache(LockQuorumStatus { update_lock_quorum_status_cache(LockQuorumStatus {
ready: true,
connected_clients: 2,
total_clients: 3,
required_quorum: 2,
})
.await;
let cached = load_cached_lock_quorum_status().await;
assert_eq!(
cached,
Some(LockQuorumStatus {
ready: true, ready: true,
connected_clients: 2, connected_clients: 2,
total_clients: 3, total_clients: 3,
required_quorum: 2, required_quorum: 2,
}) })
); .await;
let cached = load_cached_lock_quorum_status().await;
assert_eq!(
cached,
Some(LockQuorumStatus {
ready: true,
connected_clients: 2,
total_clients: 3,
required_quorum: 2,
})
);
})
.await;
} }
} }
+2 -2
View File
@@ -334,8 +334,8 @@ pub(crate) mod ecstore_cluster {
pub(crate) use rustfs_ecstore::api::cluster::{ pub(crate) use rustfs_ecstore::api::cluster::{
ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage,
ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth,
ClusterPeerHealthSnapshot, ClusterPoolState, ClusterPoolStateSnapshot, ClusterPeerHealthSnapshot, ClusterPoolState, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot,
topology_snapshot_from_endpoint_pools_with_capabilities, ClusterRpcChannelSnapshot, ClusterRpcPlane, ClusterRpcTransport, topology_snapshot_from_endpoint_pools_with_capabilities,
}; };
} }
+1 -1
View File
@@ -52,7 +52,7 @@ pub(crate) mod cluster {
pub(crate) mod control_plane { pub(crate) mod control_plane {
pub(crate) use crate::storage::storage_api::ecstore_cluster::{ pub(crate) use crate::storage::storage_api::ecstore_cluster::{
ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot, ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot,
ClusterPeerHealthSnapshot, ClusterPoolStateSnapshot, ClusterPeerHealthSnapshot, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot,
}; };
} }
} }
@@ -3018,6 +3018,19 @@ if [[ -s "$ECSTORE_ROOT_RPC_IMPL_HITS_FILE" ]]; then
report_failure "ECStore internal RPC consumers must use the cluster/rpc owner path: $(paste -sd '; ' "$ECSTORE_ROOT_RPC_IMPL_HITS_FILE")" report_failure "ECStore internal RPC consumers must use the cluster/rpc owner path: $(paste -sd '; ' "$ECSTORE_ROOT_RPC_IMPL_HITS_FILE")"
fi fi
require_source_contains \
"crates/ecstore/src/cluster/control_plane.rs" \
"pub struct ClusterRpcBoundarySnapshot" \
"ECStore cluster RPC boundary snapshot"
require_source_contains \
"crates/ecstore/src/cluster/control_plane.rs" \
"ClusterRpcTransport::Grpc" \
"ECStore cluster control RPC transport model"
require_source_contains \
"crates/ecstore/src/cluster/rpc/internode_data_transport.rs" \
"Internode metadata, lock, health, and administrative calls remain on the" \
"ECStore internode data transport control-plane separation note"
( (
cd "$ROOT_DIR" cd "$ROOT_DIR"
{ {