feat(madmin): add scanner reset client helpers (#7333)

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
This commit is contained in:
houseme
2026-09-07 10:28:42 +08:00
committed by GitHub
parent fbd30a6f43
commit d633a635ec
+127 -1
View File
@@ -230,6 +230,40 @@ pub struct ScannerStatus {
pub extra: serde_json::Map<String, serde_json::Value>,
}
/// `POST /v3/scanner/cycle-state/reset` response for the legacy synchronous
/// full-rescan reset path.
#[derive(Debug, Clone, Deserialize)]
pub struct ScannerCycleResetResponse {
pub status: String,
pub mode: String,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
/// `POST /v3/scanner/usage-state/reset` response for the legacy synchronous
/// full-rebuild reset path.
#[derive(Debug, Clone, Deserialize)]
pub struct ScannerUsageStateResetResponse {
pub status: String,
pub mode: String,
pub usage_state: String,
pub leader_epoch: u64,
pub next_cycle: u64,
pub reset_paths: Vec<String>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Serialize)]
struct ScannerCycleResetRequest<'a> {
mode: &'a str,
}
#[derive(Debug, Serialize)]
struct ScannerUsageStateResetRequest<'a> {
mode: &'a str,
}
/// Freshness block of the scanner status response.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -419,6 +453,25 @@ impl AdminClient {
self.get_json("/v3/scanner/status").await
}
/// Request a legacy synchronous scanner cycle reset (`full-rescan`).
pub async fn scanner_cycle_state_reset_full_rescan(&self) -> Result<ScannerCycleResetResponse, AdminClientError> {
let body =
serde_json::to_vec(&ScannerCycleResetRequest { mode: "full-rescan" }).map_err(|err| AdminClientError::Decode {
message: err.to_string(),
})?;
self.post_json("/v3/scanner/cycle-state/reset", &[], body).await
}
/// Request a legacy synchronous scanner usage reset (`full-rebuild`).
pub async fn scanner_usage_state_reset_full_rebuild(&self) -> Result<ScannerUsageStateResetResponse, AdminClientError> {
let body = serde_json::to_vec(&ScannerUsageStateResetRequest { mode: "full-rebuild" }).map_err(|err| {
AdminClientError::Decode {
message: err.to_string(),
}
})?;
self.post_json("/v3/scanner/usage-state/reset", &[], body).await
}
/// ILM expiry worker status. The payload is owned by the expiry
/// subsystem and still evolving; returned verbatim.
pub async fn ilm_expiry_status(&self) -> Result<serde_json::Value, AdminClientError> {
@@ -587,7 +640,7 @@ pub(crate) fn percent_encode_path_segment(segment: &str) -> String {
mod tests {
use super::{
AdminClient, AdminClientError, BackgroundHealStatus, HealOpts, HealScanMode, HealStartSuccess, HealTaskStatus,
ScannerStatus, heal_path, percent_encode_path_segment,
ScannerCycleResetResponse, ScannerStatus, ScannerUsageStateResetResponse, heal_path, percent_encode_path_segment,
};
use crate::test_support::TestServer;
use serde_json::json;
@@ -730,6 +783,33 @@ mod tests {
assert_eq!(bare.freshness(), "unknown");
}
#[test]
fn scanner_reset_responses_preserve_future_fields() {
let cycle: ScannerCycleResetResponse =
serde_json::from_value(json!({"status": "reset", "mode": "full-rescan", "future": true})).unwrap();
assert_eq!(cycle.status, "reset");
assert_eq!(cycle.mode, "full-rescan");
assert_eq!(cycle.extra["future"], true);
let usage: ScannerUsageStateResetResponse = serde_json::from_value(json!({
"status": "reset",
"mode": "full-rebuild",
"usage_state": "bootstrap-pending",
"leader_epoch": 11,
"next_cycle": 42,
"reset_paths": [".usage.json"],
"future": {"accepted": false}
}))
.unwrap();
assert_eq!(usage.status, "reset");
assert_eq!(usage.mode, "full-rebuild");
assert_eq!(usage.usage_state, "bootstrap-pending");
assert_eq!(usage.leader_epoch, 11);
assert_eq!(usage.next_cycle, 42);
assert_eq!(usage.reset_paths, [".usage.json"]);
assert_eq!(usage.extra["future"]["accepted"], false);
}
#[test]
fn invalid_endpoint_is_rejected_without_io() {
let err = AdminClient::new("not a url", "ak", "sk").unwrap_err();
@@ -772,6 +852,52 @@ mod tests {
assert!(request.body.contains("\"recursive\":true"));
}
#[tokio::test]
async fn scanner_cycle_reset_posts_legacy_full_rescan_request() {
let server = TestServer::spawn(r#"{"status":"reset","mode":"full-rescan"}"#, 200).await;
let client = AdminClient::new(&format!("http://{}", server.addr), "ak", "sk").unwrap();
let reset = client
.scanner_cycle_state_reset_full_rescan()
.await
.expect("cycle reset response decodes");
assert_eq!(reset.status, "reset");
assert_eq!(reset.mode, "full-rescan");
let request = server.recorded();
assert_eq!(request.method, "POST");
assert_eq!(request.path, "/rustfs/admin/v3/scanner/cycle-state/reset");
assert_eq!(request.query, "");
assert!(request.body.contains("\"mode\":\"full-rescan\""));
}
#[tokio::test]
async fn scanner_usage_reset_posts_legacy_full_rebuild_request() {
let server = TestServer::spawn(
r#"{"status":"reset","mode":"full-rebuild","usage_state":"bootstrap-pending","leader_epoch":11,"next_cycle":42,"reset_paths":[".usage.json"]}"#,
200,
)
.await;
let client = AdminClient::new(&format!("http://{}", server.addr), "ak", "sk").unwrap();
let reset = client
.scanner_usage_state_reset_full_rebuild()
.await
.expect("usage reset response decodes");
assert_eq!(reset.status, "reset");
assert_eq!(reset.mode, "full-rebuild");
assert_eq!(reset.usage_state, "bootstrap-pending");
assert_eq!(reset.leader_epoch, 11);
assert_eq!(reset.next_cycle, 42);
assert_eq!(reset.reset_paths, [".usage.json"]);
let request = server.recorded();
assert_eq!(request.method, "POST");
assert_eq!(request.path, "/rustfs/admin/v3/scanner/usage-state/reset");
assert_eq!(request.query, "");
assert!(request.body.contains("\"mode\":\"full-rebuild\""));
}
#[tokio::test]
async fn query_sends_client_token_on_the_same_path() {
let body = r#"{"summary":"running","detail":"","settings":{"recursive":false},"items":[],"truncated":false}"#;