// 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 super::*; use crate::core::pools::local_decommission_queue_prefix; use crate::error::is_err_decommission_running; use crate::runtime::sources as runtime_sources; use crate::storage_api_contracts::object::EcstoreObjectIO; use tracing::{debug, error, info, warn}; const LOG_COMPONENT_ECSTORE: &str = "ecstore"; const LOG_SUBSYSTEM_STORE_INIT: &str = "store_init"; const EVENT_DECOMMISSION_RESUME_RETRY: &str = "decommission_resume_retry"; const EVENT_DECOMMISSION_RESUME_FAILED: &str = "decommission_resume_failed"; const EVENT_STORE_FORMAT_RETRY: &str = "store_format_retry"; const EVENT_ECSTORE_INIT_STATUS: &str = "ecstore_init_status"; fn pool_first_endpoint_is_local(pool: &crate::layout::endpoints::PoolEndpoints) -> bool { pool.endpoints.as_ref().first().is_some_and(|endpoint| endpoint.is_local) } fn should_resume_local_decommission(endpoints: &EndpointServerPools, idx: usize) -> Result { let pool = endpoints.as_ref().get(idx).ok_or_else(|| { Error::other(format!( "store init failed to resolve decommission resume pool index {idx} from current endpoints" )) })?; let endpoint = pool.endpoints.as_ref().first().ok_or_else(|| { Error::other(format!( "store init failed to resolve decommission resume pool index {idx}: no endpoints available" )) })?; Ok(endpoint.is_local) } const LOCAL_DECOMMISSION_RESUME_MAX_CONFIG_RETRIES: usize = 6; const LOCAL_DECOMMISSION_INITIAL_RESUME_DELAY: Duration = Duration::from_secs(60 * 3); const LOCAL_DECOMMISSION_RESUME_RETRY_DELAY: Duration = Duration::from_secs(30); fn should_retry_local_decommission_resume(err: &Error, attempt: usize) -> bool { matches!(err, Error::ConfigNotFound) && attempt < LOCAL_DECOMMISSION_RESUME_MAX_CONFIG_RETRIES } fn should_auto_start_rebalance_after_init(decommission_running: bool, rebalance_meta_loaded: bool) -> bool { rebalance_meta_loaded && !decommission_running } fn pool_meta_has_active_decommission(meta: &PoolMeta) -> bool { meta.pools.iter().any(|pool| { pool.decommission .as_ref() .is_some_and(|info| info.has_decommission_state() && !info.complete && !info.failed && !info.canceled) }) } fn should_auto_start_rebalance_after_recovered_meta(pool_meta: &PoolMeta, rebalance_meta_loaded: bool) -> bool { should_auto_start_rebalance_after_init(pool_meta_has_active_decommission(pool_meta), rebalance_meta_loaded) } async fn wait_for_local_decommission_resume_delay(rx: &CancellationToken, delay: Duration) -> bool { tokio::select! { _ = rx.cancelled() => false, _ = tokio::time::sleep(delay) => true, } } fn resolve_store_init_stage_result(result: Result<()>, stage: &str) -> Result<()> { result.map_err(|err| Error::other(format!("store init failed during {stage}: {err}"))) } async fn load_pool_meta_for_startup(pool: Arc) -> Result where S: EcstoreObjectIO, { let mut meta = PoolMeta::default(); resolve_store_init_stage_result(meta.load_for_startup(pool).await, "load_pool_meta")?; Ok(meta) } async fn save_validated_pool_meta_for_startup(meta: &PoolMeta, pools: Vec>) -> Result<()> where S: EcstoreObjectIO, { resolve_store_init_stage_result(meta.save_for_startup(pools).await, "save_validated_pool_meta") } async fn resume_local_decommission_after_init(store: Arc, rx: CancellationToken, pool_indices: Vec) { for attempt in 0..=LOCAL_DECOMMISSION_RESUME_MAX_CONFIG_RETRIES { if rx.is_cancelled() { return; } let result = if pool_indices.len() > 1 { store .spawn_decommission_routines(store.clone(), rx.clone(), pool_indices.clone()) .await } else { store.decommission(rx.clone(), pool_indices.clone()).await }; match result { Ok(()) => return, Err(err) if is_err_decommission_running(&err) => { if let Err(spawn_err) = store .spawn_decommission_routines(store.clone(), rx.clone(), pool_indices.clone()) .await { error!( event = EVENT_DECOMMISSION_RESUME_FAILED, component = LOG_COMPONENT_ECSTORE, subsystem = LOG_SUBSYSTEM_STORE_INIT, pool_indices = ?pool_indices, error = %spawn_err, reason = "spawn_workers_failed", "Failed to resume decommission workers" ); } return; } Err(err) if should_retry_local_decommission_resume(&err, attempt) => { warn!( event = EVENT_DECOMMISSION_RESUME_RETRY, component = LOG_COMPONENT_ECSTORE, subsystem = LOG_SUBSYSTEM_STORE_INIT, pool_indices = ?pool_indices, retry_count = attempt + 1, retry_limit = LOCAL_DECOMMISSION_RESUME_MAX_CONFIG_RETRIES + 1, error = %err, "Retrying decommission resume after missing config" ); tokio::select! { _ = rx.cancelled() => return, _ = tokio::time::sleep(LOCAL_DECOMMISSION_RESUME_RETRY_DELAY) => {} } } Err(err) => { error!( event = EVENT_DECOMMISSION_RESUME_FAILED, component = LOG_COMPONENT_ECSTORE, subsystem = LOG_SUBSYSTEM_STORE_INIT, pool_indices = ?pool_indices, error = %err, reason = "resume_failed", "Failed to resume decommission" ); return; } } } } impl ECStore { #[allow(clippy::new_ret_no_self)] #[instrument(level = "debug", skip(endpoint_pools))] pub async fn new(address: SocketAddr, endpoint_pools: EndpointServerPools, ctx: CancellationToken) -> Result> { // let layouts = DisksLayout::from_volumes(endpoints.as_slice())?; let mut deployment_id = None; // let (endpoint_pools, _) = EndpointServerPools::create_server_endpoints(address.as_str(), &layouts)?; let mut pools = Vec::with_capacity(endpoint_pools.as_ref().len()); let mut disk_map = HashMap::with_capacity(endpoint_pools.as_ref().len()); let mut local_disks = Vec::new(); debug!( event = EVENT_ECSTORE_INIT_STATUS, component = LOG_COMPONENT_ECSTORE, subsystem = LOG_SUBSYSTEM_STORE_INIT, address = %address, "Initializing ECStore address" ); let mut host = address.ip().to_string(); if host.is_empty() { host = runtime_sources::rustfs_host().await } let mut port = address.port().to_string(); if port.is_empty() { port = runtime_sources::rustfs_port().to_string() } debug!( event = EVENT_ECSTORE_INIT_STATUS, component = LOG_COMPONENT_ECSTORE, subsystem = LOG_SUBSYSTEM_STORE_INIT, host = %host, port = %port, "Initializing ECStore host" ); init_local_peer(&endpoint_pools, &host, &port).await; // debug!("endpoint_pools: {:?}", endpoint_pools); let mut common_parity_drives = 0; for (i, pool_eps) in endpoint_pools.as_ref().iter().enumerate() { let pool_first_is_local = pool_first_endpoint_is_local(pool_eps); if common_parity_drives == 0 { let parity_drives = ec_drives_no_config(pool_eps.drives_per_set)?; storageclass::validate_parity(parity_drives, pool_eps.drives_per_set)?; common_parity_drives = parity_drives; } // validate_parity(parity_count, pool_eps.drives_per_set)?; // Build disks with health monitoring available, but do not start // periodic monitoring until format loading succeeds. Startup RPC // failures can still spawn recovery probes for peers that come up // after this node. let (disks, errs) = init_format::init_disks( &pool_eps.endpoints, &DiskOption { cleanup: true, health_check: true, }, ) .await; check_disk_fatal_errs(&errs)?; let fm = { let mut times = 0; let mut interval = 1; loop { match init_format::connect_load_init_formats( pool_first_is_local, &disks, pool_eps.set_count, pool_eps.drives_per_set, deployment_id, ) .await { Ok(fm) => break Ok(fm), // Wrap the final error if we are giving up Err(e) if times >= 10 => { break Err(Error::other(format!("store init failed to load formats after {times} retries: {e}"))); } // Retrying so just drop the error Err(_) => {} } times += 1; if interval < 16 { interval *= 2; } debug!( event = EVENT_STORE_FORMAT_RETRY, component = LOG_COMPONENT_ECSTORE, subsystem = LOG_SUBSYSTEM_STORE_INIT, retry_count = times, retry_delay_secs = interval, "Retrying storage format load" ); select! { _ = tokio::signal::ctrl_c() => { info!( event = EVENT_STORE_FORMAT_RETRY, component = LOG_COMPONENT_ECSTORE, subsystem = LOG_SUBSYSTEM_STORE_INIT, reason = "ctrl_c", "Interrupted storage format retry loop" ); exit(0); } _ = sleep(Duration::from_secs(interval)) => { } } // After waiting for peers, clear transient faulty marks so the next attempt can open RPCs again // (these `DiskStore` handles are reused; `is_faulty()` would otherwise short-circuit). for disk in disks.iter().flatten() { disk.reset_health_for_store_init_retry(); } } }?; // Format loading succeeded, enable health monitoring on all disks for disk in disks.iter().flatten() { disk.enable_health_check(); } if deployment_id.is_none() { deployment_id = Some(fm.id); } if deployment_id != Some(fm.id) { return Err(Error::other("store init failed: deployment IDs do not match across pools")); } if deployment_id.is_some_and(|id| id.is_nil()) { deployment_id = Some(Uuid::new_v4()); } for disk in disks.iter() { if disk.is_some() && disk.as_ref().expect("operation should succeed").is_local() { local_disks.push(disk.as_ref().expect("operation should succeed").clone()); } } let sets = Sets::new(disks.clone(), pool_eps, &fm, i, common_parity_drives).await?; pools.push(sets); disk_map.insert(i, disks); } // Replace the local disk if !runtime_sources::setup_is_dist_erasure().await { runtime_sources::record_local_disks(local_disks).await; } let peer_sys = S3PeerSys::new(&endpoint_pools); let mut pool_meta = PoolMeta::new(&pools, &PoolMeta::default()); pool_meta.dont_save = true; let decommission_cancelers = RwLock::new(vec![None; pools.len()]); let ec = Arc::new(ECStore { id: deployment_id.ok_or_else(|| Error::other("store init failed: deployment id is not initialized"))?, disk_map, pools, peer_sys, pool_meta: RwLock::new(pool_meta), rebalance_meta: RwLock::new(None), decommission_cancelers, start_gate: Mutex::new(()), pool_meta_save_gate: Mutex::new(()), // Adopt the process bootstrap context so startup writes (erasure // type recorded before this point) and later reads share one cell. ctx: crate::runtime::instance::bootstrap_ctx(), }); // Only set it when the global deployment ID is not yet configured if let Some(dep_id) = deployment_id { runtime_sources::ensure_deployment_id(dep_id); } let wait_sec = 5; let mut exit_count = 0; loop { if let Err(err) = ec.init(ctx.clone()).await { error!("init err: {}", err); error!("retry after {} second", wait_sec); sleep(Duration::from_secs(wait_sec)).await; if exit_count > 10 { return Err(Error::other("store init failed: init retry budget exhausted")); } exit_count += 1; continue; } break; } runtime_sources::publish_object_store(ec.clone()).await; Ok(ec) } #[instrument(level = "debug", skip(self, rx))] pub async fn init(self: &Arc, rx: CancellationToken) -> Result<()> { runtime_sources::ensure_boot_time().await; let meta = load_pool_meta_for_startup( self.pools .first() .cloned() .ok_or_else(|| Error::other("store init failed: no storage pools available"))?, ) .await?; let update = meta.validate(self.pools.clone())?; let endpoints = runtime_sources::endpoint_pools_or_default(); let should_persist_pool_meta = runtime_sources::first_cluster_node_is_local().await; let installed_pool_meta = if !update { meta.clone() } else { let new_meta = PoolMeta::new(&self.pools, &meta); // Only one local node should persist validated pool metadata here; otherwise // distributed startup can race on the same lock and replay the prior init bug. if should_persist_pool_meta { save_validated_pool_meta_for_startup(&new_meta, self.pools.clone()).await?; } new_meta }; { let mut pool_meta = self.pool_meta.write().await; *pool_meta = installed_pool_meta.clone(); } resolve_store_init_stage_result(self.load_rebalance_meta().await, "load_rebalance_meta")?; let rebalance_meta_loaded = self.rebalance_meta.read().await.is_some(); let decommission_running = pool_meta_has_active_decommission(&installed_pool_meta) || self.is_decommission_running().await; if should_auto_start_rebalance_after_init(decommission_running, rebalance_meta_loaded) { resolve_store_init_stage_result(self.start_rebalance().await, "start_rebalance")?; } else if decommission_running && rebalance_meta_loaded { warn!( event = EVENT_ECSTORE_INIT_STATUS, component = LOG_COMPONENT_ECSTORE, subsystem = LOG_SUBSYSTEM_STORE_INIT, stage = "start_rebalance", reason = "active_decommission", "Deferred rebalance auto-start during store init because decommission is active" ); } let pools = installed_pool_meta.return_resumable_pools(); let mut pool_indices = Vec::with_capacity(pools.len()); for p in pools.iter() { if let Some(idx) = endpoints.get_pool_idx(&p.cmd_line) { pool_indices.push(idx); } else { return Err(Error::other(format!( "store init failed to resolve resumable decommission pool `{}` from current endpoints", p.cmd_line ))); } } let local_pool_indices = local_decommission_queue_prefix(&endpoints, &pool_indices)?; if !local_pool_indices.is_empty() { let store = self.clone(); tokio::spawn(async move { if !wait_for_local_decommission_resume_delay(&rx, LOCAL_DECOMMISSION_INITIAL_RESUME_DELAY).await { return; } resume_local_decommission_after_init(store, rx, local_pool_indices).await; }); } runtime_sources::init_bucket_monitor_for_current_endpoints(); init_background_expiry(self.clone()).await; crate::bucket::lifecycle::bucket_lifecycle_ops::init_background_stale_multipart_upload_cleanup(self.clone()); TransitionState::init(self.clone()).await; crate::services::tier::tier::try_migrate_tiering_config(self.clone()).await; if let Err(err) = runtime_sources::init_tier_config_mgr(self.clone()).await { info!("TierConfigMgr init error: {}", err); } Ok(()) } pub fn init_local_disks() {} pub fn single_pool(&self) -> bool { self.pools.len() == 1 } } #[cfg(test)] mod tests { use super::{ LOCAL_DECOMMISSION_RESUME_MAX_CONFIG_RETRIES, load_pool_meta_for_startup, pool_first_endpoint_is_local, resolve_store_init_stage_result, save_validated_pool_meta_for_startup, should_auto_start_rebalance_after_init, should_auto_start_rebalance_after_recovered_meta, should_resume_local_decommission, should_retry_local_decommission_resume, wait_for_local_decommission_resume_delay, }; use crate::{ core::pools::{POOL_META_VERSION, PoolDecommissionInfo, PoolMeta, PoolStatus}, disk::endpoint::Endpoint, error::{Error, Result, StorageError}, layout::endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}, object_api::{GetObjectReader, ObjectInfo, ObjectOptions, PutObjReader}, services::rebalance::RebalanceMeta, storage_api_contracts::{object::ObjectIO, range::HTTPRangeSpec}, }; use http::HeaderMap; use std::{ io::Cursor, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, time::Duration, }; use time::OffsetDateTime; use tokio_util::sync::CancellationToken; #[derive(Debug)] struct StartupPoolMetaStorage { read_payload: Vec, read_without_lock: AtomicBool, wrote_without_lock: AtomicBool, wrote_with_max_parity: AtomicBool, } impl StartupPoolMetaStorage { fn new(read_payload: Vec) -> Self { Self { read_payload, read_without_lock: AtomicBool::new(false), wrote_without_lock: AtomicBool::new(false), wrote_with_max_parity: AtomicBool::new(false), } } fn object_info(&self, bucket: &str, object: &str, size: usize) -> ObjectInfo { ObjectInfo { bucket: bucket.to_string(), name: object.to_string(), size: size as i64, actual_size: size as i64, ..Default::default() } } } #[async_trait::async_trait] impl ObjectIO for StartupPoolMetaStorage { type Error = Error; type RangeSpec = HTTPRangeSpec; type HeaderMap = HeaderMap; type ObjectOptions = ObjectOptions; type ObjectInfo = ObjectInfo; type GetObjectReader = GetObjectReader; type PutObjectReader = PutObjReader; async fn get_object_reader( &self, bucket: &str, object: &str, _range: Option, _h: HeaderMap, opts: &ObjectOptions, ) -> Result { assert!(opts.no_lock, "store init pool metadata load must not require namespace locks"); self.read_without_lock.store(true, Ordering::SeqCst); Ok(GetObjectReader { stream: Box::new(Cursor::new(self.read_payload.clone())), object_info: self.object_info(bucket, object, self.read_payload.len()), buffered_body: None, }) } async fn put_object( &self, bucket: &str, object: &str, _data: &mut PutObjReader, opts: &ObjectOptions, ) -> Result { assert!(opts.no_lock, "store init pool metadata save must not require namespace locks"); self.wrote_without_lock.store(true, Ordering::SeqCst); self.wrote_with_max_parity.store(opts.max_parity, Ordering::SeqCst); Ok(self.object_info(bucket, object, 0)) } } fn init_test_pool_meta(decommission: Option) -> PoolMeta { PoolMeta { version: POOL_META_VERSION, pools: vec![PoolStatus { id: 0, cmd_line: "pool-0".to_string(), last_update: OffsetDateTime::UNIX_EPOCH, decommission, }], dont_save: false, } } #[tokio::test] async fn test_store_init_pool_meta_io_bypasses_namespace_lock_surface() { let storage = Arc::new(StartupPoolMetaStorage::new(Vec::new())); let loaded = load_pool_meta_for_startup(storage.clone()) .await .expect("startup pool metadata load should tolerate missing metadata without locks"); assert!(loaded.pools.is_empty()); assert!(storage.read_without_lock.load(Ordering::SeqCst)); let meta = PoolMeta { version: POOL_META_VERSION, pools: Vec::new(), dont_save: false, }; save_validated_pool_meta_for_startup(&meta, vec![storage.clone()]) .await .expect("startup pool metadata save should bypass locks"); assert!(storage.wrote_without_lock.load(Ordering::SeqCst)); assert!(storage.wrote_with_max_parity.load(Ordering::SeqCst)); } #[test] fn test_should_resume_local_decommission_respects_local_flag() { let mut local_endpoint = Endpoint::try_from("http://127.0.0.1:9000/data").expect("endpoint should parse"); local_endpoint.is_local = true; let endpoints = EndpointServerPools::from(vec![PoolEndpoints { legacy: false, set_count: 1, drives_per_set: 1, endpoints: Endpoints::from(vec![local_endpoint]), cmd_line: "pool-0".to_string(), platform: String::new(), }]); assert!(should_resume_local_decommission(&endpoints, 0).expect("local endpoint should resume")); } #[test] fn test_should_resume_local_decommission_rejects_unresolvable_pool() { let endpoints = EndpointServerPools::default(); let err = should_resume_local_decommission(&endpoints, 0).expect_err("missing pool should error"); assert_eq!( err.to_string(), "Io error: store init failed to resolve decommission resume pool index 0 from current endpoints" ); } #[test] fn test_should_resume_local_decommission_rejects_missing_endpoint() { let endpoints = EndpointServerPools::from(vec![PoolEndpoints { legacy: false, set_count: 1, drives_per_set: 1, endpoints: Endpoints::from(Vec::::new()), cmd_line: "pool-0".to_string(), platform: String::new(), }]); let err = should_resume_local_decommission(&endpoints, 0).expect_err("missing endpoint should error"); assert_eq!( err.to_string(), "Io error: store init failed to resolve decommission resume pool index 0: no endpoints available" ); } #[test] fn test_should_retry_local_decommission_resume_accepts_config_not_found_before_retry_limit() { assert!(should_retry_local_decommission_resume(&StorageError::ConfigNotFound, 0)); } #[test] fn test_should_retry_local_decommission_resume_rejects_config_not_found_at_retry_limit() { assert!(!should_retry_local_decommission_resume( &StorageError::ConfigNotFound, LOCAL_DECOMMISSION_RESUME_MAX_CONFIG_RETRIES )); } #[test] fn test_should_retry_local_decommission_resume_rejects_non_config_errors() { assert!(!should_retry_local_decommission_resume(&StorageError::SlowDown, 0)); } #[test] fn test_should_auto_start_rebalance_after_init_allows_loaded_rebalance_without_decommission() { assert!(should_auto_start_rebalance_after_init(false, true)); } #[test] fn test_should_auto_start_rebalance_after_init_rejects_active_decommission() { assert!(!should_auto_start_rebalance_after_init(true, true)); } #[test] fn test_should_auto_start_rebalance_after_init_rejects_missing_rebalance_meta() { assert!(!should_auto_start_rebalance_after_init(false, false)); } #[test] fn test_store_init_recovery_skips_rebalance_when_decommission_metadata_is_active() { let pool_meta = init_test_pool_meta(Some(PoolDecommissionInfo { start_time: Some(OffsetDateTime::UNIX_EPOCH), complete: false, failed: false, canceled: false, ..Default::default() })); let rebalance_meta = Some(RebalanceMeta::default()); assert!(!should_auto_start_rebalance_after_recovered_meta(&pool_meta, rebalance_meta.is_some())); } #[test] fn test_store_init_recovery_allows_rebalance_when_only_rebalance_metadata_exists() { let pool_meta = init_test_pool_meta(None); let rebalance_meta = Some(RebalanceMeta::default()); assert!(should_auto_start_rebalance_after_recovered_meta(&pool_meta, rebalance_meta.is_some())); } #[test] fn test_resolve_store_init_stage_result_passthrough_ok() { resolve_store_init_stage_result(Ok(()), "load_rebalance_meta").expect("successful stage should pass through"); } #[test] fn test_resolve_store_init_stage_result_wraps_error_context() { let err = resolve_store_init_stage_result(Err(StorageError::SlowDown), "start_rebalance") .expect_err("failed stage should be wrapped"); let err_message = err.to_string(); assert!(err_message.contains("store init failed during start_rebalance")); assert!(err_message.contains(&StorageError::SlowDown.to_string())); } #[tokio::test] async fn test_wait_for_local_decommission_resume_delay_returns_true_after_delay() { let rx = CancellationToken::new(); assert!(wait_for_local_decommission_resume_delay(&rx, Duration::from_millis(1)).await); } #[tokio::test] async fn test_wait_for_local_decommission_resume_delay_returns_false_when_cancelled() { let rx = CancellationToken::new(); rx.cancel(); assert!(!wait_for_local_decommission_resume_delay(&rx, Duration::from_secs(1)).await); } #[test] fn test_pool_first_endpoint_is_local_uses_pool_scope_for_expansion() { let mut remote_endpoint = Endpoint::try_from("http://127.0.0.2:9000/data1").expect("remote endpoint should parse"); remote_endpoint.is_local = false; let mut local_endpoint = Endpoint::try_from("http://127.0.0.1:9000/data1").expect("local endpoint should parse"); local_endpoint.is_local = true; let endpoints = EndpointServerPools::from(vec![ PoolEndpoints { legacy: false, set_count: 1, drives_per_set: 1, endpoints: Endpoints::from(vec![remote_endpoint]), cmd_line: "pool-0".to_string(), platform: String::new(), }, PoolEndpoints { legacy: false, set_count: 1, drives_per_set: 1, endpoints: Endpoints::from(vec![local_endpoint]), cmd_line: "pool-1".to_string(), platform: String::new(), }, ]); assert!(!endpoints.first_local(), "cluster first endpoint is intentionally remote"); assert!( pool_first_endpoint_is_local(endpoints.as_ref().get(1).expect("second pool should exist")), "the expanded pool should be initialized by its own first local endpoint" ); } }