mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-11 15:46:53 +00:00
d6120f5788
* refactor(ecstore): migrate mutable globals into ECStore struct fields Phase 1 of global singleton consolidation. Move mutable globals from lazy_static into ECStore struct fields as the first step toward dependency injection and multi-instance support. New ECStore fields: - is_erasure, is_dist_erasure, is_erasure_sd (erasure type flags) - local_disk_map, local_disk_id_map, local_disk_set_drives - root_disk_threshold - tier_config_mgr, event_notifier, bucket_monitor New accessor methods: - is_erasure(), is_dist_erasure(), is_erasure_sd() - update_erasure_type() - tier_config_mgr(), event_notifier(), bucket_monitor() Global functions in global.rs preserved for backward compatibility. 1151 ecstore tests pass. * fix: address PR #3214 review comments - Sync ECStore fields from globals after init() - Enforce DistErasure => is_erasure invariant in update_erasure_type() - Change bucket_monitor from Option to OnceLock for deferred initialization - Restrict TypeLocalDiskSetDrives to pub(crate) - 1151 tests pass * fix: address PR #3150 review comments - Restore get_host_addr as best-effort wrapper (String return type) - Replace bare expect("err") with descriptive messages in unsigned trailer - Simplify aws-chunked header construction - 20 signer + 96 io-core tests pass * fix(ecstore): format store imports * fix(ecstore): keep migrated accessors in sync * fix(signer): preserve host fallback and unsigned trailer errors * fix(ecstore): defer migrated global accessors * fix(signer): box unsigned trailer signing errors * fix(ecstore): avoid lock awaits during sync * fix(ecstore): narrow phase one globals --------- Co-authored-by: houseme <housemecn@gmail.com>
525 lines
20 KiB
Rust
525 lines
20 KiB
Rust
// 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<bool> {
|
|
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<ECStore>, rx: CancellationToken, pool_indices: Vec<usize>) {
|
|
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<Arc<Self>> {
|
|
// 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<Self>, 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::<Endpoint>::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"
|
|
);
|
|
}
|
|
}
|