// 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::error::is_err_decommission_running; use crate::global::{ GLOBAL_EventNotifier, GLOBAL_LOCAL_DISK_ID_MAP, GLOBAL_LOCAL_DISK_MAP, GLOBAL_LOCAL_DISK_SET_DRIVES, GLOBAL_TierConfigMgr, get_global_bucket_monitor, is_dist_erasure, is_first_cluster_node_local, }; fn pool_first_endpoint_is_local(pool: &crate::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 } 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 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; } match store.decommission(rx.clone(), pool_indices.clone()).await { 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!( "store init failed to resume decommission workers for pools {:?}: {}", pool_indices, spawn_err ); } return; } Err(err) if should_retry_local_decommission_resume(&err, attempt) => { warn!( "store init decommission resume missing config for pools {:?}, retry {}/{}: {}", pool_indices, attempt + 1, LOCAL_DECOMMISSION_RESUME_MAX_CONFIG_RETRIES + 1, err ); tokio::select! { _ = rx.cancelled() => return, _ = tokio::time::sleep(LOCAL_DECOMMISSION_RESUME_RETRY_DELAY) => {} } } Err(err) => { error!("store init failed to resume decommission for pools {:?}: {}", pool_indices, err); 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(); info!("ECStore new address: {}", address.to_string()); let mut host = address.ip().to_string(); if host.is_empty() { host = GLOBAL_RUSTFS_HOST.read().await.to_string() } let mut port = address.port().to_string(); if port.is_empty() { port = GLOBAL_RUSTFS_PORT.read().await.to_string() } info!("ECStore new host: {}, port: {}", host, port); 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) = store_init::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 store_init::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; } info!("retrying get formats after {:?}", interval); select! { _ = tokio::signal::ctrl_c() => { info!("got ctrl+c, exits"); 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().unwrap().is_local() { local_disks.push(disk.as_ref().unwrap().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 !is_dist_erasure().await { let mut global_local_disk_map = GLOBAL_LOCAL_DISK_MAP.write().await; for disk in local_disks { let path = disk.endpoint().to_string(); global_local_disk_map.insert(path, Some(disk.clone())); } } 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, local_disk_map: GLOBAL_LOCAL_DISK_MAP.clone(), local_disk_id_map: GLOBAL_LOCAL_DISK_ID_MAP.clone(), local_disk_set_drives: GLOBAL_LOCAL_DISK_SET_DRIVES.clone(), tier_config_mgr: GLOBAL_TierConfigMgr.clone(), event_notifier: GLOBAL_EventNotifier.clone(), bucket_monitor: OnceLock::new(), }); // Only set it when the global deployment ID is not yet configured if let Some(dep_id) = deployment_id && get_global_deployment_id().is_none() { set_global_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; } set_object_layer(ec.clone()).await; if let Some(monitor) = get_global_bucket_monitor() { let _ = ec.bucket_monitor.set(monitor); } Ok(ec) } #[instrument(level = "debug", skip(self, rx))] pub async fn init(self: &Arc, rx: CancellationToken) -> Result<()> { GLOBAL_BOOT_TIME.get_or_init(|| async { SystemTime::now() }).await; resolve_store_init_stage_result(self.load_rebalance_meta().await, "load_rebalance_meta")?; if self.rebalance_meta.read().await.is_some() { resolve_store_init_stage_result(self.start_rebalance().await, "start_rebalance")?; } let mut meta = PoolMeta::default(); resolve_store_init_stage_result( meta.load( self.pools .first() .cloned() .ok_or_else(|| Error::other("store init failed: no storage pools available"))?, self.pools.clone(), ) .await, "load_pool_meta", )?; let update = meta.validate(self.pools.clone())?; let endpoints = get_global_endpoints(); let should_persist_pool_meta = is_first_cluster_node_local().await; if !update { { let mut pool_meta = self.pool_meta.write().await; *pool_meta = 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 { resolve_store_init_stage_result(new_meta.save(self.pools.clone()).await, "save_validated_pool_meta")?; } { let mut pool_meta = self.pool_meta.write().await; *pool_meta = new_meta; } } let pools = 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 ))); } } if !pool_indices.is_empty() { let idx = pool_indices[0]; if should_resume_local_decommission(&endpoints, idx)? { 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, pool_indices).await; }); } } let num_nodes = get_global_endpoints().get_nodes().len() as u64; init_global_bucket_monitor(num_nodes); 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::tier::tier::try_migrate_tiering_config(self.clone()).await; if let Err(err) = GLOBAL_TierConfigMgr.write().await.init(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, pool_first_endpoint_is_local, resolve_store_init_stage_result, should_resume_local_decommission, should_retry_local_decommission_resume, wait_for_local_decommission_resume_delay, }; use crate::{ disk::endpoint::Endpoint, endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}, error::StorageError, }; use std::time::Duration; use tokio_util::sync::CancellationToken; #[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_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" ); } }