From 3035cad082fca234a720029061de85fb0c52649e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Sat, 20 Jun 2026 12:03:22 +0800 Subject: [PATCH] refactor: move ecstore set heal helpers (#3648) --- crates/ecstore/src/layout/mod.rs | 1 + crates/ecstore/src/layout/set_heal.rs | 139 ++++++++++++++++++++++++ crates/ecstore/src/sets.rs | 71 +----------- docs/architecture/migration-progress.md | 48 ++++++-- 4 files changed, 179 insertions(+), 80 deletions(-) create mode 100644 crates/ecstore/src/layout/set_heal.rs diff --git a/crates/ecstore/src/layout/mod.rs b/crates/ecstore/src/layout/mod.rs index 959bf2ae6..62c58f125 100644 --- a/crates/ecstore/src/layout/mod.rs +++ b/crates/ecstore/src/layout/mod.rs @@ -8,4 +8,5 @@ pub(crate) mod disks_layout; pub(crate) mod endpoint; pub(crate) mod endpoints; pub(crate) mod format; +pub(crate) mod set_heal; pub(crate) mod set_layout; diff --git a/crates/ecstore/src/layout/set_heal.rs b/crates/ecstore/src/layout/set_heal.rs new file mode 100644 index 000000000..702016f24 --- /dev/null +++ b/crates/ecstore/src/layout/set_heal.rs @@ -0,0 +1,139 @@ +// 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 crate::disk::{DiskInfo, error::DiskError}; +use crate::layout::{endpoints::Endpoints, format::FormatV3}; +use rustfs_common::heal_channel::DriveState; +use rustfs_madmin::heal_commands::HealDriveInfo; + +pub(crate) fn formats_to_drives_info( + endpoints: &Endpoints, + formats: &[Option], + errs: &[Option], +) -> Vec { + let mut before_drives = Vec::with_capacity(endpoints.as_ref().len()); + for (index, format) in formats.iter().enumerate() { + let drive = endpoints.get_string(index); + let state = if format.is_some() { + DriveState::Ok.to_string() + } else if let Some(Some(err)) = errs.get(index) { + if *err == DiskError::UnformattedDisk { + DriveState::Missing.to_string() + } else if *err == DiskError::DiskNotFound { + DriveState::Offline.to_string() + } else { + DriveState::Corrupt.to_string() + } + } else { + DriveState::Corrupt.to_string() + }; + + let uuid = if let Some(format) = format { + format.erasure.this.to_string() + } else { + "".to_string() + }; + before_drives.push(HealDriveInfo { + uuid, + endpoint: drive, + state: state.to_string(), + }); + } + before_drives +} + +pub(crate) fn new_heal_format_sets( + ref_format: &FormatV3, + set_count: usize, + set_drive_count: usize, + formats: &[Option], + errs: &[Option], +) -> (Vec>>, Vec>) { + let mut new_formats = vec![vec![None; set_drive_count]; set_count]; + let mut current_disks_info = vec![vec![DiskInfo::default(); set_drive_count]; set_count]; + for (i, set) in ref_format.erasure.sets.iter().enumerate() { + for j in 0..set.len() { + if let Some(Some(err)) = errs.get(i * set_drive_count + j) + && *err == DiskError::UnformattedDisk + { + let mut fm = FormatV3::new(set_count, set_drive_count); + fm.id = ref_format.id; + fm.format = ref_format.format.clone(); + fm.version = ref_format.version.clone(); + fm.erasure.this = ref_format.erasure.sets[i][j]; + fm.erasure.sets = ref_format.erasure.sets.clone(); + fm.erasure.version = ref_format.erasure.version.clone(); + fm.erasure.distribution_algo = ref_format.erasure.distribution_algo.clone(); + new_formats[i][j] = Some(fm); + } + if let (Some(format), None) = (&formats[i * set_drive_count + j], &errs[i * set_drive_count + j]) + && let Some(info) = &format.disk_info + && !info.endpoint.is_empty() + { + current_disks_info[i][j] = info.clone(); + } + } + } + + (new_formats, current_disks_info) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::layout::endpoint::Endpoint; + + #[test] + fn formats_to_drives_info_maps_missing_offline_and_ok_states() { + let endpoints = Endpoints::from(vec![ + Endpoint::try_from("/tmp/rustfs-set-heal-a").unwrap(), + Endpoint::try_from("/tmp/rustfs-set-heal-b").unwrap(), + Endpoint::try_from("/tmp/rustfs-set-heal-c").unwrap(), + ]); + let format = FormatV3::new(1, 1); + let uuid = format.erasure.this.to_string(); + let drives = formats_to_drives_info( + &endpoints, + &[Some(format), None, None], + &[None, Some(DiskError::UnformattedDisk), Some(DiskError::DiskNotFound)], + ); + + assert_eq!(drives.len(), 3); + assert_eq!(drives[0].uuid, uuid); + assert_eq!(drives[0].state, DriveState::Ok.to_string()); + assert_eq!(drives[1].state, DriveState::Missing.to_string()); + assert_eq!(drives[2].state, DriveState::Offline.to_string()); + } + + #[test] + fn new_heal_format_sets_only_recreates_unformatted_slots() { + let ref_format = FormatV3::new(1, 2); + let existing_format = ref_format.clone(); + let (new_formats, current_disks_info) = new_heal_format_sets( + &ref_format, + 1, + 2, + &[Some(existing_format), None], + &[None, Some(DiskError::UnformattedDisk)], + ); + + assert!(new_formats[0][0].is_none()); + let repaired = new_formats[0][1].as_ref().unwrap(); + assert_eq!(repaired.id, ref_format.id); + assert_eq!(repaired.erasure.this, ref_format.erasure.sets[0][1]); + assert_eq!(repaired.erasure.sets, ref_format.erasure.sets); + assert_eq!(current_disks_info.len(), 1); + assert_eq!(current_disks_info[0].len(), 2); + } +} diff --git a/crates/ecstore/src/sets.rs b/crates/ecstore/src/sets.rs index cfc563d1a..12b20e78b 100644 --- a/crates/ecstore/src/sets.rs +++ b/crates/ecstore/src/sets.rs @@ -15,9 +15,10 @@ use crate::disk::error_reduce::count_errs; use crate::error::{Error, Result}; +use crate::layout::set_heal::{formats_to_drives_info, new_heal_format_sets}; use crate::{ disk::{ - DiskAPI, DiskInfo, DiskOption, DiskStore, + DiskAPI, DiskOption, DiskStore, error::DiskError, format::{DistributionAlgoVersion, FormatV3}, new_disk, @@ -1017,74 +1018,6 @@ async fn init_storage_disks_with_errors( (disks, errs) } -fn formats_to_drives_info(endpoints: &Endpoints, formats: &[Option], errs: &[Option]) -> Vec { - let mut before_drives = Vec::with_capacity(endpoints.as_ref().len()); - for (index, format) in formats.iter().enumerate() { - let drive = endpoints.get_string(index); - let state = if format.is_some() { - DriveState::Ok.to_string() - } else if let Some(Some(err)) = errs.get(index) { - if *err == DiskError::UnformattedDisk { - DriveState::Missing.to_string() - } else if *err == DiskError::DiskNotFound { - DriveState::Offline.to_string() - } else { - DriveState::Corrupt.to_string() - } - } else { - DriveState::Corrupt.to_string() - }; - - let uuid = if let Some(format) = format { - format.erasure.this.to_string() - } else { - "".to_string() - }; - before_drives.push(HealDriveInfo { - uuid, - endpoint: drive, - state: state.to_string(), - }); - } - before_drives -} - -fn new_heal_format_sets( - ref_format: &FormatV3, - set_count: usize, - set_drive_count: usize, - formats: &[Option], - errs: &[Option], -) -> (Vec>>, Vec>) { - let mut new_formats = vec![vec![None; set_drive_count]; set_count]; - let mut current_disks_info = vec![vec![DiskInfo::default(); set_drive_count]; set_count]; - for (i, set) in ref_format.erasure.sets.iter().enumerate() { - for j in 0..set.len() { - if let Some(Some(err)) = errs.get(i * set_drive_count + j) - && *err == DiskError::UnformattedDisk - { - let mut fm = FormatV3::new(set_count, set_drive_count); - fm.id = ref_format.id; - fm.format = ref_format.format.clone(); - fm.version = ref_format.version.clone(); - fm.erasure.this = ref_format.erasure.sets[i][j]; - fm.erasure.sets = ref_format.erasure.sets.clone(); - fm.erasure.version = ref_format.erasure.version.clone(); - fm.erasure.distribution_algo = ref_format.erasure.distribution_algo.clone(); - new_formats[i][j] = Some(fm); - } - if let (Some(format), None) = (&formats[i * set_drive_count + j], &errs[i * set_drive_count + j]) - && let Some(info) = &format.disk_info - && !info.endpoint.is_empty() - { - current_disks_info[i][j] = info.clone(); - } - } - } - - (new_formats, current_disks_info) -} - #[cfg(test)] mod tests { use super::*; diff --git a/docs/architecture/migration-progress.md b/docs/architecture/migration-progress.md index 5f4c47bd6..232c8b74f 100644 --- a/docs/architecture/migration-progress.md +++ b/docs/architecture/migration-progress.md @@ -5,16 +5,16 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block ## Current Context - Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660) -- Branch: `overtrue/arch-ecstore-layout-endpoints-move` -- Baseline: merged `E-002/E-LAYOUT-001`. -- Stacked on: merged ECStore layout foundation and format layout ownership - slices. +- Branch: `overtrue/arch-ecstore-layout-set-heal-helpers` +- Baseline: merged `E-003/E-LAYOUT-002`. +- Stacked on: merged ECStore layout foundation, format layout ownership, and + endpoint layout move slices. - PR type for this branch: `pure-move` - Runtime behavior changes: none. -- Rust code changes: pure-move ECStore endpoint parsing and endpoint grouping - modules into the internal layout bucket while preserving old public paths. +- Rust code changes: move runtime-neutral ECStore set-format heal helpers into + the internal layout bucket while preserving `Sets` runtime orchestration. - CI/script changes: none. -- Docs changes: record the ECStore endpoint pure move slice. +- Docs changes: record the ECStore set-format heal helper layout slice. ## Phase 0 Tasks @@ -2253,10 +2253,24 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block checks, migration/layer guards, formatting, diff hygiene, Rust risk scan, branch freshness check, pre-commit quality gate, and three-expert review. +- [x] `E-004/E-LAYOUT-003` Move ECStore set-format heal helpers. + - Do: move runtime-neutral set-format heal helper logic into the ECStore + layout bucket while keeping disk initialization and `Sets` orchestration in + `sets.rs`. + - Acceptance: `layout::set_heal` owns drive-info mapping and unformatted + format regeneration helpers, `Sets` keeps the same heal orchestration, and + focused tests cover the extracted helper behavior. + - Must preserve: disk format heal state mapping, unformatted disk format + regeneration, current disk-info preservation, dry-run behavior, save-format + behavior, and all `Sets` runtime control flow. + - Verification: focused ECStore set-heal tests, ECStore/RustFS/Heal compile + checks, migration/layer guards, formatting, diff hygiene, Rust risk scan, + branch freshness check, pre-commit quality gate, and three-expert review. + ## Next PRs 1. `pure-move`: continue moving runtime-neutral pool/set layout helpers once - E-003/E-LAYOUT-002 lands. + E-004/E-LAYOUT-003 lands. 2. `pure-move`: continue pruning residual embedded startup-only orchestration once the lifecycle helpers are merged. @@ -2264,9 +2278,9 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block | Expert | Status | Notes | |---|---|---| -| Quality/architecture | passed | E-003/E-LAYOUT-002 is a pure move into ECStore layout with compatibility stubs at old public paths; no new runtime owner or dependency boundary is introduced. | -| Migration preservation | passed | Endpoint parsing, local-host detection, pool/set/disk indexes, endpoint grouping, disk independence checks, setup type classification, and old public module paths remain preserved. | -| Testing/verification | passed | Focused endpoint/layout checks, compile checks, guards, formatting, diff hygiene, Rust risk scan, and full pre-commit passed. | +| Quality/architecture | passed | E-004/E-LAYOUT-003 extracts runtime-neutral set-format heal helpers into ECStore layout without moving disk init or `Sets` orchestration. | +| Migration preservation | passed | Drive-info state mapping, unformatted format regeneration, current disk-info preservation, dry-run/save behavior, and `Sets` runtime control flow remain preserved. | +| Testing/verification | passed | Focused set-heal helper tests, compile checks, guards, formatting, diff hygiene, Rust risk scan, and full pre-commit passed. | ## Verification Notes @@ -2354,6 +2368,18 @@ Passed before push: - `make pre-commit`: passed. - Three-expert review: passed. +- Issue #660 E-004/E-LAYOUT-003 current slice: + - `cargo test -p rustfs-ecstore layout::set_heal -- --nocapture`: passed. + - `cargo check -p rustfs-ecstore -p rustfs -p rustfs-heal`: passed. + - `./scripts/check_architecture_migration_rules.sh`: passed. + - `./scripts/check_layer_dependencies.sh`: passed. + - `cargo fmt --all --check`: passed. + - `git diff --check`: passed. + - Rust risk scan on changed Rust files: passed; only test-only unwrap + expectations were added around deterministic helper construction. + - `make pre-commit`: passed. + - Three-expert review: passed. + - Issue #660 X-012 current slice: - `cargo test -p rustfs-extension-schema`: passed. - `cargo check -p rustfs-extension-schema`: passed.