refactor(app): migrate restore/select and admin info orchestration (#1917)

This commit is contained in:
安正超
2026-02-23 14:20:07 +08:00
committed by GitHub
parent 5b8cbaf7c7
commit 1614cc1b2c
6 changed files with 636 additions and 412 deletions
+282 -8
View File
@@ -15,11 +15,18 @@
//! Admin application use-case contracts.
#![allow(dead_code)]
use crate::app::context::AppContext;
use crate::app::context::{AppContext, get_global_app_context};
use crate::error::ApiError;
use rustfs_common::data_usage::DataUsageInfo;
use rustfs_ecstore::admin_server_info::get_server_info;
use rustfs_madmin::InfoMessage;
use rustfs_ecstore::data_usage::load_data_usage_from_backend;
use rustfs_ecstore::pools::{PoolStatus, get_total_usable_capacity, get_total_usable_capacity_free};
use rustfs_ecstore::store_api::StorageAPI;
use rustfs_ecstore::{GLOBAL_Endpoints, new_object_layer_fn};
use rustfs_madmin::{InfoMessage, StorageInfo};
use s3s::S3ErrorCode;
use std::sync::Arc;
use tracing::{debug, error, info, warn};
pub type AdminUsecaseResult<T> = Result<T, ApiError>;
@@ -38,30 +45,297 @@ impl std::fmt::Debug for QueryServerInfoResponse {
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DependencyReadiness {
pub storage_ready: bool,
pub iam_ready: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct QueryPoolStatusRequest {
pub pool: String,
pub by_id: bool,
}
#[async_trait::async_trait]
pub trait AdminUsecase: Send + Sync {
async fn query_server_info(&self, req: QueryServerInfoRequest) -> AdminUsecaseResult<QueryServerInfoResponse>;
async fn query_storage_info(&self) -> AdminUsecaseResult<StorageInfo>;
async fn query_data_usage_info(&self) -> AdminUsecaseResult<DataUsageInfo>;
async fn list_pool_statuses(&self) -> AdminUsecaseResult<Vec<PoolStatus>>;
async fn query_pool_status(&self, req: QueryPoolStatusRequest) -> AdminUsecaseResult<PoolStatus>;
fn collect_dependency_readiness(&self) -> DependencyReadiness;
}
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct DefaultAdminUsecase {
context: Arc<AppContext>,
context: Option<Arc<AppContext>>,
}
impl DefaultAdminUsecase {
pub fn new(context: Arc<AppContext>) -> Self {
Self { context }
Self { context: Some(context) }
}
pub fn context(&self) -> Arc<AppContext> {
pub fn without_context() -> Self {
Self { context: None }
}
pub fn from_global() -> Self {
Self {
context: get_global_app_context(),
}
}
pub fn context(&self) -> Option<Arc<AppContext>> {
self.context.clone()
}
fn app_error(code: S3ErrorCode, message: impl Into<String>) -> ApiError {
ApiError {
code,
message: message.into(),
source: None,
}
}
fn app_error_default(code: S3ErrorCode) -> ApiError {
let message = ApiError::error_code_to_message(&code);
Self::app_error(code, message)
}
pub async fn execute_query_server_info(&self, req: QueryServerInfoRequest) -> AdminUsecaseResult<QueryServerInfoResponse> {
if let Some(context) = &self.context {
let _ = context.object_store();
}
let info = get_server_info(req.include_pools).await;
Ok(QueryServerInfoResponse { info })
}
pub async fn execute_query_storage_info(&self) -> AdminUsecaseResult<StorageInfo> {
if let Some(context) = &self.context {
let _ = context.object_store();
}
let Some(store) = new_object_layer_fn() else {
return Err(Self::app_error(S3ErrorCode::InternalError, "Not init"));
};
Ok(store.storage_info().await)
}
pub async fn execute_query_data_usage_info(&self) -> AdminUsecaseResult<DataUsageInfo> {
if let Some(context) = &self.context {
let _ = context.object_store();
}
let Some(store) = new_object_layer_fn() else {
return Err(Self::app_error(S3ErrorCode::InternalError, "Not init"));
};
let mut info = load_data_usage_from_backend(store.clone()).await.map_err(|e| {
error!("load_data_usage_from_backend failed {:?}", e);
Self::app_error(S3ErrorCode::InternalError, "load_data_usage_from_backend failed")
})?;
let storage_info = store.storage_info().await;
// Keep the same capacity correction behavior as the previous admin handler implementation.
const MAX_REASONABLE_CAPACITY: u64 = 100_000 * 1024 * 1024 * 1024 * 1024; // 100 PiB
const MIN_REASONABLE_CAPACITY: u64 = 1024 * 1024 * 1024; // 1 GiB
let total_u64 = get_total_usable_capacity(&storage_info.disks, &storage_info) as u64;
let free_u64 = get_total_usable_capacity_free(&storage_info.disks, &storage_info) as u64;
if total_u64 > MAX_REASONABLE_CAPACITY {
error!(
"Abnormal total capacity detected: {} bytes ({:.2} TiB), capping to physical capacity",
total_u64,
total_u64 as f64 / (1024.0_f64.powi(4))
);
let disk_count = storage_info.disks.len();
if disk_count > 0 {
use std::collections::HashSet;
let unique_disks: HashSet<String> = storage_info
.disks
.iter()
.map(|disk| format!("{}|{}", disk.endpoint, disk.drive_path))
.collect();
let actual_disk_count = unique_disks.len();
if let Some(first_disk) = storage_info.disks.first() {
info.total_capacity = first_disk.total_space * actual_disk_count as u64;
info.total_free_capacity = first_disk.available_space * actual_disk_count as u64;
info!(
"Applied capacity correction: {} unique disks, capacity per disk: {} bytes",
actual_disk_count, first_disk.total_space
);
} else {
info.total_capacity = 0;
info.total_free_capacity = 0;
}
} else {
info.total_capacity = 0;
info.total_free_capacity = 0;
}
} else if total_u64 < MIN_REASONABLE_CAPACITY && total_u64 > 0 {
warn!(
"Unusually small total capacity: {} bytes ({:.2} GiB)",
total_u64,
total_u64 as f64 / (1024.0_f64.powi(3))
);
info.total_capacity = total_u64;
info.total_free_capacity = free_u64;
} else {
info.total_capacity = total_u64;
info.total_free_capacity = free_u64;
}
info.total_used_capacity = info.total_capacity.saturating_sub(info.total_free_capacity);
debug!(
"Capacity statistics: total={:.2} TiB, free={:.2} TiB, used={:.2} TiB",
info.total_capacity as f64 / (1024.0_f64.powi(4)),
info.total_free_capacity as f64 / (1024.0_f64.powi(4)),
info.total_used_capacity as f64 / (1024.0_f64.powi(4))
);
Ok(info)
}
pub async fn execute_list_pool_statuses(&self) -> AdminUsecaseResult<Vec<PoolStatus>> {
if let Some(context) = &self.context {
let _ = context.object_store();
}
let Some(store) = new_object_layer_fn() else {
return Err(Self::app_error(S3ErrorCode::InternalError, "Not init"));
};
let Some(endpoints) = GLOBAL_Endpoints.get() else {
return Err(Self::app_error_default(S3ErrorCode::NotImplemented));
};
if endpoints.legacy() {
return Err(Self::app_error_default(S3ErrorCode::NotImplemented));
}
let mut pool_statuses = Vec::new();
for (idx, _) in endpoints.as_ref().iter().enumerate() {
let state = store.status(idx).await.map_err(ApiError::from)?;
pool_statuses.push(state);
}
Ok(pool_statuses)
}
pub async fn execute_query_pool_status(&self, req: QueryPoolStatusRequest) -> AdminUsecaseResult<PoolStatus> {
if let Some(context) = &self.context {
let _ = context.object_store();
}
let Some(endpoints) = GLOBAL_Endpoints.get() else {
return Err(Self::app_error_default(S3ErrorCode::NotImplemented));
};
if endpoints.legacy() {
return Err(Self::app_error_default(S3ErrorCode::NotImplemented));
}
let has_idx = if req.by_id {
let idx = req.pool.parse::<usize>().unwrap_or_default();
if idx < endpoints.as_ref().len() { Some(idx) } else { None }
} else {
endpoints.get_pool_idx(&req.pool)
};
let Some(idx) = has_idx else {
warn!("specified pool {} not found, please specify a valid pool", req.pool);
return Err(Self::app_error_default(S3ErrorCode::InvalidArgument));
};
let Some(store) = new_object_layer_fn() else {
return Err(Self::app_error(S3ErrorCode::InternalError, "Not init"));
};
store.status(idx).await.map_err(ApiError::from)
}
pub fn execute_collect_dependency_readiness(&self) -> DependencyReadiness {
if let Some(context) = &self.context {
let _ = context.object_store();
let _ = context.iam();
}
DependencyReadiness {
storage_ready: new_object_layer_fn().is_some(),
iam_ready: rustfs_iam::get().is_ok(),
}
}
}
#[async_trait::async_trait]
impl AdminUsecase for DefaultAdminUsecase {
async fn query_server_info(&self, req: QueryServerInfoRequest) -> AdminUsecaseResult<QueryServerInfoResponse> {
let info = get_server_info(req.include_pools).await;
Ok(QueryServerInfoResponse { info })
self.execute_query_server_info(req).await
}
async fn query_storage_info(&self) -> AdminUsecaseResult<StorageInfo> {
self.execute_query_storage_info().await
}
async fn query_data_usage_info(&self) -> AdminUsecaseResult<DataUsageInfo> {
self.execute_query_data_usage_info().await
}
async fn list_pool_statuses(&self) -> AdminUsecaseResult<Vec<PoolStatus>> {
self.execute_list_pool_statuses().await
}
async fn query_pool_status(&self, req: QueryPoolStatusRequest) -> AdminUsecaseResult<PoolStatus> {
self.execute_query_pool_status(req).await
}
fn collect_dependency_readiness(&self) -> DependencyReadiness {
self.execute_collect_dependency_readiness()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn execute_query_storage_info_returns_internal_error_when_store_uninitialized() {
let usecase = DefaultAdminUsecase::without_context();
let err = usecase.execute_query_storage_info().await.unwrap_err();
assert_eq!(err.code, S3ErrorCode::InternalError);
}
#[tokio::test]
async fn execute_query_data_usage_info_returns_internal_error_when_store_uninitialized() {
let usecase = DefaultAdminUsecase::without_context();
let err = usecase.execute_query_data_usage_info().await.unwrap_err();
assert_eq!(err.code, S3ErrorCode::InternalError);
}
#[test]
fn execute_collect_dependency_readiness_returns_state_flags() {
let usecase = DefaultAdminUsecase::without_context();
let readiness = usecase.execute_collect_dependency_readiness();
let _ = readiness.storage_ready;
let _ = readiness.iam_ready;
}
}