mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-18 10:43:15 +00:00
feat(admin): add MinIO-compatible diagnostics endpoints (#4333)
feat(admin): add MinIO-compatible diagnostics and service-control endpoints Register and implement MinIO admin API compatibility routes: - POST /v3/service: restart/stop trigger the existing graceful-shutdown path; freeze/unfreeze record advisory state (request admission is not yet gated, surfaced honestly via effective=false) - profiling/trace and healthinfo/obdinfo/log diagnostics endpoints - GET /v3/top/locks and POST /v3/force-unlock, backed by new FastObjectLockManager::list_locks()/force_unlock() introspection - speedtest routes where feasible Refs rustfs/backlog#604 #606 #607 #615
This commit is contained in:
@@ -290,6 +290,28 @@ impl FastObjectLockManager {
|
||||
shard.get_lock_info(key)
|
||||
}
|
||||
|
||||
/// Enumerate every currently held lock across all shards.
|
||||
///
|
||||
/// Powers the admin "top locks" view. Order is shard-then-insertion and is
|
||||
/// not otherwise stable across calls.
|
||||
pub fn list_locks(&self) -> Vec<crate::fast_lock::types::ObjectLockInfo> {
|
||||
let mut infos = Vec::new();
|
||||
for shard in &self.shards {
|
||||
infos.extend(shard.list_locks());
|
||||
}
|
||||
infos
|
||||
}
|
||||
|
||||
/// Force-release every holder of the lock on `key`.
|
||||
///
|
||||
/// Returns the number of owners released (0 if the resource was not locked).
|
||||
/// This bypasses guard tracking and is intended only for administrative
|
||||
/// recovery of a stuck resource.
|
||||
pub fn force_unlock(&self, key: &crate::fast_lock::types::ObjectKey) -> usize {
|
||||
let shard = self.get_shard(key);
|
||||
shard.force_release_all(key)
|
||||
}
|
||||
|
||||
/// Get aggregated metrics
|
||||
pub fn get_metrics(&self) -> crate::fast_lock::metrics::AggregatedMetrics {
|
||||
let shard_metrics: Vec<_> = self.shards.iter().map(|shard| shard.metrics().snapshot()).collect();
|
||||
@@ -467,6 +489,7 @@ impl LockManager for FastObjectLockManager {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::fast_lock::types::LockMode;
|
||||
|
||||
fn make_request(manager: &FastObjectLockManager, shard_id: usize, suffix: usize) -> ObjectLockRequest {
|
||||
let mut candidate = 0usize;
|
||||
@@ -507,4 +530,57 @@ mod tests {
|
||||
|
||||
manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_locks_reports_held_locks() {
|
||||
let manager = FastObjectLockManager::new();
|
||||
let write_key = ObjectKey::new("bucket", "write-object");
|
||||
let read_key = ObjectKey::new("bucket", "read-object");
|
||||
|
||||
let _write_guard = manager
|
||||
.acquire_write_lock(write_key.clone(), "writer")
|
||||
.await
|
||||
.expect("write lock should acquire");
|
||||
let _read_guard = manager
|
||||
.acquire_read_lock(read_key.clone(), "reader")
|
||||
.await
|
||||
.expect("read lock should acquire");
|
||||
|
||||
let mut locks = manager.list_locks();
|
||||
locks.sort_by(|a, b| a.key.object.cmp(&b.key.object));
|
||||
assert_eq!(locks.len(), 2);
|
||||
|
||||
let read = locks.iter().find(|l| l.key == read_key).expect("read lock listed");
|
||||
assert_eq!(read.mode, LockMode::Shared);
|
||||
assert_eq!(read.owner.as_ref(), "reader");
|
||||
|
||||
let write = locks.iter().find(|l| l.key == write_key).expect("write lock listed");
|
||||
assert_eq!(write.mode, LockMode::Exclusive);
|
||||
assert_eq!(write.owner.as_ref(), "writer");
|
||||
|
||||
manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_force_unlock_releases_stuck_lock() {
|
||||
let manager = FastObjectLockManager::new();
|
||||
let key = ObjectKey::new("bucket", "stuck-object");
|
||||
|
||||
let guard = manager
|
||||
.acquire_write_lock(key.clone(), "owner")
|
||||
.await
|
||||
.expect("write lock should acquire");
|
||||
// Leak the guard so it cannot release on drop, mimicking a stuck lock.
|
||||
std::mem::forget(guard);
|
||||
assert_eq!(manager.total_lock_count(), 1);
|
||||
|
||||
let released = manager.force_unlock(&key);
|
||||
assert_eq!(released, 1);
|
||||
assert!(manager.list_locks().is_empty());
|
||||
|
||||
// Force-unlocking an already-clear resource is a no-op.
|
||||
assert_eq!(manager.force_unlock(&key), 0);
|
||||
|
||||
manager.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +508,87 @@ impl LockShard {
|
||||
None
|
||||
}
|
||||
|
||||
/// Enumerate every currently held lock in this shard.
|
||||
///
|
||||
/// Exclusive locks yield a single entry; shared locks yield one entry per
|
||||
/// distinct owner so administrative "top locks" views can attribute every
|
||||
/// holder. Entries for objects that are tracked but not currently locked
|
||||
/// (e.g. pooled-but-idle state) are skipped.
|
||||
pub fn list_locks(&self) -> Vec<crate::fast_lock::types::ObjectLockInfo> {
|
||||
let objects = self.objects.read();
|
||||
let mut infos = Vec::new();
|
||||
for (key, state) in objects.iter() {
|
||||
let Some(mode) = state.current_mode() else {
|
||||
continue;
|
||||
};
|
||||
let priority = *state.priority.read();
|
||||
match mode {
|
||||
LockMode::Exclusive => {
|
||||
if let Some(info) = state.current_owner.read().clone() {
|
||||
let expires_at = info
|
||||
.acquired_at
|
||||
.checked_add(info.lock_timeout)
|
||||
.unwrap_or_else(|| info.acquired_at + crate::fast_lock::DEFAULT_LOCK_TIMEOUT);
|
||||
infos.push(crate::fast_lock::types::ObjectLockInfo {
|
||||
key: key.clone(),
|
||||
mode,
|
||||
owner: info.owner,
|
||||
acquired_at: info.acquired_at,
|
||||
expires_at,
|
||||
priority,
|
||||
});
|
||||
}
|
||||
}
|
||||
LockMode::Shared => {
|
||||
for entry in state.shared_owners.read().iter() {
|
||||
let expires_at = entry
|
||||
.acquired_at
|
||||
.checked_add(entry.lock_timeout)
|
||||
.unwrap_or_else(|| entry.acquired_at + crate::fast_lock::DEFAULT_LOCK_TIMEOUT);
|
||||
infos.push(crate::fast_lock::types::ObjectLockInfo {
|
||||
key: key.clone(),
|
||||
mode,
|
||||
owner: entry.owner.clone(),
|
||||
acquired_at: entry.acquired_at,
|
||||
expires_at,
|
||||
priority,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
infos
|
||||
}
|
||||
|
||||
/// Force-release every holder of a lock on `key`, regardless of owner.
|
||||
///
|
||||
/// Returns the number of owners that were released. Used by the admin
|
||||
/// force-unlock path to clear a stuck resource.
|
||||
pub fn force_release_all(&self, key: &ObjectKey) -> usize {
|
||||
let owners_modes: Vec<(Arc<str>, LockMode)> = {
|
||||
let objects = self.objects.read();
|
||||
let Some(state) = objects.get(key) else {
|
||||
return 0;
|
||||
};
|
||||
let mut pairs = Vec::new();
|
||||
if let Some(info) = state.current_owner.read().clone() {
|
||||
pairs.push((info.owner, LockMode::Exclusive));
|
||||
}
|
||||
for entry in state.shared_owners.read().iter() {
|
||||
pairs.push((entry.owner.clone(), LockMode::Shared));
|
||||
}
|
||||
pairs
|
||||
};
|
||||
|
||||
let mut released = 0;
|
||||
for (owner, mode) in owners_modes {
|
||||
if self.release_lock(key, &owner, mode) {
|
||||
released += 1;
|
||||
}
|
||||
}
|
||||
released
|
||||
}
|
||||
|
||||
/// Get current load factor of the shard
|
||||
pub fn current_load_factor(&self) -> f64 {
|
||||
let objects = self.objects.read();
|
||||
|
||||
Reference in New Issue
Block a user