refactor: batch cluster lock and health readiness (#3936)

This commit is contained in:
Zhengchao An
2026-06-27 10:51:06 +08:00
committed by GitHub
parent 675597ec16
commit e1a4b9e0b6
9 changed files with 312 additions and 59 deletions
+5
View File
@@ -48,3 +48,8 @@ pub const DEFAULT_HEALTH_COMPAT_BUSY_MAX_ACTIVE_REQUESTS: usize = 0;
/// in running state if a global KMS manager exists.
pub const ENV_HEALTH_COMPAT_KMS_READY_CHECK_ENABLE: &str = "RUSTFS_HEALTH_COMPAT_KMS_READY_CHECK_ENABLE";
pub const DEFAULT_HEALTH_COMPAT_KMS_READY_CHECK_ENABLE: bool = false;
/// Enable peer-health readiness impact.
/// When disabled, peer-health state is reported but does not affect readiness.
pub const ENV_HEALTH_PEER_READY_CHECK_ENABLE: &str = "RUSTFS_HEALTH_PEER_READY_CHECK_ENABLE";
pub const DEFAULT_HEALTH_PEER_READY_CHECK_ENABLE: bool = false;
+5 -14
View File
@@ -46,7 +46,6 @@ use rustfs_common::heal_channel::HealOpts;
use rustfs_common::heal_channel::{DriveState, HealItemType};
use rustfs_filemeta::FileInfo;
use rustfs_lock::NamespaceLockWrapper;
use rustfs_lock::client::LockClient;
use rustfs_madmin::heal_commands::{HealDriveInfo, HealResultItem};
use rustfs_utils::{crc_hash, path::path_join_buf, sip_hash};
use std::{collections::HashMap, sync::Arc};
@@ -102,28 +101,17 @@ impl Sets {
let mut disk_set = Vec::with_capacity(set_count);
// Get lock clients from global storage
let lock_clients = runtime_sources::global_lock_clients();
let lock_registry = runtime_sources::lock_registry();
for i in 0..set_count {
let mut set_drive = Vec::with_capacity(set_drive_count);
let mut set_endpoints = Vec::with_capacity(set_drive_count);
let mut set_lock_clients: HashMap<String, Arc<dyn LockClient>> = HashMap::new();
for j in 0..set_drive_count {
let idx = i * set_drive_count + j;
let mut disk = disks[idx].clone();
let endpoint = endpoints.endpoints.as_ref()[idx].clone();
if let Some(lock_clients_map) = lock_clients {
let host_port = endpoint.host_port();
if let Some(lock_client) = lock_clients_map.get(&host_port)
&& !set_lock_clients.contains_key(&host_port)
{
set_lock_clients.insert(host_port, lock_client.clone());
}
}
set_endpoints.push(endpoint);
if disk.is_none() {
@@ -164,7 +152,10 @@ impl Sets {
}
}
let lockers = set_lock_clients.values().cloned().collect::<Vec<Arc<dyn LockClient>>>();
let lockers = lock_registry
.as_ref()
.map(|registry| registry.clients_for_endpoints(&set_endpoints))
.unwrap_or_default();
let set_disks = SetDisks::new(
runtime_sources::local_node_name().await,
Arc::new(RwLock::new(set_drive)),
+75 -1
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
sync::{Arc, OnceLock},
time::SystemTime,
};
@@ -59,6 +59,35 @@ const TEST_RPC_SECRET: &str = "test-rpc-secret";
pub(crate) type WorkloadSnapshotProviderRef = Arc<dyn WorkloadAdmissionSnapshotProvider + Send + Sync>;
#[derive(Clone, Default)]
pub(crate) struct LockRegistry {
clients: HashMap<String, Arc<dyn LockClient>>,
}
impl LockRegistry {
pub(crate) fn new(clients: HashMap<String, Arc<dyn LockClient>>) -> Self {
Self { clients }
}
pub(crate) fn clients_for_endpoints(&self, endpoints: &[Endpoint]) -> Vec<Arc<dyn LockClient>> {
let mut seen_hosts = HashSet::with_capacity(endpoints.len());
let mut clients = Vec::with_capacity(endpoints.len());
for endpoint in endpoints {
let host_port = endpoint.host_port();
if host_port.is_empty() || !seen_hosts.insert(host_port.clone()) {
continue;
}
if let Some(client) = self.clients.get(&host_port) {
clients.push(client.clone());
}
}
clients
}
}
static WORKLOAD_ADMISSION_SNAPSHOT_PROVIDER: OnceLock<WorkloadSnapshotProviderRef> = OnceLock::new();
pub(crate) fn set_workload_admission_snapshot_provider(
@@ -256,6 +285,11 @@ pub(crate) fn global_lock_clients() -> Option<&'static HashMap<String, Arc<dyn L
get_global_lock_clients()
}
pub(crate) fn lock_registry() -> Option<LockRegistry> {
global_lock_clients()
.map(|clients| LockRegistry::new(clients.iter().map(|(host, client)| (host.clone(), client.clone())).collect()))
}
pub(crate) fn set_primary_lock_client(client: Arc<dyn LockClient>) -> std::result::Result<(), Arc<dyn LockClient>> {
set_global_lock_client(client)
}
@@ -469,3 +503,43 @@ pub(crate) async fn initialize_local_disk_maps(endpoint_pools: EndpointServerPoo
pub(crate) async fn init_tier_config_mgr(store: Arc<ECStore>) -> Result<()> {
GLOBAL_TierConfigMgr.write().await.init(store).await
}
#[cfg(test)]
mod tests {
use super::LockRegistry;
use crate::disk::endpoint::Endpoint;
use rustfs_lock::{LocalClient, LockClient};
use std::{collections::HashMap, sync::Arc};
fn url_endpoint(raw: &str) -> Endpoint {
Endpoint {
url: url::Url::parse(raw).expect("test endpoint url"),
is_local: false,
pool_idx: 0,
set_idx: 0,
disk_idx: 0,
}
}
#[test]
fn lock_registry_selects_unique_clients_in_endpoint_order() {
let client_a: Arc<dyn LockClient> = Arc::new(LocalClient::new());
let client_b: Arc<dyn LockClient> = Arc::new(LocalClient::new());
let registry = LockRegistry::new(HashMap::from([
("node-a:9000".to_string(), client_a.clone()),
("node-b:9000".to_string(), client_b.clone()),
]));
let endpoints = vec![
url_endpoint("http://node-a:9000/data-a"),
url_endpoint("http://node-a:9000/data-b"),
url_endpoint("http://node-missing:9000/data"),
url_endpoint("http://node-b:9000/data"),
];
let clients = registry.clients_for_endpoints(&endpoints);
assert_eq!(clients.len(), 2);
assert!(Arc::ptr_eq(&clients[0], &client_a));
assert!(Arc::ptr_eq(&clients[1], &client_b));
}
}
+59 -19
View File
@@ -5,28 +5,23 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
## Current Context
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
- Branch: `overtrue/arch-cluster-readonly-control-plane-batch`
- Branch: `overtrue/arch-cluster-lock-health-phase`
- 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
#3925, and CFG-009 from PR #3927.
- Current phase PR: C-007/C-009 cluster read-only control-plane consumer, RPC
boundary, and readiness invariant batch.
- Based on: E-035/E-008 branch while PR #3933 and the E-031 through E-035
follow-up ECStore owner batch are pending; rebase onto `origin/main` after
those PRs merge before opening this PR.
#3925, CFG-009 from PR #3927, and C-007/C-009 from PR #3935.
- Current phase PR: C-008/C-010 cluster lock-registry consumption and gated
peer-health readiness impact batch.
- Based on: `origin/main` after PR #3935 merged.
- PR type for this branch: `contract`.
- Runtime behavior changes: admin readiness reads keep the existing readiness
report collector and, when endpoint context is available, project that same
report through the read-only cluster snapshot runtime status before returning
readiness; `FullReady` publication now uses the existing storage, IAM, and
lock quorum readiness invariant.
- Rust code changes: add a read-only cluster RPC boundary snapshot that models
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`.
- Runtime behavior changes: lock client selection now consumes a runtime
`LockRegistry` snapshot instead of directly scanning global clients inside
each set; peer-health readiness impact is gated by
`RUSTFS_HEALTH_PEER_READY_CHECK_ENABLE` and remains disabled by default.
- Rust code changes: add the ECStore lock-registry runtime source, route
`Sets::new` through endpoint-ordered registry selection, add a disabled-by
default peer-health readiness gate, carry the peer-health readiness bit
through health and cluster runtime status views, and cover default-disabled
and enabled-degraded readiness behavior.
- 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`,
@@ -1409,6 +1404,20 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
coverage, migration guard, formatting, diff hygiene, risk scan, PR quality
gate, and three-expert review.
- [x] `C-008` Migrate lock group consumption.
- Completed slice: add an ECStore runtime `LockRegistry` snapshot and route
erasure set lock-client selection through endpoint-ordered registry
consumption.
- Acceptance: lock clients are selected by endpoint host-port in set order,
duplicate endpoint entries are deduplicated, missing hosts are skipped, and
existing host-port, quorum, connected-count, and client-count semantics stay
intact.
- Must preserve: remote lock behavior, namespace lock quorum checks, endpoint
layout ownership, and object hot-path behavior.
- Verification: focused ECStore lock-registry test, compile coverage,
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
@@ -1423,6 +1432,20 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
compile coverage, migration guard, formatting, diff hygiene, risk scan, PR
quality gate, and three-expert review.
- [x] `C-010` Peer health impact gate.
- Completed slice: add a disabled-by-default peer-health readiness gate that
consumes the read-only peer-health snapshot before impacting runtime
readiness.
- Acceptance: default-disabled behavior preserves existing readiness results;
when enabled, unsupported or unknown peer health degrades readiness with an
explicit `peer_health_unavailable` reason.
- Must preserve: object hot paths, peer-health snapshot read-only behavior,
storage/IAM/lock readiness behavior when the gate is disabled, and existing
health payload compatibility.
- Verification: focused RustFS readiness, health, and cluster snapshot tests,
compile coverage, formatting, diff hygiene, risk scan, PR quality gate, and
three-expert review.
- [x] `TEST-PRTYPE-001` Check PR type enum consistency.
- Acceptance: `./scripts/check_architecture_migration_rules.sh` parses the
allowed PR types from [`crate-boundaries.md`](crate-boundaries.md) and fails
@@ -9398,6 +9421,23 @@ Notes:
public APIs, boxed public errors, production println/eprintln, or relaxed
ordering introduced in changed Rust files.
- Issue #660 C-008/C-010 current slice:
- `cargo test -p rustfs-ecstore --lib lock_registry`: passed, 1 passed.
- `cargo test -p rustfs --lib readiness`: passed, 33 passed.
- `cargo test -p rustfs --lib health`: passed, 41 passed.
- `cargo test -p rustfs --lib cluster_snapshot`: passed, 9 passed.
- `cargo check -p rustfs-ecstore --lib`: passed.
- `cargo check -p rustfs --lib`: passed.
- `cargo fmt --all --check`: passed.
- `./scripts/check_architecture_migration_rules.sh`: passed.
- `./scripts/check_layer_dependencies.sh`: passed.
- `git diff --check`: passed.
- `make pre-pr`: passed.
- Rust risk scan: passed; only new `.expect()` matches are focused test URL
builders, with no new production unwrap/expect, numeric casts, string error
public APIs, boxed public errors, production println/eprintln, or relaxed
ordering introduced in changed Rust files.
## Handoff Notes
- Continue with larger consumer-migration batches outside the cleaned
@@ -434,6 +434,7 @@ pub(crate) struct ClusterRuntimeStatusView {
pub storage_ready: bool,
pub iam_ready: bool,
pub lock_quorum_ready: bool,
pub peer_health_ready: bool,
pub degraded_reasons: Vec<&'static str>,
}
@@ -444,6 +445,7 @@ impl From<ClusterRuntimeStatusSnapshot> for ClusterRuntimeStatusView {
storage_ready: runtime.readiness.storage_ready,
iam_ready: runtime.readiness.iam_ready,
lock_quorum_ready: runtime.readiness.lock_quorum_ready,
peer_health_ready: runtime.readiness.peer_health_ready,
degraded_reasons: runtime.degraded_reasons.into_iter().map(|reason| reason.as_str()).collect(),
}
}
@@ -767,6 +769,7 @@ mod tests {
storage_ready: false,
iam_ready: true,
lock_quorum_ready: false,
peer_health_ready: true,
},
state: ClusterRuntimeReadinessState::Degraded,
degraded_reasons: vec![ReadinessDegradedReason::StorageAndLockUnavailable],
@@ -808,6 +811,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
state: ClusterRuntimeReadinessState::Ready,
degraded_reasons: Vec::new(),
+15 -8
View File
@@ -92,7 +92,7 @@ mod tests {
#[test]
fn test_readiness_state_ready() {
let state = health_check_state(true, true, true, HealthProbe::Readiness);
let state = health_check_state(true, true, true, true, HealthProbe::Readiness);
assert_eq!(state.status_code, StatusCode::OK);
assert_eq!(state.status, "ok");
assert!(state.ready);
@@ -100,7 +100,7 @@ mod tests {
#[test]
fn test_readiness_state_storage_not_ready() {
let state = health_check_state(false, true, true, HealthProbe::Readiness);
let state = health_check_state(false, true, true, true, HealthProbe::Readiness);
assert_eq!(state.status_code, StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(state.status, "degraded");
assert!(!state.ready);
@@ -108,7 +108,7 @@ mod tests {
#[test]
fn test_liveness_state_iam_not_ready() {
let state = health_check_state(true, false, true, HealthProbe::Liveness);
let state = health_check_state(true, false, true, true, HealthProbe::Liveness);
assert_eq!(state.status_code, StatusCode::OK);
assert_eq!(state.status, "ok");
assert!(state.ready);
@@ -116,7 +116,7 @@ mod tests {
#[test]
fn test_readiness_state_iam_not_ready() {
let state = health_check_state(true, false, true, HealthProbe::Readiness);
let state = health_check_state(true, false, true, true, HealthProbe::Readiness);
assert_eq!(state.status_code, StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(state.status, "degraded");
assert!(!state.ready);
@@ -124,7 +124,7 @@ mod tests {
#[test]
fn test_readiness_state_lock_not_ready() {
let state = health_check_state(true, true, false, HealthProbe::Readiness);
let state = health_check_state(true, true, false, true, HealthProbe::Readiness);
assert_eq!(state.status_code, StatusCode::OK);
assert_eq!(state.status, "ok");
assert!(state.ready);
@@ -132,7 +132,7 @@ mod tests {
#[test]
fn test_cluster_write_state_lock_not_ready() {
let state = health_check_state(true, true, false, HealthProbe::ClusterWrite);
let state = health_check_state(true, true, false, true, HealthProbe::ClusterWrite);
assert_eq!(state.status_code, StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(state.status, "degraded");
assert!(!state.ready);
@@ -197,6 +197,7 @@ mod tests {
storage_ready: false,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
degraded_reasons: vec![crate::server::ReadinessDegradedReason::StorageQuorumUnavailable],
};
@@ -218,6 +219,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
degraded_reasons: Vec::new(),
};
@@ -239,6 +241,7 @@ mod tests {
storage_ready: false,
iam_ready: false,
lock_quorum_ready: false,
peer_health_ready: true,
},
degraded_reasons: vec![crate::server::ReadinessDegradedReason::StorageAndIamUnavailable],
};
@@ -265,6 +268,7 @@ mod tests {
storage_ready: false,
iam_ready: false,
lock_quorum_ready: false,
peer_health_ready: true,
},
degraded_reasons: vec![crate::server::ReadinessDegradedReason::StorageAndIamUnavailable],
};
@@ -282,7 +286,7 @@ mod tests {
#[test]
fn test_build_health_payload_minimal_mode_returns_status_and_ready_only() {
let health = health_check_state(true, false, true, HealthProbe::Readiness);
let health = health_check_state(true, false, true, true, HealthProbe::Readiness);
with_var(rustfs_config::ENV_HEALTH_MINIMAL_RESPONSE_ENABLE, Some("true"), || {
let payload = build_health_payload(HealthPayloadContext {
health,
@@ -306,7 +310,7 @@ mod tests {
#[test]
fn test_build_health_payload_includes_degraded_reasons() {
let health = health_check_state(false, false, false, HealthProbe::Readiness);
let health = health_check_state(false, false, false, true, HealthProbe::Readiness);
let payload = build_health_payload(HealthPayloadContext {
health,
storage_ready: false,
@@ -328,6 +332,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
degraded_reasons: Vec::new(),
};
@@ -344,6 +349,7 @@ mod tests {
storage_ready: false,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
degraded_reasons: vec![crate::server::ReadinessDegradedReason::StorageQuorumUnavailable],
};
@@ -363,6 +369,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
degraded_reasons: Vec::new(),
};
+11 -1
View File
@@ -55,7 +55,11 @@ pub enum ClusterRuntimeReadinessState {
impl ClusterRuntimeReadinessState {
fn from_report(report: &DependencyReadinessReport) -> Self {
if report.readiness.storage_ready && report.readiness.iam_ready && report.readiness.lock_quorum_ready {
if report.readiness.storage_ready
&& report.readiness.iam_ready
&& report.readiness.lock_quorum_ready
&& report.readiness.peer_health_ready
{
Self::Ready
} else if report.degraded_reasons.is_empty() {
Self::Unknown
@@ -131,6 +135,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
degraded_reasons: Vec::new(),
});
@@ -141,6 +146,7 @@ mod tests {
storage_ready: false,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
degraded_reasons: vec![ReadinessDegradedReason::StorageQuorumUnavailable],
});
@@ -156,6 +162,7 @@ mod tests {
storage_ready: false,
iam_ready: true,
lock_quorum_ready: false,
peer_health_ready: true,
},
degraded_reasons: vec![ReadinessDegradedReason::StorageAndLockUnavailable],
});
@@ -208,6 +215,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
state: ClusterRuntimeReadinessState::Ready,
degraded_reasons: Vec::new(),
@@ -221,6 +229,7 @@ mod tests {
storage_ready: false,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
state: ClusterRuntimeReadinessState::Degraded,
degraded_reasons: vec![ReadinessDegradedReason::StorageQuorumUnavailable],
@@ -270,6 +279,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
state: ClusterRuntimeReadinessState::Ready,
degraded_reasons: Vec::new(),
+12 -5
View File
@@ -88,6 +88,7 @@ pub(crate) fn health_check_state(
storage_ready: bool,
iam_ready: bool,
lock_quorum_ready: bool,
peer_health_ready: bool,
probe: HealthProbe,
) -> HealthCheckState {
if probe == HealthProbe::Liveness {
@@ -98,7 +99,7 @@ pub(crate) fn health_check_state(
};
}
let ready = storage_ready && iam_ready && (!probe.requires_lock_quorum() || lock_quorum_ready);
let ready = storage_ready && iam_ready && peer_health_ready && (!probe.requires_lock_quorum() || lock_quorum_ready);
let status = if ready { "ok" } else { "degraded" };
let status_code = if ready {
@@ -184,11 +185,12 @@ pub(crate) fn build_health_response_parts(
let storage_ready = readiness_report.readiness.storage_ready;
let iam_ready = readiness_report.readiness.iam_ready;
let lock_quorum_ready = readiness_report.readiness.lock_quorum_ready;
let peer_health_ready = readiness_report.readiness.peer_health_ready;
(
storage_ready,
iam_ready,
lock_quorum_ready,
health_check_state(storage_ready, iam_ready, lock_quorum_ready, probe),
health_check_state(storage_ready, iam_ready, lock_quorum_ready, peer_health_ready, probe),
readiness_report.degraded_reasons.clone(),
true,
)
@@ -205,9 +207,14 @@ pub(crate) fn build_health_response_parts(
vec![ReadinessDegradedReason::StorageIamAndLockUnavailable],
true,
),
(HealthProbe::Liveness, _) => {
(false, false, false, health_check_state(false, false, false, probe), Vec::new(), false)
}
(HealthProbe::Liveness, _) => (
false,
false,
false,
health_check_state(false, false, false, false, probe),
Vec::new(),
false,
),
};
if probe == HealthProbe::Readiness && matches!(kms_ready, Some(false)) {
+126 -11
View File
@@ -15,6 +15,7 @@
use crate::server::runtime_sources;
use crate::server::{ServiceState, ServiceStateManager};
use crate::server::{has_path_prefix, is_table_catalog_path};
use crate::storage_api::cluster::control_plane::ClusterControlPlane;
use crate::storage_api::server::readiness::contract::admin::StorageAdminApi;
use crate::storage_api::server::readiness::{Endpoint, EndpointServerPools, is_dist_erasure};
#[cfg(test)]
@@ -51,6 +52,7 @@ pub struct DependencyReadiness {
pub storage_ready: bool,
pub iam_ready: bool,
pub lock_quorum_ready: bool,
pub peer_health_ready: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -60,6 +62,7 @@ pub enum ReadinessDegradedReason {
LockQuorumUnavailable,
KmsNotReady,
ClusterHealthTimeout,
PeerHealthUnavailable,
StorageAndIamUnavailable,
StorageAndLockUnavailable,
IamAndLockUnavailable,
@@ -74,6 +77,7 @@ impl ReadinessDegradedReason {
ReadinessDegradedReason::LockQuorumUnavailable => "lock_quorum_unavailable",
ReadinessDegradedReason::KmsNotReady => "kms_not_ready",
ReadinessDegradedReason::ClusterHealthTimeout => "cluster_health_timeout",
ReadinessDegradedReason::PeerHealthUnavailable => "peer_health_unavailable",
ReadinessDegradedReason::StorageAndIamUnavailable => "storage_and_iam_unavailable",
ReadinessDegradedReason::StorageAndLockUnavailable => "storage_and_lock_unavailable",
ReadinessDegradedReason::IamAndLockUnavailable => "iam_and_lock_unavailable",
@@ -223,6 +227,7 @@ pub async fn publish_ready_when_runtime_ready(
storage_ready = dependency_readiness.storage_ready,
iam_ready = dependency_readiness.iam_ready,
lock_quorum_ready = dependency_readiness.lock_quorum_ready,
peer_health_ready = dependency_readiness.peer_health_ready,
"Runtime node readiness reached; publishing ready state"
);
},
@@ -531,7 +536,36 @@ fn storage_read_ready_from_runtime_state(info: &StorageInfo) -> bool {
storage_ready_from_runtime_state_with_quorum(info, pool_read_quorum)
}
fn degraded_reasons(storage_ready: bool, iam_ready_raw: bool, lock_quorum_ready: bool) -> Vec<ReadinessDegradedReason> {
fn peer_health_ready_check_enabled() -> bool {
rustfs_utils::get_env_bool(
rustfs_config::ENV_HEALTH_PEER_READY_CHECK_ENABLE,
rustfs_config::DEFAULT_HEALTH_PEER_READY_CHECK_ENABLE,
)
}
fn peer_health_ready_from_endpoint_pools(endpoint_pools: &EndpointServerPools) -> bool {
if !peer_health_ready_check_enabled() {
return true;
}
ClusterControlPlane::new(endpoint_pools.clone())
.peer_health_snapshot()
.peers
.iter()
.all(|peer| peer.status.state.is_supported())
}
fn collect_peer_health_readiness() -> bool {
if !peer_health_ready_check_enabled() {
return true;
}
runtime_sources::endpoints_handle()
.as_ref()
.is_some_and(peer_health_ready_from_endpoint_pools)
}
fn base_degraded_reasons(storage_ready: bool, iam_ready_raw: bool, lock_quorum_ready: bool) -> Vec<ReadinessDegradedReason> {
match (storage_ready, iam_ready_raw, lock_quorum_ready) {
(true, true, true) => Vec::new(),
(false, false, false) => vec![ReadinessDegradedReason::StorageIamAndLockUnavailable],
@@ -544,8 +578,19 @@ fn degraded_reasons(storage_ready: bool, iam_ready_raw: bool, lock_quorum_ready:
}
}
fn degraded_reasons(readiness: DependencyReadiness) -> Vec<ReadinessDegradedReason> {
let mut reasons = base_degraded_reasons(readiness.storage_ready, readiness.iam_ready, readiness.lock_quorum_ready);
if !readiness.peer_health_ready {
reasons.push(ReadinessDegradedReason::PeerHealthUnavailable);
}
reasons
}
fn record_readiness_report(report: &DependencyReadinessReport) {
let ready = report.readiness.storage_ready && report.readiness.iam_ready && report.readiness.lock_quorum_ready;
let ready = report.readiness.storage_ready
&& report.readiness.iam_ready
&& report.readiness.lock_quorum_ready
&& report.readiness.peer_health_ready;
gauge!(METRIC_RUNTIME_READINESS_READY).set(if ready { 1.0 } else { 0.0 });
for reason in &report.degraded_reasons {
counter!(METRIC_RUNTIME_READINESS_DEGRADED_TOTAL, "reason" => reason.as_str()).increment(1);
@@ -554,7 +599,7 @@ fn record_readiness_report(report: &DependencyReadinessReport) {
fn dependency_readiness_report_from_readiness(readiness: DependencyReadiness) -> DependencyReadinessReport {
DependencyReadinessReport {
degraded_reasons: degraded_reasons(readiness.storage_ready, readiness.iam_ready, readiness.lock_quorum_ready),
degraded_reasons: degraded_reasons(readiness),
readiness,
}
}
@@ -574,6 +619,7 @@ pub async fn collect_dependency_readiness_report() -> DependencyReadinessReport
storage_ready,
iam_ready: iam_ready_raw,
lock_quorum_ready: lock_quorum_status.ready,
peer_health_ready: collect_peer_health_readiness(),
};
let report = dependency_readiness_report_from_readiness(readiness);
record_readiness_report(&report);
@@ -593,6 +639,7 @@ pub async fn collect_node_readiness_report() -> DependencyReadinessReport {
storage_ready: runtime_sources::object_store_handle().is_some(),
iam_ready: runtime_sources::iam_ready(),
lock_quorum_ready: collect_lock_quorum_status().await.ready,
peer_health_ready: collect_peer_health_readiness(),
};
let report = dependency_readiness_report_from_readiness(readiness);
record_readiness_report(&report);
@@ -630,6 +677,7 @@ fn cluster_health_timeout_report() -> DependencyReadinessReport {
storage_ready: false,
iam_ready: false,
lock_quorum_ready: false,
peer_health_ready: false,
},
degraded_reasons: vec![ReadinessDegradedReason::ClusterHealthTimeout],
}
@@ -648,6 +696,7 @@ pub async fn collect_cluster_read_dependency_readiness_report() -> DependencyRea
storage_ready,
iam_ready: iam_ready_raw,
lock_quorum_ready: lock_quorum_status.ready,
peer_health_ready: collect_peer_health_readiness(),
};
let report = dependency_readiness_report_from_readiness(readiness);
record_readiness_report(&report);
@@ -659,6 +708,7 @@ pub(crate) async fn snapshot_dependency_readiness_report() -> DependencyReadines
storage_ready: collect_storage_readiness_uncached().await,
iam_ready: runtime_sources::iam_ready(),
lock_quorum_ready: collect_lock_quorum_status_uncached().await.ready,
peer_health_ready: collect_peer_health_readiness(),
};
dependency_readiness_report_from_readiness(readiness)
@@ -810,18 +860,19 @@ where
loop {
let readiness = load_readiness().await;
if readiness.storage_ready && readiness.iam_ready && readiness.lock_quorum_ready {
if readiness.storage_ready && readiness.iam_ready && readiness.lock_quorum_ready && readiness.peer_health_ready {
on_ready(readiness);
return Ok(());
}
if tokio::time::Instant::now() >= startup_deadline {
let reason = format!(
"startup readiness timed out after {}s: storage_ready={}, iam_ready={}, lock_quorum_ready={}",
"startup readiness timed out after {}s: storage_ready={}, iam_ready={}, lock_quorum_ready={}, peer_health_ready={}",
max_wait.as_secs(),
readiness.storage_ready,
readiness.iam_ready,
readiness.lock_quorum_ready
readiness.lock_quorum_ready,
readiness.peer_health_ready
);
return Err(std::io::Error::other(reason));
}
@@ -831,6 +882,7 @@ where
storage_ready = readiness.storage_ready,
iam_ready = readiness.iam_ready,
lock_quorum_ready = readiness.lock_quorum_ready,
peer_health_ready = readiness.peer_health_ready,
"Runtime node readiness has not been reached yet; delaying ready state publication"
);
tokio::time::sleep(poll_interval).await;
@@ -844,7 +896,7 @@ mod tests {
use serial_test::serial;
use std::future;
use std::sync::atomic::{AtomicUsize, Ordering};
use temp_env::async_with_vars;
use temp_env::{async_with_vars, with_var};
#[test]
fn startup_runtime_readiness_wait_constants_are_ordered() {
@@ -853,6 +905,46 @@ mod tests {
assert_eq!(STARTUP_RUNTIME_READINESS_POLL_INTERVAL.as_secs(), 1);
}
fn peer_health_test_pools() -> EndpointServerPools {
EndpointServerPools::from(vec![PoolEndpoints {
legacy: false,
set_count: 1,
drives_per_set: 2,
endpoints: Endpoints::from(vec![
Endpoint {
url: url::Url::parse("http://node1:9000/data1").expect("test endpoint url"),
is_local: true,
pool_idx: 0,
set_idx: 0,
disk_idx: 0,
},
Endpoint {
url: url::Url::parse("http://node2:9000/data2").expect("test endpoint url"),
is_local: false,
pool_idx: 0,
set_idx: 0,
disk_idx: 1,
},
]),
cmd_line: String::new(),
platform: String::new(),
}])
}
#[test]
fn peer_health_gate_defaults_to_ready() {
with_var(rustfs_config::ENV_HEALTH_PEER_READY_CHECK_ENABLE, Some("false"), || {
assert!(peer_health_ready_from_endpoint_pools(&peer_health_test_pools()));
});
}
#[test]
fn peer_health_gate_degrades_unknown_peer_health_when_enabled() {
with_var(rustfs_config::ENV_HEALTH_PEER_READY_CHECK_ENABLE, Some("true"), || {
assert!(!peer_health_ready_from_endpoint_pools(&peer_health_test_pools()));
});
}
#[tokio::test]
#[serial]
async fn cluster_health_report_singleflight_reuses_inflight_result() {
@@ -869,6 +961,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
degraded_reasons: Vec::new(),
};
@@ -924,6 +1017,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
},
degraded_reasons: Vec::new(),
}
@@ -951,6 +1045,7 @@ mod tests {
storage_ready: false,
iam_ready: false,
lock_quorum_ready: false,
peer_health_ready: true,
})
},
|_| {
@@ -979,6 +1074,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: false,
peer_health_ready: true,
})
},
|_| {
@@ -1007,6 +1103,7 @@ mod tests {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: true,
})
},
|_| {
@@ -1276,18 +1373,36 @@ mod tests {
#[test]
fn degraded_reasons_include_lock_quorum_failures() {
assert_eq!(degraded_reasons(true, true, false), vec![ReadinessDegradedReason::LockQuorumUnavailable]);
assert_eq!(
degraded_reasons(false, true, false),
base_degraded_reasons(true, true, false),
vec![ReadinessDegradedReason::LockQuorumUnavailable]
);
assert_eq!(
base_degraded_reasons(false, true, false),
vec![ReadinessDegradedReason::StorageAndLockUnavailable]
);
assert_eq!(degraded_reasons(true, false, false), vec![ReadinessDegradedReason::IamAndLockUnavailable]);
assert_eq!(
degraded_reasons(false, false, false),
base_degraded_reasons(true, false, false),
vec![ReadinessDegradedReason::IamAndLockUnavailable]
);
assert_eq!(
base_degraded_reasons(false, false, false),
vec![ReadinessDegradedReason::StorageIamAndLockUnavailable]
);
}
#[test]
fn degraded_reasons_append_peer_health_gate_failures() {
let readiness = DependencyReadiness {
storage_ready: true,
iam_ready: true,
lock_quorum_ready: true,
peer_health_ready: false,
};
assert_eq!(degraded_reasons(readiness), vec![ReadinessDegradedReason::PeerHealthUnavailable]);
}
#[tokio::test]
#[serial]
async fn lock_quorum_status_cache_roundtrip() {