mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 19:42:17 +00:00
refactor: NamespaceLock (nslock), AHM→Heal Crate, and Lock/Clippy Fixes (#1664)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: weisd <2057561+weisd@users.noreply.github.com> Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! test endpoint index settings
|
||||
|
||||
use rustfs_ecstore::disk::endpoint::Endpoint;
|
||||
use rustfs_ecstore::endpoints::{EndpointServerPools, Endpoints, PoolEndpoints};
|
||||
use std::net::SocketAddr;
|
||||
use tempfile::TempDir;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn test_endpoint_index_settings() -> anyhow::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
// create test disk paths
|
||||
let disk_paths: Vec<_> = (0..4).map(|i| temp_dir.path().join(format!("disk{i}"))).collect();
|
||||
|
||||
for path in &disk_paths {
|
||||
tokio::fs::create_dir_all(path).await?;
|
||||
}
|
||||
|
||||
// build endpoints
|
||||
let mut endpoints: Vec<Endpoint> = disk_paths
|
||||
.iter()
|
||||
.map(|p| Endpoint::try_from(p.to_string_lossy().as_ref()).unwrap())
|
||||
.collect();
|
||||
|
||||
// set endpoint indexes correctly
|
||||
for (i, endpoint) in endpoints.iter_mut().enumerate() {
|
||||
endpoint.set_pool_index(0);
|
||||
endpoint.set_set_index(0);
|
||||
endpoint.set_disk_index(i); // note: disk_index is usize type
|
||||
println!(
|
||||
"Endpoint {}: pool_idx={}, set_idx={}, disk_idx={}",
|
||||
i, endpoint.pool_idx, endpoint.set_idx, endpoint.disk_idx
|
||||
);
|
||||
}
|
||||
|
||||
let pool_endpoints = PoolEndpoints {
|
||||
legacy: false,
|
||||
set_count: 1,
|
||||
drives_per_set: endpoints.len(),
|
||||
endpoints: Endpoints::from(endpoints.clone()),
|
||||
cmd_line: "test".to_string(),
|
||||
platform: format!("OS: {} | Arch: {}", std::env::consts::OS, std::env::consts::ARCH),
|
||||
};
|
||||
|
||||
let endpoint_pools = EndpointServerPools(vec![pool_endpoints]);
|
||||
|
||||
// validate all endpoint indexes are in valid range
|
||||
for (i, ep) in endpoints.iter().enumerate() {
|
||||
assert_eq!(ep.pool_idx, 0, "Endpoint {i} pool_idx should be 0");
|
||||
assert_eq!(ep.set_idx, 0, "Endpoint {i} set_idx should be 0");
|
||||
assert_eq!(ep.disk_idx, i as i32, "Endpoint {i} disk_idx should be {i}");
|
||||
println!(
|
||||
"Endpoint {} indices are valid: pool={}, set={}, disk={}",
|
||||
i, ep.pool_idx, ep.set_idx, ep.disk_idx
|
||||
);
|
||||
}
|
||||
|
||||
// test ECStore initialization
|
||||
rustfs_ecstore::store::init_local_disks(endpoint_pools.clone()).await?;
|
||||
|
||||
let server_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
let ecstore = rustfs_ecstore::store::ECStore::new(server_addr, endpoint_pools, CancellationToken::new()).await?;
|
||||
|
||||
println!("ECStore initialized successfully with {} pools", ecstore.pools.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use rustfs_heal::heal::{
|
||||
event::{HealEvent, Severity},
|
||||
task::{HealPriority, HealType},
|
||||
utils,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_heal_event_to_heal_request_no_panic() {
|
||||
use rustfs_ecstore::disk::endpoint::Endpoint;
|
||||
|
||||
// Test that invalid pool/set indices don't cause panic
|
||||
// Create endpoint using try_from or similar method
|
||||
let endpoint_result = Endpoint::try_from("http://localhost:9000");
|
||||
if let Ok(mut endpoint) = endpoint_result {
|
||||
endpoint.pool_idx = -1;
|
||||
endpoint.set_idx = -1;
|
||||
endpoint.disk_idx = 0;
|
||||
|
||||
let event = HealEvent::DiskStatusChange {
|
||||
endpoint,
|
||||
old_status: "ok".to_string(),
|
||||
new_status: "offline".to_string(),
|
||||
};
|
||||
|
||||
// Should return error instead of panicking
|
||||
let result = event.to_heal_request();
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("Invalid heal type"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_event_to_heal_request_valid_indices() {
|
||||
use rustfs_ecstore::disk::endpoint::Endpoint;
|
||||
|
||||
// Test that valid indices work correctly
|
||||
let endpoint_result = Endpoint::try_from("http://localhost:9000");
|
||||
if let Ok(mut endpoint) = endpoint_result {
|
||||
endpoint.pool_idx = 0;
|
||||
endpoint.set_idx = 1;
|
||||
endpoint.disk_idx = 0;
|
||||
|
||||
let event = HealEvent::DiskStatusChange {
|
||||
endpoint,
|
||||
old_status: "ok".to_string(),
|
||||
new_status: "offline".to_string(),
|
||||
};
|
||||
|
||||
let result = event.to_heal_request();
|
||||
assert!(result.is_ok());
|
||||
let request = result.unwrap();
|
||||
assert!(matches!(request.heal_type, HealType::ErasureSet { .. }));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_event_object_corruption() {
|
||||
let event = HealEvent::ObjectCorruption {
|
||||
bucket: "test-bucket".to_string(),
|
||||
object: "test-object".to_string(),
|
||||
version_id: None,
|
||||
corruption_type: rustfs_heal::heal::event::CorruptionType::DataCorruption,
|
||||
severity: Severity::High,
|
||||
};
|
||||
|
||||
let result = event.to_heal_request();
|
||||
assert!(result.is_ok());
|
||||
let request = result.unwrap();
|
||||
assert!(matches!(request.heal_type, HealType::Object { .. }));
|
||||
assert_eq!(request.priority, HealPriority::High);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_event_ec_decode_failure() {
|
||||
let event = HealEvent::ECDecodeFailure {
|
||||
bucket: "test-bucket".to_string(),
|
||||
object: "test-object".to_string(),
|
||||
version_id: None,
|
||||
missing_shards: vec![0, 1],
|
||||
available_shards: vec![2, 3],
|
||||
};
|
||||
|
||||
let result = event.to_heal_request();
|
||||
assert!(result.is_ok());
|
||||
let request = result.unwrap();
|
||||
assert!(matches!(request.heal_type, HealType::ECDecode { .. }));
|
||||
assert_eq!(request.priority, HealPriority::Urgent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_set_disk_id_from_i32_negative() {
|
||||
// Test that negative indices return None
|
||||
assert!(utils::format_set_disk_id_from_i32(-1, 0).is_none());
|
||||
assert!(utils::format_set_disk_id_from_i32(0, -1).is_none());
|
||||
assert!(utils::format_set_disk_id_from_i32(-1, -1).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_set_disk_id_from_i32_valid() {
|
||||
// Test that valid indices return Some
|
||||
let result = utils::format_set_disk_id_from_i32(0, 1);
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), "pool_0_set_1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resume_state_timestamp_handling() {
|
||||
use rustfs_heal::heal::resume::ResumeState;
|
||||
|
||||
// Test that ResumeState creation doesn't panic even if system time is before epoch
|
||||
// This is a theoretical test - in practice, system time should never be before epoch
|
||||
// But we want to ensure unwrap_or_default handles edge cases
|
||||
let state = ResumeState::new(
|
||||
"test-task".to_string(),
|
||||
"test-type".to_string(),
|
||||
"pool_0_set_1".to_string(),
|
||||
vec!["bucket1".to_string()],
|
||||
);
|
||||
|
||||
// Verify fields are initialized (u64 is always >= 0)
|
||||
// The important thing is that unwrap_or_default prevents panic
|
||||
let _ = state.start_time;
|
||||
let _ = state.last_update;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resume_checkpoint_timestamp_handling() {
|
||||
use rustfs_heal::heal::resume::ResumeCheckpoint;
|
||||
|
||||
// Test that ResumeCheckpoint creation doesn't panic
|
||||
let checkpoint = ResumeCheckpoint::new("test-task".to_string());
|
||||
|
||||
// Verify field is initialized (u64 is always >= 0)
|
||||
// The important thing is that unwrap_or_default prevents panic
|
||||
let _ = checkpoint.checkpoint_time;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_to_str_helper() {
|
||||
use std::path::Path;
|
||||
|
||||
// Test that path conversion handles non-UTF-8 paths gracefully
|
||||
// Note: This is a compile-time test - actual non-UTF-8 paths are hard to construct in Rust
|
||||
// The helper function should properly handle the conversion
|
||||
let valid_path = Path::new("test/path");
|
||||
assert!(valid_path.to_str().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_task_status_atomic_update() {
|
||||
use rustfs_heal::heal::storage::HealStorageAPI;
|
||||
use rustfs_heal::heal::task::{HealOptions, HealRequest, HealTask, HealTaskStatus};
|
||||
use std::sync::Arc;
|
||||
|
||||
// Mock storage for testing
|
||||
struct MockStorage;
|
||||
#[async_trait::async_trait]
|
||||
impl HealStorageAPI for MockStorage {
|
||||
async fn get_object_meta(
|
||||
&self,
|
||||
_bucket: &str,
|
||||
_object: &str,
|
||||
) -> rustfs_heal::Result<Option<rustfs_ecstore::store_api::ObjectInfo>> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn get_object_data(&self, _bucket: &str, _object: &str) -> rustfs_heal::Result<Option<Vec<u8>>> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn put_object_data(&self, _bucket: &str, _object: &str, _data: &[u8]) -> rustfs_heal::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn delete_object(&self, _bucket: &str, _object: &str) -> rustfs_heal::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn verify_object_integrity(&self, _bucket: &str, _object: &str) -> rustfs_heal::Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
async fn ec_decode_rebuild(&self, _bucket: &str, _object: &str) -> rustfs_heal::Result<Vec<u8>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_disk_status(
|
||||
&self,
|
||||
_endpoint: &rustfs_ecstore::disk::endpoint::Endpoint,
|
||||
) -> rustfs_heal::Result<rustfs_heal::heal::storage::DiskStatus> {
|
||||
Ok(rustfs_heal::heal::storage::DiskStatus::Ok)
|
||||
}
|
||||
async fn format_disk(&self, _endpoint: &rustfs_ecstore::disk::endpoint::Endpoint) -> rustfs_heal::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn get_bucket_info(&self, _bucket: &str) -> rustfs_heal::Result<Option<rustfs_ecstore::store_api::BucketInfo>> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn heal_bucket_metadata(&self, _bucket: &str) -> rustfs_heal::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn list_buckets(&self) -> rustfs_heal::Result<Vec<rustfs_ecstore::store_api::BucketInfo>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn object_exists(&self, _bucket: &str, _object: &str) -> rustfs_heal::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
async fn get_object_size(&self, _bucket: &str, _object: &str) -> rustfs_heal::Result<Option<u64>> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn get_object_checksum(&self, _bucket: &str, _object: &str) -> rustfs_heal::Result<Option<String>> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn heal_object(
|
||||
&self,
|
||||
_bucket: &str,
|
||||
_object: &str,
|
||||
_version_id: Option<&str>,
|
||||
_opts: &rustfs_common::heal_channel::HealOpts,
|
||||
) -> rustfs_heal::Result<(rustfs_madmin::heal_commands::HealResultItem, Option<rustfs_heal::Error>)> {
|
||||
Ok((rustfs_madmin::heal_commands::HealResultItem::default(), None))
|
||||
}
|
||||
async fn heal_bucket(
|
||||
&self,
|
||||
_bucket: &str,
|
||||
_opts: &rustfs_common::heal_channel::HealOpts,
|
||||
) -> rustfs_heal::Result<rustfs_madmin::heal_commands::HealResultItem> {
|
||||
Ok(rustfs_madmin::heal_commands::HealResultItem::default())
|
||||
}
|
||||
async fn heal_format(
|
||||
&self,
|
||||
_dry_run: bool,
|
||||
) -> rustfs_heal::Result<(rustfs_madmin::heal_commands::HealResultItem, Option<rustfs_heal::Error>)> {
|
||||
Ok((rustfs_madmin::heal_commands::HealResultItem::default(), None))
|
||||
}
|
||||
async fn list_objects_for_heal(&self, _bucket: &str, _prefix: &str) -> rustfs_heal::Result<Vec<String>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn list_objects_for_heal_page(
|
||||
&self,
|
||||
_bucket: &str,
|
||||
_prefix: &str,
|
||||
_continuation_token: Option<&str>,
|
||||
) -> rustfs_heal::Result<(Vec<String>, Option<String>, bool)> {
|
||||
Ok((vec![], None, false))
|
||||
}
|
||||
async fn get_disk_for_resume(&self, _set_disk_id: &str) -> rustfs_heal::Result<rustfs_ecstore::disk::DiskStore> {
|
||||
Err(rustfs_heal::Error::other("Not implemented in mock"))
|
||||
}
|
||||
}
|
||||
|
||||
// Create a heal request and task
|
||||
let request = HealRequest::new(
|
||||
HealType::Object {
|
||||
bucket: "test-bucket".to_string(),
|
||||
object: "test-object".to_string(),
|
||||
version_id: None,
|
||||
},
|
||||
HealOptions::default(),
|
||||
HealPriority::Normal,
|
||||
);
|
||||
|
||||
let storage: Arc<dyn HealStorageAPI> = Arc::new(MockStorage);
|
||||
let task = HealTask::from_request(request, storage);
|
||||
|
||||
// Verify initial status
|
||||
let status = tokio::runtime::Runtime::new().unwrap().block_on(task.get_status());
|
||||
assert_eq!(status, HealTaskStatus::Pending);
|
||||
|
||||
// The task should have task_start_instant field initialized
|
||||
// This is an internal detail, but we can verify it doesn't cause issues
|
||||
// by checking that the task can be created successfully
|
||||
// Note: We can't directly access private fields, but creation without panic
|
||||
// confirms the fix works
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use rustfs_common::heal_channel::{HealOpts, HealScanMode};
|
||||
use rustfs_ecstore::{
|
||||
disk::endpoint::Endpoint,
|
||||
endpoints::{EndpointServerPools, Endpoints, PoolEndpoints},
|
||||
store::ECStore,
|
||||
store_api::{ObjectIO, ObjectOptions, PutObjReader, StorageAPI},
|
||||
};
|
||||
use rustfs_heal::heal::{
|
||||
manager::{HealConfig, HealManager},
|
||||
storage::{ECStoreHealStorage, HealStorageAPI},
|
||||
task::{HealOptions, HealPriority, HealRequest, HealTaskStatus, HealType},
|
||||
};
|
||||
use serial_test::serial;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Once, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::fs;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::info;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
static GLOBAL_ENV: OnceLock<(Vec<PathBuf>, Arc<ECStore>, Arc<ECStoreHealStorage>)> = OnceLock::new();
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
pub fn init_tracing() {
|
||||
INIT.call_once(|| {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339())
|
||||
.with_thread_names(true)
|
||||
.try_init();
|
||||
});
|
||||
}
|
||||
|
||||
/// Test helper: Create test environment with ECStore
|
||||
async fn setup_test_env() -> (Vec<PathBuf>, Arc<ECStore>, Arc<ECStoreHealStorage>) {
|
||||
init_tracing();
|
||||
|
||||
// Fast path: already initialized, just clone and return
|
||||
if let Some((paths, ecstore, heal_storage)) = GLOBAL_ENV.get() {
|
||||
return (paths.clone(), ecstore.clone(), heal_storage.clone());
|
||||
}
|
||||
|
||||
// create temp dir as 4 disks with unique base dir
|
||||
let test_base_dir = format!("/tmp/rustfs_heal_heal_test_{}", uuid::Uuid::new_v4());
|
||||
let temp_dir = std::path::PathBuf::from(&test_base_dir);
|
||||
if temp_dir.exists() {
|
||||
fs::remove_dir_all(&temp_dir).await.ok();
|
||||
}
|
||||
fs::create_dir_all(&temp_dir).await.unwrap();
|
||||
|
||||
// create 4 disk dirs
|
||||
let disk_paths = vec![
|
||||
temp_dir.join("disk1"),
|
||||
temp_dir.join("disk2"),
|
||||
temp_dir.join("disk3"),
|
||||
temp_dir.join("disk4"),
|
||||
];
|
||||
|
||||
for disk_path in &disk_paths {
|
||||
fs::create_dir_all(disk_path).await.unwrap();
|
||||
}
|
||||
|
||||
// create EndpointServerPools
|
||||
let mut endpoints = Vec::new();
|
||||
for (i, disk_path) in disk_paths.iter().enumerate() {
|
||||
let mut endpoint = Endpoint::try_from(disk_path.to_str().unwrap()).unwrap();
|
||||
// set correct index
|
||||
endpoint.set_pool_index(0);
|
||||
endpoint.set_set_index(0);
|
||||
endpoint.set_disk_index(i);
|
||||
endpoints.push(endpoint);
|
||||
}
|
||||
|
||||
let pool_endpoints = PoolEndpoints {
|
||||
legacy: false,
|
||||
set_count: 1,
|
||||
drives_per_set: 4,
|
||||
endpoints: Endpoints::from(endpoints),
|
||||
cmd_line: "test".to_string(),
|
||||
platform: format!("OS: {} | Arch: {}", std::env::consts::OS, std::env::consts::ARCH),
|
||||
};
|
||||
|
||||
let endpoint_pools = EndpointServerPools(vec![pool_endpoints]);
|
||||
|
||||
// format disks (only first time)
|
||||
rustfs_ecstore::store::init_local_disks(endpoint_pools.clone()).await.unwrap();
|
||||
|
||||
// create ECStore with dynamic port 0 (let OS assign) or fixed 9001 if free
|
||||
let port = 9001; // for simplicity
|
||||
let server_addr: std::net::SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
|
||||
let ecstore = ECStore::new(server_addr, endpoint_pools, CancellationToken::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// init bucket metadata system
|
||||
let buckets_list = ecstore
|
||||
.list_bucket(&rustfs_ecstore::store_api::BucketOptions {
|
||||
no_metadata: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buckets = buckets_list.into_iter().map(|v| v.name).collect();
|
||||
rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys(ecstore.clone(), buckets).await;
|
||||
|
||||
// Create heal storage layer
|
||||
let heal_storage = Arc::new(ECStoreHealStorage::new(ecstore.clone()));
|
||||
|
||||
// Store in global once lock
|
||||
let _ = GLOBAL_ENV.set((disk_paths.clone(), ecstore.clone(), heal_storage.clone()));
|
||||
|
||||
(disk_paths, ecstore, heal_storage)
|
||||
}
|
||||
|
||||
/// Test helper: Create a test bucket
|
||||
async fn create_test_bucket(ecstore: &Arc<ECStore>, bucket_name: &str) {
|
||||
(**ecstore)
|
||||
.make_bucket(bucket_name, &Default::default())
|
||||
.await
|
||||
.expect("Failed to create test bucket");
|
||||
info!("Created test bucket: {}", bucket_name);
|
||||
}
|
||||
|
||||
/// Test helper: Upload test object
|
||||
async fn upload_test_object(ecstore: &Arc<ECStore>, bucket: &str, object: &str, data: &[u8]) {
|
||||
let mut reader = PutObjReader::from_vec(data.to_vec());
|
||||
let object_info = (**ecstore)
|
||||
.put_object(bucket, object, &mut reader, &ObjectOptions::default())
|
||||
.await
|
||||
.expect("Failed to upload test object");
|
||||
|
||||
info!("Uploaded test object: {}/{} ({} bytes)", bucket, object, object_info.size);
|
||||
}
|
||||
|
||||
mod serial_tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[serial]
|
||||
async fn test_heal_object_basic() {
|
||||
let (disk_paths, ecstore, heal_storage) = setup_test_env().await;
|
||||
|
||||
// Create test bucket and object
|
||||
let bucket_name = "test-heal-object-basic";
|
||||
let object_name = "test-object.txt";
|
||||
let test_data = b"Hello, this is test data for healing!";
|
||||
|
||||
create_test_bucket(&ecstore, bucket_name).await;
|
||||
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
|
||||
|
||||
// ─── 1️⃣ delete single data shard file ─────────────────────────────────────
|
||||
let obj_dir = disk_paths[0].join(bucket_name).join(object_name);
|
||||
// find part file at depth 2, e.g. .../<uuid>/part.1
|
||||
let target_part = WalkDir::new(&obj_dir)
|
||||
.min_depth(2)
|
||||
.max_depth(2)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.find(|e| e.file_type().is_file() && e.file_name().to_str().map(|n| n.starts_with("part.")).unwrap_or(false))
|
||||
.map(|e| e.into_path())
|
||||
.expect("Failed to locate part file to delete");
|
||||
|
||||
std::fs::remove_file(&target_part).expect("failed to delete part file");
|
||||
assert!(!target_part.exists());
|
||||
println!("✅ Deleted shard part file: {target_part:?}");
|
||||
|
||||
// Create heal manager with faster interval
|
||||
let cfg = HealConfig {
|
||||
heal_interval: Duration::from_millis(1),
|
||||
..Default::default()
|
||||
};
|
||||
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
|
||||
heal_manager.start().await.unwrap();
|
||||
|
||||
// Submit heal request for the object
|
||||
let heal_request = HealRequest::new(
|
||||
HealType::Object {
|
||||
bucket: bucket_name.to_string(),
|
||||
object: object_name.to_string(),
|
||||
version_id: None,
|
||||
},
|
||||
HealOptions {
|
||||
dry_run: false,
|
||||
recursive: false,
|
||||
remove_corrupted: false,
|
||||
recreate_missing: true,
|
||||
scan_mode: HealScanMode::Normal,
|
||||
update_parity: true,
|
||||
timeout: Some(Duration::from_secs(300)),
|
||||
pool_index: None,
|
||||
set_index: None,
|
||||
},
|
||||
HealPriority::Normal,
|
||||
);
|
||||
|
||||
let task_id = heal_manager
|
||||
.submit_heal_request(heal_request)
|
||||
.await
|
||||
.expect("Failed to submit heal request");
|
||||
|
||||
info!("Submitted heal request with task ID: {}", task_id);
|
||||
|
||||
// Wait for task completion
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(8)).await;
|
||||
|
||||
// Attempt to fetch task status (might be removed if finished)
|
||||
match heal_manager.get_task_status(&task_id).await {
|
||||
Ok(status) => info!("Task status: {:?}", status),
|
||||
Err(e) => info!("Task status not found (likely completed): {}", e),
|
||||
}
|
||||
|
||||
// ─── 2️⃣ verify each part file is restored ───────
|
||||
assert!(target_part.exists());
|
||||
|
||||
info!("Heal object basic test passed");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[serial]
|
||||
async fn test_heal_bucket_basic() {
|
||||
let (disk_paths, ecstore, heal_storage) = setup_test_env().await;
|
||||
|
||||
// Create test bucket
|
||||
let bucket_name = "test-heal-bucket-basic";
|
||||
create_test_bucket(&ecstore, bucket_name).await;
|
||||
|
||||
// ─── 1️⃣ delete bucket dir on disk ──────────────
|
||||
let broken_bucket_path = disk_paths[0].join(bucket_name);
|
||||
assert!(broken_bucket_path.exists(), "bucket dir does not exist on disk");
|
||||
std::fs::remove_dir_all(&broken_bucket_path).expect("failed to delete bucket dir on disk");
|
||||
assert!(!broken_bucket_path.exists(), "bucket dir still exists after deletion");
|
||||
println!("✅ Deleted bucket directory on disk: {broken_bucket_path:?}");
|
||||
|
||||
// Create heal manager with faster interval
|
||||
let cfg = HealConfig {
|
||||
heal_interval: Duration::from_millis(1),
|
||||
..Default::default()
|
||||
};
|
||||
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
|
||||
heal_manager.start().await.unwrap();
|
||||
|
||||
// Submit heal request for the bucket
|
||||
let heal_request = HealRequest::new(
|
||||
HealType::Bucket {
|
||||
bucket: bucket_name.to_string(),
|
||||
},
|
||||
HealOptions {
|
||||
dry_run: false,
|
||||
recursive: true,
|
||||
remove_corrupted: false,
|
||||
recreate_missing: false,
|
||||
scan_mode: HealScanMode::Normal,
|
||||
update_parity: false,
|
||||
timeout: Some(Duration::from_secs(300)),
|
||||
pool_index: None,
|
||||
set_index: None,
|
||||
},
|
||||
HealPriority::Normal,
|
||||
);
|
||||
|
||||
let task_id = heal_manager
|
||||
.submit_heal_request(heal_request)
|
||||
.await
|
||||
.expect("Failed to submit bucket heal request");
|
||||
|
||||
info!("Submitted bucket heal request with task ID: {}", task_id);
|
||||
|
||||
// Wait for task completion
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
|
||||
// Attempt to fetch task status (optional)
|
||||
if let Ok(status) = heal_manager.get_task_status(&task_id).await {
|
||||
if status == HealTaskStatus::Completed {
|
||||
info!("Bucket heal task status: {:?}", status);
|
||||
} else {
|
||||
panic!("Bucket heal task status: {status:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 3️⃣ Verify bucket directory is restored on every disk ───────
|
||||
assert!(broken_bucket_path.exists(), "bucket dir does not exist on disk");
|
||||
|
||||
info!("Heal bucket basic test passed");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[serial]
|
||||
async fn test_heal_format_basic() {
|
||||
let (disk_paths, _ecstore, heal_storage) = setup_test_env().await;
|
||||
|
||||
// ─── 1️⃣ delete format.json on one disk ──────────────
|
||||
let format_path = disk_paths[0].join(".rustfs.sys").join("format.json");
|
||||
assert!(format_path.exists(), "format.json does not exist on disk");
|
||||
std::fs::remove_file(&format_path).expect("failed to delete format.json on disk");
|
||||
assert!(!format_path.exists(), "format.json still exists after deletion");
|
||||
println!("✅ Deleted format.json on disk: {format_path:?}");
|
||||
|
||||
// Create heal manager with faster interval
|
||||
let cfg = HealConfig {
|
||||
heal_interval: Duration::from_secs(2),
|
||||
..Default::default()
|
||||
};
|
||||
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
|
||||
heal_manager.start().await.unwrap();
|
||||
|
||||
// Wait for task completion
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
|
||||
// ─── 2️⃣ verify format.json is restored ───────
|
||||
assert!(format_path.exists(), "format.json does not exist on disk after heal");
|
||||
|
||||
info!("Heal format basic test passed");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[serial]
|
||||
async fn test_heal_format_with_data() {
|
||||
let (disk_paths, ecstore, heal_storage) = setup_test_env().await;
|
||||
|
||||
// Create test bucket and object
|
||||
let bucket_name = "test-heal-format-with-data";
|
||||
let object_name = "test-object.txt";
|
||||
let test_data = b"Hello, this is test data for healing!";
|
||||
|
||||
create_test_bucket(&ecstore, bucket_name).await;
|
||||
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
|
||||
|
||||
let obj_dir = disk_paths[0].join(bucket_name).join(object_name);
|
||||
let target_part = WalkDir::new(&obj_dir)
|
||||
.min_depth(2)
|
||||
.max_depth(2)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.find(|e| e.file_type().is_file() && e.file_name().to_str().map(|n| n.starts_with("part.")).unwrap_or(false))
|
||||
.map(|e| e.into_path())
|
||||
.expect("Failed to locate part file to delete");
|
||||
|
||||
// ─── 1️⃣ delete format.json on one disk ──────────────
|
||||
let format_path = disk_paths[0].join(".rustfs.sys").join("format.json");
|
||||
std::fs::remove_dir_all(&disk_paths[0]).expect("failed to delete all contents under disk_paths[0]");
|
||||
std::fs::create_dir_all(&disk_paths[0]).expect("failed to recreate disk_paths[0] directory");
|
||||
println!("✅ Deleted format.json on disk: {:?}", disk_paths[0]);
|
||||
|
||||
// Create heal manager with faster interval
|
||||
let cfg = HealConfig {
|
||||
heal_interval: Duration::from_secs(1),
|
||||
..Default::default()
|
||||
};
|
||||
let heal_manager = HealManager::new(heal_storage.clone(), Some(cfg));
|
||||
heal_manager.start().await.unwrap();
|
||||
|
||||
// Wait for task completion
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
|
||||
// ─── 2️⃣ verify format.json is restored ───────
|
||||
assert!(format_path.exists(), "format.json does not exist on disk after heal");
|
||||
// ─── 3 verify each part file is restored ───────
|
||||
assert!(target_part.exists());
|
||||
|
||||
info!("Heal format basic test passed");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
#[serial]
|
||||
async fn test_heal_storage_api_direct() {
|
||||
let (_disk_paths, ecstore, heal_storage) = setup_test_env().await;
|
||||
|
||||
// Test direct heal storage API calls
|
||||
|
||||
// Test heal_format
|
||||
let format_result = heal_storage.heal_format(true).await; // dry run
|
||||
assert!(format_result.is_ok());
|
||||
info!("Direct heal_format test passed");
|
||||
|
||||
// Test heal_bucket
|
||||
let bucket_name = "test-bucket-direct";
|
||||
create_test_bucket(&ecstore, bucket_name).await;
|
||||
|
||||
let heal_opts = HealOpts {
|
||||
recursive: true,
|
||||
dry_run: true,
|
||||
remove: false,
|
||||
recreate: false,
|
||||
scan_mode: HealScanMode::Normal,
|
||||
update_parity: false,
|
||||
no_lock: false,
|
||||
pool: None,
|
||||
set: None,
|
||||
};
|
||||
|
||||
let bucket_result = heal_storage.heal_bucket(bucket_name, &heal_opts).await;
|
||||
assert!(bucket_result.is_ok());
|
||||
info!("Direct heal_bucket test passed");
|
||||
|
||||
// Test heal_object
|
||||
let object_name = "test-object-direct.txt";
|
||||
let test_data = b"Test data for direct heal API";
|
||||
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
|
||||
|
||||
let object_heal_opts = HealOpts {
|
||||
recursive: false,
|
||||
dry_run: true,
|
||||
remove: false,
|
||||
recreate: false,
|
||||
scan_mode: HealScanMode::Normal,
|
||||
update_parity: false,
|
||||
no_lock: false,
|
||||
pool: None,
|
||||
set: None,
|
||||
};
|
||||
|
||||
let object_result = heal_storage
|
||||
.heal_object(bucket_name, object_name, None, &object_heal_opts)
|
||||
.await;
|
||||
assert!(object_result.is_ok());
|
||||
info!("Direct heal_object test passed");
|
||||
|
||||
info!("Direct heal storage API test passed");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user