mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-07 04:25:54 +00:00
fix(scanner): add supported usage state reset (#6972)
Add an authenticated scanner usage-state reset endpoint that publishes a fenced bootstrap marker for full rebuilds instead of requiring operators to delete usage metadata by hand. Guard the reset with the scanner leader lock, storage publication epoch, and per-slot revision preconditions, and make startup resumable across stale cleanup leftovers while still rejecting newer conflicting usage state. Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
This commit is contained in:
@@ -152,6 +152,7 @@ mod tests {
|
||||
let _remove_remote_target_handler = replication::RemoveRemoteTargetHandler {};
|
||||
let _scanner_status_handler = scanner::ScannerStatusHandler {};
|
||||
let _scanner_cycle_state_reset_handler = scanner::ScannerCycleStateResetHandler {};
|
||||
let _scanner_usage_state_reset_handler = scanner::ScannerUsageStateResetHandler {};
|
||||
let _ilm_expiry_status_handler = scanner::IlmExpiryStatusHandler {};
|
||||
let _manual_transition_handler = ilm_transition::ManualTransitionRunHandler {};
|
||||
let _manual_transition_status_handler = ilm_transition::ManualTransitionJobStatusHandler {};
|
||||
|
||||
@@ -60,6 +60,12 @@ struct ScannerCycleResetRequest {
|
||||
mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ScannerUsageStateResetRequest {
|
||||
mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ScannerFreshnessStatus {
|
||||
state: &'static str,
|
||||
@@ -233,6 +239,11 @@ pub fn register_scanner_route(r: &mut S3Router<AdminOperation>) -> std::io::Resu
|
||||
format!("{ADMIN_PREFIX}/v3/scanner/cycle-state/reset").as_str(),
|
||||
AdminOperation(&ScannerCycleStateResetHandler {}),
|
||||
)?;
|
||||
r.insert(
|
||||
Method::POST,
|
||||
format!("{ADMIN_PREFIX}/v3/scanner/usage-state/reset").as_str(),
|
||||
AdminOperation(&ScannerUsageStateResetHandler {}),
|
||||
)?;
|
||||
r.insert(
|
||||
Method::GET,
|
||||
format!("{ADMIN_PREFIX}/v3/ilm/expiry/status").as_str(),
|
||||
@@ -303,6 +314,8 @@ pub struct IlmExpiryStatusHandler {}
|
||||
|
||||
pub struct ScannerCycleStateResetHandler {}
|
||||
|
||||
pub struct ScannerUsageStateResetHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for ScannerCycleStateResetHandler {
|
||||
async fn call(&self, mut req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
@@ -332,6 +345,40 @@ impl Operation for ScannerCycleStateResetHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for ScannerUsageStateResetHandler {
|
||||
async fn call(&self, mut req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let _cred = validate_scanner_reset_request(&req).await?;
|
||||
let body = req
|
||||
.input
|
||||
.store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE)
|
||||
.await
|
||||
.map_err(|err| S3Error::with_message(S3ErrorCode::InvalidRequest, format!("invalid reset request body: {err}")))?;
|
||||
let reset = serde_json::from_slice::<ScannerUsageStateResetRequest>(&body)
|
||||
.map_err(|err| S3Error::with_message(S3ErrorCode::InvalidRequest, format!("invalid reset request body: {err}")))?;
|
||||
if reset.mode != "full-rebuild" {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InvalidRequest, "reset mode must be full-rebuild"));
|
||||
}
|
||||
let context = app_context_from_req(&req)
|
||||
.ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "storage layer not initialized"))?;
|
||||
let store = current_object_store_handle_for_context(Some(context.as_ref()))
|
||||
.ok_or_else(|| S3Error::with_message(S3ErrorCode::InternalError, "storage layer not initialized"))?;
|
||||
let result = supervise_admin_mutation("scanner usage state reset", async move {
|
||||
rustfs_scanner::scanner::reset_scanner_usage_state_for_full_rebuild(CancellationToken::new(), store)
|
||||
.await
|
||||
.map_err(|err| S3Error::with_message(S3ErrorCode::InternalError, err.to_string()))
|
||||
})
|
||||
.await?;
|
||||
let body = serde_json::to_vec(&result).map_err(|err| {
|
||||
S3Error::with_message(
|
||||
S3ErrorCode::InternalError,
|
||||
format!("failed to encode scanner usage reset response: {err}"),
|
||||
)
|
||||
})?;
|
||||
json_response(body)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for IlmExpiryStatusHandler {
|
||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
@@ -422,6 +469,20 @@ mod tests {
|
||||
assert!(serde_json::from_str::<ScannerCycleResetRequest>(r#"{"mode":"full-rescan","cursor":"untrusted"}"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_usage_reset_requires_full_rebuild_without_untrusted_fields() {
|
||||
let full_rebuild: ScannerUsageStateResetRequest =
|
||||
serde_json::from_str(r#"{"mode":"full-rebuild"}"#).expect("full rebuild must be accepted");
|
||||
assert_eq!(full_rebuild.mode, "full-rebuild");
|
||||
let cycle_mode: ScannerUsageStateResetRequest =
|
||||
serde_json::from_str(r#"{"mode":"full-rescan"}"#).expect("mode validation belongs to the handler");
|
||||
assert_ne!(cycle_mode.mode, "full-rebuild");
|
||||
assert!(
|
||||
serde_json::from_str::<ScannerUsageStateResetRequest>(r#"{"mode":"full-rebuild","delete_files":[".usage.v2.json"]}"#)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scanner_disabled_reason_reports_startup_env_key() {
|
||||
assert_eq!(scanner_disabled_reason(true), None);
|
||||
|
||||
@@ -445,6 +445,12 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
||||
CONFIG_UPDATE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/scanner/usage-state/reset",
|
||||
CONFIG_UPDATE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/ilm/expiry/status",
|
||||
@@ -2107,6 +2113,12 @@ mod tests {
|
||||
assert_not_action(HttpMethod::Post, "/rustfs/admin/v3/scanner/cycle-state/reset", SERVER_INFO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_policy_requires_config_update_for_scanner_usage_reset() {
|
||||
assert_action(HttpMethod::Post, "/rustfs/admin/v3/scanner/usage-state/reset", CONFIG_UPDATE);
|
||||
assert_not_action(HttpMethod::Post, "/rustfs/admin/v3/scanner/usage-state/reset", SERVER_INFO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_policy_uses_tier_actions_for_transition_routes() {
|
||||
assert_action(HttpMethod::Post, "/rustfs/admin/v3/ilm/transition/run", SET_TIER);
|
||||
|
||||
@@ -255,6 +255,7 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
|
||||
admin_route(Method::PUT, "/v3/config"),
|
||||
admin_route(Method::GET, "/v3/scanner/status"),
|
||||
admin_route(Method::POST, "/v3/scanner/cycle-state/reset"),
|
||||
admin_route(Method::POST, "/v3/scanner/usage-state/reset"),
|
||||
admin_route(Method::GET, "/v3/audit/target/list"),
|
||||
admin_route_sample(
|
||||
Method::PUT,
|
||||
@@ -909,6 +910,7 @@ fn test_register_routes_cover_representative_admin_paths() {
|
||||
assert_route(&router, Method::PUT, &admin_path("/v3/config"));
|
||||
assert_route(&router, Method::GET, &admin_path("/v3/scanner/status"));
|
||||
assert_route(&router, Method::POST, &admin_path("/v3/scanner/cycle-state/reset"));
|
||||
assert_route(&router, Method::POST, &admin_path("/v3/scanner/usage-state/reset"));
|
||||
assert_route(&router, Method::GET, &admin_path("/v3/ilm/expiry/status"));
|
||||
assert_route(&router, Method::POST, &admin_path("/v3/ilm/transition/run"));
|
||||
assert_route(
|
||||
@@ -1399,6 +1401,7 @@ fn test_admin_alias_paths_match_existing_admin_routes() {
|
||||
(Method::PUT, compat_admin_alias_path("/v3/config")),
|
||||
(Method::GET, compat_admin_alias_path("/v3/scanner/status")),
|
||||
(Method::POST, compat_admin_alias_path("/v3/scanner/cycle-state/reset")),
|
||||
(Method::POST, compat_admin_alias_path("/v3/scanner/usage-state/reset")),
|
||||
(Method::GET, compat_admin_alias_path("/v3/ilm/expiry/status")),
|
||||
] {
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user