Files
rustfs/rustfs/src/storage/deadlock_detector.rs
T
Zhengchao An d0a965f2ee fix(lock): harden transient quorum and diagnostics (#4268)
* fix(lock): retry transient remote quorum gaps

* perf(storage): reduce deadlock detector blocking

* fix(storage): satisfy deadlock detector clippy
2026-07-05 03:01:20 +08:00

747 lines
24 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.
//! Deadlock Detection for Concurrent Request Monitoring.
//!
//! This module provides deadlock detection capabilities for diagnosing
//! hanging requests and lock contention issues in production systems.
//!
//! # Migration Note
//!
//! This module extends `rustfs_io_core::DeadlockDetector` with request-level
//! resource tracking (memory, file handles). For basic deadlock detection,
//! consider using the io-core version directly:
//!
//! ```ignore
//! // Basic deadlock detection
//! use rustfs_io_core::DeadlockDetector;
//! let detector = DeadlockDetector::with_defaults();
//! ```
//!
//! # Key Features
//!
//! - Request resource tracking (locks, memory, file handles)
//! - Lock wait graph analysis for cycle detection
//! - Configurable detection interval and hang threshold
//! - Deadlock metrics emitted through the shared metrics pipeline
//! - Detailed diagnostic logging
//!
//! # Usage
//!
//! ```ignore
//! use crate::storage::deadlock_detector::{DeadlockDetector, RequestHangDetectionPolicy};
//!
//! let config = RequestHangDetectionPolicy::from_env();
//! let detector = DeadlockDetector::new(config);
//! detector.start();
//!
//! // Track a request
//! let request_id = detector.register_request();
//! detector.record_lock_acquire(request_id, lock_info);
//!
//! // ... request processing ...
//!
//! detector.unregister_request(request_id);
//! ```
// Allow dead_code for public API that may be used by external modules or future features
#![allow(dead_code)]
use parking_lot::{Mutex, RwLock};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::broadcast;
use tracing::{debug, error, warn};
use metrics::counter;
use rustfs_concurrency::DeadlockMonitorPolicy;
use rustfs_io_core::DeadlockDetectorConfig as CoreDeadlockConfig;
/// Request identifier type.
pub type RequestId = String;
/// Lock identifier type.
pub type LockId = String;
/// Request-level hang and deadlock diagnosis policy.
#[derive(Debug, Clone)]
pub struct RequestHangDetectionPolicy {
/// Whether deadlock detection is enabled.
pub enabled: bool,
/// Detection check interval.
pub check_interval: Duration,
/// Hang threshold - requests running longer than this are considered potentially hung.
pub hang_threshold: Duration,
/// Whether to capture backtraces (expensive, only for debugging).
pub capture_backtrace: bool,
}
impl Default for RequestHangDetectionPolicy {
fn default() -> Self {
Self {
enabled: rustfs_config::DEFAULT_OBJECT_DEADLOCK_DETECTION_ENABLE,
check_interval: Duration::from_secs(rustfs_config::DEFAULT_OBJECT_DEADLOCK_CHECK_INTERVAL),
hang_threshold: Duration::from_secs(rustfs_config::DEFAULT_OBJECT_DEADLOCK_HANG_THRESHOLD),
capture_backtrace: false,
}
}
}
impl RequestHangDetectionPolicy {
/// Load configuration from environment variables.
pub fn from_env() -> Self {
let enabled = rustfs_utils::get_env_bool(
rustfs_config::ENV_OBJECT_DEADLOCK_DETECTION_ENABLE,
rustfs_config::DEFAULT_OBJECT_DEADLOCK_DETECTION_ENABLE,
);
let check_interval = Duration::from_secs(rustfs_utils::get_env_u64(
rustfs_config::ENV_OBJECT_DEADLOCK_CHECK_INTERVAL,
rustfs_config::DEFAULT_OBJECT_DEADLOCK_CHECK_INTERVAL,
));
let hang_threshold = Duration::from_secs(rustfs_utils::get_env_u64(
rustfs_config::ENV_OBJECT_DEADLOCK_HANG_THRESHOLD,
rustfs_config::DEFAULT_OBJECT_DEADLOCK_HANG_THRESHOLD,
));
Self {
enabled,
check_interval,
hang_threshold,
capture_backtrace: false,
}
}
/// Convert the request-level policy into the shared io-core deadlock config.
pub fn to_core_config(&self) -> CoreDeadlockConfig {
self.to_concurrency_policy().to_core_config()
}
/// Convert the request-level policy into the shared concurrency facade policy.
pub fn to_concurrency_policy(&self) -> DeadlockMonitorPolicy {
DeadlockMonitorPolicy {
enabled: self.enabled,
check_interval: self.check_interval,
hang_threshold: self.hang_threshold,
}
}
}
/// Lock information for tracking.
#[derive(Debug, Clone)]
pub struct LockInfo {
/// Lock identifier.
pub id: LockId,
/// Lock type (read/write).
pub lock_type: LockType,
/// Resource being locked (bucket/key).
pub resource: String,
/// When the lock was acquired.
pub acquire_time: Instant,
}
/// Lock type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockType {
/// Read lock (shared).
Read,
/// Write lock (exclusive).
Write,
}
impl std::fmt::Display for LockType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LockType::Read => write!(f, "read"),
LockType::Write => write!(f, "write"),
}
}
}
/// Resource type being tracked.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResourceType {
/// Lock resource.
Lock,
/// Memory resource.
Memory,
/// File handle.
FileHandle,
/// I/O permit.
IoPermit,
}
/// Request resource tracker.
#[derive(Debug, Clone)]
pub struct RequestResourceTracker {
/// Request ID.
pub request_id: RequestId,
/// When the request started.
pub start_time: Instant,
/// Locks currently held by this request.
pub held_locks: Vec<LockInfo>,
/// Lock this request is waiting for (if any).
pub waiting_lock: Option<LockInfo>,
/// Resources held by this request.
pub resources: HashMap<ResourceType, usize>,
/// Request description (e.g., "GetObject bucket/key").
pub description: String,
}
impl RequestResourceTracker {
/// Create a new tracker for a request.
pub fn new(request_id: RequestId, description: impl Into<String>) -> Self {
Self {
request_id,
start_time: Instant::now(),
held_locks: Vec::new(),
waiting_lock: None,
resources: HashMap::new(),
description: description.into(),
}
}
/// Get the elapsed time since the request started.
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
/// Check if this request is potentially hung.
pub fn is_hung(&self, threshold: Duration) -> bool {
self.elapsed() > threshold
}
}
/// Deadlock detection result.
#[derive(Debug, Clone)]
pub struct DeadlockInfo {
/// Time of detection.
pub detected_at: Instant,
/// Requests involved in the deadlock cycle.
pub cycle: Vec<RequestId>,
/// Lock wait graph showing the deadlock.
pub wait_graph: Vec<WaitGraphEdge>,
/// Resource usage at detection time.
pub resource_usage: ResourceUsage,
}
/// Edge in the lock wait graph.
#[derive(Debug, Clone)]
pub struct WaitGraphEdge {
/// Request that is waiting.
pub from: RequestId,
/// Request that is blocking (holds the lock).
pub to: RequestId,
/// The lock being waited for.
pub lock_id: LockId,
}
/// Resource usage snapshot.
#[derive(Debug, Clone, Default)]
pub struct ResourceUsage {
/// Total memory used (bytes).
pub memory_bytes: usize,
/// Total file handles open.
pub open_files: usize,
/// Total active requests.
pub active_requests: usize,
/// Total locks held.
pub locks_held: usize,
}
/// Deadlock detector.
pub struct DeadlockDetector {
/// Configuration.
config: RequestHangDetectionPolicy,
/// Shared concurrency facade policy.
policy: DeadlockMonitorPolicy,
/// Active request trackers.
requests: Arc<RwLock<HashMap<RequestId, RequestResourceTracker>>>,
/// Detection task handle.
detector_task: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
/// Shutdown signal.
shutdown_tx: broadcast::Sender<()>,
/// Total deadlocks detected.
deadlocks_detected: Arc<AtomicU64>,
/// Is currently running.
running: Arc<AtomicBool>,
}
impl DeadlockDetector {
/// Create a new deadlock detector.
pub fn new(config: RequestHangDetectionPolicy) -> Self {
let (shutdown_tx, _) = broadcast::channel(1);
let policy = config.to_concurrency_policy();
Self {
config,
policy,
requests: Arc::new(RwLock::new(HashMap::new())),
detector_task: Arc::new(Mutex::new(None)),
shutdown_tx,
deadlocks_detected: Arc::new(AtomicU64::new(0)),
running: Arc::new(AtomicBool::new(false)),
}
}
/// Check if detection is enabled.
pub fn is_enabled(&self) -> bool {
self.policy.enabled
}
/// Start the detection task.
pub fn start(&self) {
if !self.policy.enabled {
debug!("Deadlock detection is disabled");
return;
}
if self.running.swap(true, Ordering::Relaxed) {
debug!("Deadlock detector already running");
return;
}
let requests = self.requests.clone();
let policy = self.policy;
let deadlocks_detected = self.deadlocks_detected.clone();
let mut shutdown_rx = self.shutdown_tx.subscribe();
let running = self.running.clone();
let handle = tokio::spawn(async move {
debug!(
check_interval_secs = policy.check_interval.as_secs(),
hang_threshold_secs = policy.hang_threshold.as_secs(),
"Deadlock detector started"
);
loop {
tokio::select! {
_ = tokio::time::sleep(policy.check_interval) => {
Self::detect_cycle(&requests, &policy, &deadlocks_detected);
}
_ = shutdown_rx.recv() => {
debug!("Deadlock detector shutting down");
break;
}
}
}
running.store(false, Ordering::Relaxed);
});
*self.detector_task.lock() = Some(handle);
}
/// Stop the detection task.
pub fn stop(&self) {
let _ = self.shutdown_tx.send(());
if let Some(handle) = self.detector_task.lock().take() {
// Don't await the handle as we're in a non-async context
handle.abort();
}
self.running.store(false, Ordering::Relaxed);
}
/// Register a new request for tracking.
pub fn register_request(&self, request_id: impl Into<String>, description: impl Into<String>) {
if !self.policy.enabled {
return;
}
let request_id = request_id.into();
let tracker = RequestResourceTracker::new(request_id.clone(), description);
self.requests.write().insert(request_id.clone(), tracker);
debug!(request_id = %request_id, "Request registered for deadlock tracking");
}
/// Unregister a request (it completed or was cancelled).
pub fn unregister_request(&self, request_id: &str) {
if !self.policy.enabled {
return;
}
self.requests.write().remove(request_id);
debug!(request_id = %request_id, "Request unregistered from deadlock tracking");
}
/// Record a lock acquisition.
pub fn record_lock_acquire(&self, request_id: &str, lock: LockInfo) {
if !self.policy.enabled {
return;
}
if let Some(tracker) = self.requests.write().get_mut(request_id) {
tracker.held_locks.push(lock);
}
}
/// Record a lock release.
pub fn record_lock_release(&self, request_id: &str, lock_id: &LockId) {
if !self.policy.enabled {
return;
}
if let Some(tracker) = self.requests.write().get_mut(request_id) {
tracker.held_locks.retain(|l| &l.id != lock_id);
}
}
/// Record that a request is waiting for a lock.
pub fn record_lock_wait(&self, request_id: &str, lock: LockInfo) {
if !self.policy.enabled {
return;
}
if let Some(tracker) = self.requests.write().get_mut(request_id) {
tracker.waiting_lock = Some(lock);
}
}
/// Clear the waiting lock (acquired or gave up).
pub fn clear_lock_wait(&self, request_id: &str) {
if !self.policy.enabled {
return;
}
if let Some(tracker) = self.requests.write().get_mut(request_id) {
tracker.waiting_lock = None;
}
}
/// Record resource usage.
pub fn record_resource(&self, request_id: &str, resource_type: ResourceType, amount: usize) {
if !self.policy.enabled {
return;
}
if let Some(tracker) = self.requests.write().get_mut(request_id) {
tracker.resources.insert(resource_type, amount);
}
}
/// Get current number of tracked requests.
pub fn tracked_count(&self) -> usize {
self.requests.read().len()
}
/// Get total deadlocks detected.
pub fn total_detected(&self) -> u64 {
self.deadlocks_detected.load(Ordering::Relaxed)
}
/// Detect deadlock cycles in the lock wait graph.
fn detect_cycle(
requests: &Arc<RwLock<HashMap<RequestId, RequestResourceTracker>>>,
policy: &DeadlockMonitorPolicy,
deadlocks_detected: &Arc<AtomicU64>,
) {
let hung_request_snapshot: Vec<_> = requests
.read()
.values()
.filter(|r| r.is_hung(policy.hang_threshold))
.cloned()
.collect();
if hung_request_snapshot.is_empty() {
return;
}
// Build lock wait graph
// Edge: request A -> request B means A is waiting for a lock that B holds
let mut wait_graph: Vec<WaitGraphEdge> = Vec::new();
for waiting in &hung_request_snapshot {
if let Some(waiting_for) = &waiting.waiting_lock {
// Find who holds this lock
for holding in &hung_request_snapshot {
if holding.request_id == waiting.request_id {
continue;
}
if holding.held_locks.iter().any(|l| l.id == waiting_for.id) {
wait_graph.push(WaitGraphEdge {
from: waiting.request_id.clone(),
to: holding.request_id.clone(),
lock_id: waiting_for.id.clone(),
});
}
}
}
}
// Detect cycles using DFS
if let Some(cycle) = Self::find_cycle(&wait_graph) {
deadlocks_detected.fetch_add(1, Ordering::Relaxed);
counter!("rustfs_deadlock_detected_total").increment(1);
// Log detailed deadlock information
error!(
cycle = ?cycle,
wait_graph = ?wait_graph,
hung_requests_count = hung_request_snapshot.len(),
"Deadlock detected: circular lock wait chain found"
);
// Log each request in the cycle
for request_id in &cycle {
if let Some(tracker) = hung_request_snapshot.iter().find(|tracker| &tracker.request_id == request_id) {
warn!(
request_id = %request_id,
description = %tracker.description,
elapsed_secs = tracker.elapsed().as_secs(),
held_locks = ?tracker.held_locks.iter().map(|l| &l.id).collect::<Vec<_>>(),
waiting_lock = ?tracker.waiting_lock.as_ref().map(|l| &l.id),
"Request in deadlock cycle"
);
}
}
} else {
// No cycle, but log hung requests for diagnosis
debug!(
hung_requests_count = hung_request_snapshot.len(),
wait_graph_edges = wait_graph.len(),
"Hung requests detected but no deadlock cycle"
);
}
}
/// Find a cycle in the wait graph using DFS.
fn find_cycle(edges: &[WaitGraphEdge]) -> Option<Vec<RequestId>> {
if edges.is_empty() {
return None;
}
// Build adjacency list: from -> [to]
let mut graph: HashMap<&RequestId, Vec<&RequestId>> = HashMap::new();
for edge in edges {
graph.entry(&edge.from).or_default().push(&edge.to);
}
let mut visited: HashSet<&RequestId> = HashSet::new();
let mut path: Vec<&RequestId> = Vec::new();
let mut path_set: HashSet<&RequestId> = HashSet::new();
for start in graph.keys() {
if visited.contains(start) {
continue;
}
if Self::dfs_find_cycle(start, &graph, &mut visited, &mut path, &mut path_set) {
return Some(path.iter().map(|s| (*s).clone()).collect());
}
}
None
}
/// DFS helper for cycle detection.
fn dfs_find_cycle<'a>(
node: &'a RequestId,
graph: &HashMap<&'a RequestId, Vec<&'a RequestId>>,
visited: &mut HashSet<&'a RequestId>,
path: &mut Vec<&'a RequestId>,
path_set: &mut HashSet<&'a RequestId>,
) -> bool {
visited.insert(node);
path.push(node);
path_set.insert(node);
if let Some(neighbors) = graph.get(&node) {
for neighbor in neighbors {
if path_set.contains(neighbor)
&& let Some(cycle_start) = path.iter().position(|n| *n == *neighbor)
{
path.drain(0..cycle_start);
return true;
}
if !visited.contains(neighbor) && Self::dfs_find_cycle(neighbor, graph, visited, path, path_set) {
return true;
}
}
}
path.pop();
path_set.remove(node);
false
}
}
impl Drop for DeadlockDetector {
fn drop(&mut self) {
self.stop();
}
}
/// Global deadlock detector instance.
static DEADLOCK_DETECTOR: std::sync::OnceLock<Arc<DeadlockDetector>> = std::sync::OnceLock::new();
/// Get the global deadlock detector.
pub fn get_deadlock_detector() -> Arc<DeadlockDetector> {
DEADLOCK_DETECTOR
.get_or_init(|| {
let config = RequestHangDetectionPolicy::from_env();
Arc::new(DeadlockDetector::new(config))
})
.clone()
}
/// Initialize and start the global deadlock detector.
pub fn init_deadlock_detector() {
let detector = get_deadlock_detector();
detector.start();
}
/// Check if deadlock detection is enabled.
pub fn is_deadlock_detection_enabled() -> bool {
rustfs_utils::get_env_bool(
rustfs_config::ENV_OBJECT_DEADLOCK_DETECTION_ENABLE,
rustfs_config::DEFAULT_OBJECT_DEADLOCK_DETECTION_ENABLE,
)
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::{DeadlockDetector, LockInfo, LockType, RequestHangDetectionPolicy, RequestResourceTracker, WaitGraphEdge};
use std::time::{Duration, Instant};
#[test]
fn test_deadlock_detector_config_default() {
let config = RequestHangDetectionPolicy::default();
assert!(!config.enabled);
assert_eq!(config.check_interval, Duration::from_secs(5));
assert_eq!(config.hang_threshold, Duration::from_secs(10));
}
#[test]
fn test_request_hang_policy_projects_to_concurrency_and_core_config() {
let config = RequestHangDetectionPolicy {
enabled: true,
check_interval: Duration::from_secs(7),
hang_threshold: Duration::from_secs(11),
capture_backtrace: true,
};
let concurrency = config.to_concurrency_policy();
let core = config.to_core_config();
assert!(concurrency.enabled);
assert_eq!(concurrency.check_interval, config.check_interval);
assert_eq!(concurrency.hang_threshold, config.hang_threshold);
assert!(core.enabled);
assert_eq!(core.detection_interval, config.check_interval);
assert_eq!(core.max_hold_time, config.hang_threshold);
assert!(config.capture_backtrace);
}
#[test]
fn test_deadlock_detector_consumes_concurrency_policy() {
let config = RequestHangDetectionPolicy {
enabled: true,
check_interval: Duration::from_secs(7),
hang_threshold: Duration::from_secs(11),
capture_backtrace: true,
};
let concurrency = config.to_concurrency_policy();
let detector = DeadlockDetector::new(config);
assert!(detector.is_enabled());
assert_eq!(detector.policy.check_interval, concurrency.check_interval);
assert_eq!(detector.policy.hang_threshold, concurrency.hang_threshold);
assert!(detector.config.capture_backtrace);
}
#[test]
fn test_request_resource_tracker() {
let tracker = RequestResourceTracker::new("req-1".to_string(), "GetObject bucket/key");
assert!(tracker.elapsed() < Duration::from_secs(1));
assert!(!tracker.is_hung(Duration::from_secs(10)));
}
#[test]
fn test_lock_info() {
let lock = LockInfo {
id: "lock-1".to_string(),
lock_type: LockType::Read,
resource: "bucket/key".to_string(),
acquire_time: Instant::now(),
};
assert_eq!(format!("{}", lock.lock_type), "read");
}
#[test]
fn test_deadlock_detector_registration() {
let config = RequestHangDetectionPolicy {
enabled: true,
..Default::default()
};
let detector = DeadlockDetector::new(config);
detector.register_request("req-1", "Test request");
assert_eq!(detector.tracked_count(), 1);
detector.unregister_request("req-1");
assert_eq!(detector.tracked_count(), 0);
}
#[test]
fn test_cycle_detection() {
// Create a simple cycle: A -> B -> C -> A
let edges = vec![
WaitGraphEdge {
from: "A".to_string(),
to: "B".to_string(),
lock_id: "lock-1".to_string(),
},
WaitGraphEdge {
from: "B".to_string(),
to: "C".to_string(),
lock_id: "lock-2".to_string(),
},
WaitGraphEdge {
from: "C".to_string(),
to: "A".to_string(),
lock_id: "lock-3".to_string(),
},
];
let cycle = DeadlockDetector::find_cycle(&edges);
assert!(cycle.is_some());
let cycle = cycle.unwrap();
assert!(cycle.len() >= 2);
}
#[test]
fn test_no_cycle() {
// No cycle: A -> B -> C
let edges = vec![
WaitGraphEdge {
from: "A".to_string(),
to: "B".to_string(),
lock_id: "lock-1".to_string(),
},
WaitGraphEdge {
from: "B".to_string(),
to: "C".to_string(),
lock_id: "lock-2".to_string(),
},
];
let cycle = DeadlockDetector::find_cycle(&edges);
assert!(cycle.is_none());
}
}