Files
rustfs/crates/ecstore/src/disk/health_state.rs
T
2026-06-27 23:34:59 +08:00

254 lines
8.0 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::{DiskAPI, DiskStore};
use crate::disk::endpoint::Endpoint;
use metrics::{counter, gauge};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum RuntimeDriveHealthState {
Online = 0,
Suspect = 1,
Offline = 2,
Returning = 3,
}
impl RuntimeDriveHealthState {
pub fn as_str(self) -> &'static str {
match self {
Self::Online => "online",
Self::Suspect => "suspect",
Self::Offline => "offline",
Self::Returning => "returning",
}
}
pub fn from_u32(value: u32) -> Self {
match value {
1 => Self::Suspect,
2 => Self::Offline,
3 => Self::Returning,
_ => Self::Online,
}
}
pub fn is_snapshot_eligible(self) -> bool {
matches!(self, Self::Online | Self::Suspect | Self::Returning)
}
pub fn is_strictly_online(self) -> bool {
matches!(self, Self::Online)
}
pub fn should_probe_for_admin(self) -> bool {
matches!(self, Self::Online | Self::Returning)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DriveRecoveryClass {
Short,
Medium,
Long,
}
impl DriveRecoveryClass {
pub fn as_str(self) -> &'static str {
match self {
Self::Short => "short_offline",
Self::Medium => "medium_offline",
Self::Long => "long_offline",
}
}
}
pub fn get_drive_suspect_failure_threshold() -> u32 {
rustfs_utils::get_env_u64(
rustfs_config::ENV_DRIVE_SUSPECT_FAILURE_THRESHOLD,
rustfs_config::DEFAULT_DRIVE_SUSPECT_FAILURE_THRESHOLD,
) as u32
}
pub fn get_drive_returning_success_threshold() -> u32 {
rustfs_utils::get_env_u64(
rustfs_config::ENV_DRIVE_RETURNING_SUCCESS_THRESHOLD,
rustfs_config::DEFAULT_DRIVE_RETURNING_SUCCESS_THRESHOLD,
) as u32
}
pub fn get_drive_returning_probe_interval() -> Duration {
Duration::from_secs(rustfs_utils::get_env_u64(
rustfs_config::ENV_DRIVE_RETURNING_PROBE_INTERVAL_SECS,
rustfs_config::DEFAULT_DRIVE_RETURNING_PROBE_INTERVAL_SECS,
))
}
pub fn get_drive_offline_grace_period() -> Duration {
Duration::from_secs(rustfs_utils::get_env_u64(
rustfs_config::ENV_DRIVE_OFFLINE_GRACE_PERIOD_SECS,
rustfs_config::DEFAULT_DRIVE_OFFLINE_GRACE_PERIOD_SECS,
))
}
pub fn get_drive_long_offline_threshold() -> Duration {
Duration::from_secs(rustfs_utils::get_env_u64(
rustfs_config::ENV_DRIVE_LONG_OFFLINE_THRESHOLD_SECS,
rustfs_config::DEFAULT_DRIVE_LONG_OFFLINE_THRESHOLD_SECS,
))
}
pub fn classify_drive_recovery(duration: Duration) -> DriveRecoveryClass {
if duration <= get_drive_offline_grace_period() {
DriveRecoveryClass::Short
} else if duration >= get_drive_long_offline_threshold() {
DriveRecoveryClass::Long
} else {
DriveRecoveryClass::Medium
}
}
pub fn record_drive_runtime_state(endpoint: &Endpoint, state: RuntimeDriveHealthState) {
let endpoint_label = endpoint.to_string();
let pool_label = endpoint.pool_idx.to_string();
let set_label = endpoint.set_idx.to_string();
let disk_label = endpoint.disk_idx.to_string();
for candidate in [
RuntimeDriveHealthState::Online,
RuntimeDriveHealthState::Suspect,
RuntimeDriveHealthState::Offline,
RuntimeDriveHealthState::Returning,
] {
gauge!(
"rustfs_drive_runtime_state",
"endpoint" => endpoint_label.clone(),
"pool" => pool_label.clone(),
"set" => set_label.clone(),
"disk" => disk_label.clone(),
"state" => candidate.as_str().to_string()
)
.set(if candidate == state { 1.0 } else { 0.0 });
}
}
pub fn record_drive_state_transition(
endpoint: &Endpoint,
from: RuntimeDriveHealthState,
to: RuntimeDriveHealthState,
reason: &'static str,
) {
counter!(
"rustfs_drive_state_transition_total",
"endpoint" => endpoint.to_string(),
"pool" => endpoint.pool_idx.to_string(),
"set" => endpoint.set_idx.to_string(),
"disk" => endpoint.disk_idx.to_string(),
"from" => from.as_str().to_string(),
"to" => to.as_str().to_string(),
"reason" => reason.to_string()
)
.increment(1);
}
pub fn record_drive_recovery_class(class: DriveRecoveryClass) {
counter!(
"rustfs_drive_recovery_class_total",
"class" => class.as_str().to_string()
)
.increment(1);
}
pub fn record_drive_offline_duration(endpoint: &Endpoint, duration: Duration) {
gauge!(
"rustfs_drive_offline_duration_seconds",
"endpoint" => endpoint.to_string(),
"pool" => endpoint.pool_idx.to_string(),
"set" => endpoint.set_idx.to_string(),
"disk" => endpoint.disk_idx.to_string()
)
.set(duration.as_secs_f64());
}
#[derive(Debug, Clone, Default)]
pub struct DriveMembershipSnapshot {
pub online: Vec<DiskStore>,
pub suspect: Vec<DiskStore>,
pub returning: Vec<DiskStore>,
pub offline: Vec<DiskStore>,
}
impl DriveMembershipSnapshot {
pub fn from_optional_disks(disks: &[Option<DiskStore>]) -> Self {
let mut snapshot = Self::default();
for disk in disks.iter().flatten() {
match disk.runtime_state() {
RuntimeDriveHealthState::Online => snapshot.online.push(disk.clone()),
RuntimeDriveHealthState::Suspect => snapshot.suspect.push(disk.clone()),
RuntimeDriveHealthState::Returning => snapshot.returning.push(disk.clone()),
RuntimeDriveHealthState::Offline => snapshot.offline.push(disk.clone()),
}
}
snapshot
}
pub fn scanner_heal_candidates(&self) -> Vec<DiskStore> {
let mut disks = Vec::with_capacity(self.online.len() + self.suspect.len() + self.returning.len());
disks.extend(self.online.iter().cloned());
disks.extend(self.suspect.iter().cloned());
disks.extend(self.returning.iter().cloned());
disks
}
pub fn strict_online_candidates(&self) -> Vec<DiskStore> {
self.online.clone()
}
pub fn strict_online_local_candidates(&self) -> Vec<DiskStore> {
self.online.iter().filter(|disk| disk.is_local()).cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_drive_health_state_snapshot_eligibility_matches_membership_policy() {
assert!(RuntimeDriveHealthState::Online.is_snapshot_eligible());
assert!(RuntimeDriveHealthState::Suspect.is_snapshot_eligible());
assert!(RuntimeDriveHealthState::Returning.is_snapshot_eligible());
assert!(!RuntimeDriveHealthState::Offline.is_snapshot_eligible());
}
#[test]
fn runtime_drive_health_state_preserves_strict_online_boundary() {
assert!(RuntimeDriveHealthState::Online.is_strictly_online());
assert!(!RuntimeDriveHealthState::Suspect.is_strictly_online());
assert!(!RuntimeDriveHealthState::Returning.is_strictly_online());
assert!(!RuntimeDriveHealthState::Offline.is_strictly_online());
}
#[test]
fn runtime_drive_health_state_preserves_admin_probe_boundary() {
assert!(RuntimeDriveHealthState::Online.should_probe_for_admin());
assert!(RuntimeDriveHealthState::Returning.should_probe_for_admin());
assert!(!RuntimeDriveHealthState::Suspect.should_probe_for_admin());
assert!(!RuntimeDriveHealthState::Offline.should_probe_for_admin());
}
}