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:
Zhengchao An
2026-07-07 04:46:42 +08:00
committed by GitHub
parent 68f048b8fe
commit 3420f28c17
9 changed files with 1755 additions and 10 deletions
+76
View File
@@ -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;
}
}
+81
View File
@@ -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();