mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 16:59:52 +00:00
refactor(ecstore): add per-instance InstanceContext, migrate erasure setup type (#4413)
* refactor(ecstore): add per-instance InstanceContext, migrate erasure setup type Phase 5 of the global-singleton consolidation (backlog#939): begin moving runtime identity state out of process globals so multiple ECStore instances can coexist in one process. Isolation is carried by the object graph (ECStore -> Sets -> SetDisks holding an Arc<InstanceContext>), not a task-local, which does not propagate across the many internal tokio::spawn boundaries in the data/background paths. This first slice migrates the erasure setup type -- previously three independent process-global bools -- into a single per-instance RwLock<SetupType> that derives is_erasure / is_dist_erasure / is_erasure_sd, removing a triple source of truth that could drift out of sync. - New runtime::instance module: InstanceContext + process bootstrap context. - The legacy free-function facade (is_erasure/update_erasure_type/...) keeps its signatures and forwards to the current instance's context, falling back to the bootstrap context before a store is published. - ECStore gains a pub(crate) ctx field and setup_is_* accessors; its constructors adopt the bootstrap context (never mint a fresh one) so startup writes and post-construction reads share one cell -- single-instance behavior is byte-for-byte unchanged. Tests: erasure predicate derivation vs the legacy behavior, object-graph carrier isolation across two ECStore instances, and bootstrap adoption. Refs: backlog#939 (Phase 5, Slice 1), backlog#653 (item 8) * refactor(ecstore): thread InstanceContext down the object graph (Phase 5 Slice 2) (#4415) * refactor(ecstore): source the namespace lock manager per-instance (#4417) refactor(ecstore): source the namespace lock manager per-instance (Phase 5 Slice 3) Phase 5 Slice 3 (backlog#939): give each instance its own lock namespace by sourcing SetDisks' lock manager from the instance context instead of the process singleton. This removes the false cross-instance mutual exclusion (and attendant ABBA risk) that a shared GlobalLockManager would cause once multiple instances coexist. - InstanceContext gains a `lock_manager: Arc<GlobalLockManager>`. `new()` mints a fresh manager (independent per-instance); `bootstrap_ctx()` aliases the process singleton via get_global_lock_manager(), so a single-instance deployment keeps exactly one shared namespace. - SetDisks::new sources `local_lock_manager` from `ctx.lock_manager()` (the ctx it already adopts), not `runtime_sources::global_lock_manager()`. Single instance: same Arc as before, so behavior is unchanged. - Remove the now-unused `runtime_sources::global_lock_manager()` wrapper. Tests: bootstrap lock manager aliases the process singleton; two fresh contexts own distinct managers; a SetDisks' lock manager is the one from its context and aliases the global singleton in a single-instance build. Verification: cargo test -p rustfs-ecstore (10 Phase 5 + set_disk locking regressions green), cargo clippy -p rustfs-ecstore --all-targets (clean), make pre-commit (pass). Refs: backlog#939 (Phase 5, Slice 3). Stacked on #4415 (Slice 2).
This commit is contained in:
@@ -334,6 +334,9 @@ impl ECStore {
|
||||
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
|
||||
|
||||
@@ -44,6 +44,7 @@ use crate::error::{
|
||||
is_err_read_quorum, is_err_version_not_found, to_object_err,
|
||||
};
|
||||
use crate::runtime::global::DISK_RESERVE_FRACTION;
|
||||
use crate::runtime::instance::InstanceContext;
|
||||
use crate::runtime::sources as runtime_sources;
|
||||
use crate::services::rebalance::RebalanceMeta;
|
||||
use crate::storage_api_contracts::{
|
||||
@@ -187,6 +188,14 @@ pub struct ECStore {
|
||||
/// The saver then clones the latest `pool_meta` under a short read lock and
|
||||
/// releases it before awaiting disk writes.
|
||||
pub(crate) pool_meta_save_gate: Mutex<()>,
|
||||
/// Per-instance runtime state (Phase 5, backlog#939).
|
||||
///
|
||||
/// Carries this instance's identity/runtime out of the process globals so
|
||||
/// multiple instances can coexist without cross-contamination. `new`
|
||||
/// adopts the process bootstrap context (never mints a fresh one) so that
|
||||
/// startup writes and post-construction reads share one cell — single
|
||||
/// instance behavior is unchanged.
|
||||
pub(crate) ctx: Arc<InstanceContext>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ECStore {
|
||||
@@ -285,6 +294,29 @@ impl ECStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 5: Per-instance erasure setup accessors (backlog#939)
|
||||
///
|
||||
/// These read this instance's own [`InstanceContext`] rather than a process
|
||||
/// global, so two instances carrying different contexts stay isolated. The
|
||||
/// legacy free-function facade (`runtime::global::is_erasure` etc.) forwards to
|
||||
/// the current instance's context, preserving single-instance behavior.
|
||||
impl ECStore {
|
||||
/// Whether this instance uses erasure coding (single-node or distributed).
|
||||
pub async fn setup_is_erasure(&self) -> bool {
|
||||
self.ctx.is_erasure().await
|
||||
}
|
||||
|
||||
/// Whether this instance uses distributed erasure coding.
|
||||
pub async fn setup_is_dist_erasure(&self) -> bool {
|
||||
self.ctx.is_dist_erasure().await
|
||||
}
|
||||
|
||||
/// Whether this instance uses single-drive erasure coding.
|
||||
pub async fn setup_is_erasure_sd(&self) -> bool {
|
||||
self.ctx.is_erasure_sd().await
|
||||
}
|
||||
}
|
||||
|
||||
// impl Clone for ECStore {
|
||||
// fn clone(&self) -> Self {
|
||||
// let pool_meta = match self.pool_meta.read() {
|
||||
@@ -783,7 +815,7 @@ impl crate::storage_api_contracts::admin::StorageAdminApi for ECStore {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::layout::endpoints::{Endpoints, PoolEndpoints};
|
||||
use crate::layout::endpoints::{Endpoints, PoolEndpoints, SetupType};
|
||||
use crate::runtime::global::reset_local_disk_test_state;
|
||||
use crate::runtime::sources::{clear_local_disk_id_map_for_test, local_disk_path_by_id};
|
||||
use crate::store::init_format::{connect_load_init_formats, init_disks};
|
||||
@@ -800,6 +832,60 @@ mod tests {
|
||||
assert!(infos.iter().all(|info| info.is_none()));
|
||||
}
|
||||
|
||||
// Build a minimal ECStore carrying an explicit instance context. Empty
|
||||
// pools/disks are sufficient: the Phase 5 accessors read only `self.ctx`.
|
||||
fn build_store_with_ctx(ctx: Arc<InstanceContext>) -> ECStore {
|
||||
let endpoint_pools = EndpointServerPools::default();
|
||||
ECStore {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
disk_map: std::collections::HashMap::new(),
|
||||
pools: Vec::new(),
|
||||
peer_sys: crate::cluster::rpc::S3PeerSys::new(&endpoint_pools),
|
||||
pool_meta: RwLock::new(PoolMeta::default()),
|
||||
rebalance_meta: RwLock::new(None),
|
||||
decommission_cancelers: RwLock::new(Vec::new()),
|
||||
start_gate: Mutex::new(()),
|
||||
pool_meta_save_gate: Mutex::new(()),
|
||||
ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// The object graph is the isolation carrier: two ECStore instances holding
|
||||
// distinct contexts report independent erasure state through their real
|
||||
// `&self` accessors — no cross-contamination.
|
||||
#[tokio::test]
|
||||
async fn instance_context_carrier_isolates_two_stores() {
|
||||
let ctx_a = Arc::new(InstanceContext::new());
|
||||
let ctx_b = Arc::new(InstanceContext::new());
|
||||
ctx_a.update_erasure_type(SetupType::DistErasure).await;
|
||||
ctx_b.update_erasure_type(SetupType::ErasureSD).await;
|
||||
|
||||
let store_a = build_store_with_ctx(ctx_a);
|
||||
let store_b = build_store_with_ctx(ctx_b);
|
||||
|
||||
// store_a: distributed erasure (implies is_erasure), not single-drive.
|
||||
assert!(store_a.setup_is_erasure().await);
|
||||
assert!(store_a.setup_is_dist_erasure().await);
|
||||
assert!(!store_a.setup_is_erasure_sd().await);
|
||||
|
||||
// store_b: single-drive erasure only.
|
||||
assert!(store_b.setup_is_erasure_sd().await);
|
||||
assert!(!store_b.setup_is_erasure().await);
|
||||
assert!(!store_b.setup_is_dist_erasure().await);
|
||||
}
|
||||
|
||||
// The production/test constructors ADOPT the process bootstrap context
|
||||
// (same Arc), so a startup write recorded before the store existed is
|
||||
// visible through the store afterward — single-instance behavior preserved.
|
||||
#[tokio::test]
|
||||
async fn store_adopts_bootstrap_context() {
|
||||
let store = build_store_with_ctx(crate::runtime::instance::bootstrap_ctx());
|
||||
assert!(
|
||||
Arc::ptr_eq(&store.ctx, &crate::runtime::instance::bootstrap_ctx()),
|
||||
"store built via adoption must share the bootstrap context Arc"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_has_space_for() {
|
||||
let disk_infos = vec![None, None]; // No actual disk info
|
||||
|
||||
@@ -1967,9 +1967,49 @@ mod tests {
|
||||
decommission_cancelers: RwLock::new(Vec::new()),
|
||||
start_gate: Mutex::new(()),
|
||||
pool_meta_save_gate: Mutex::new(()),
|
||||
ctx: crate::runtime::instance::bootstrap_ctx(),
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5 Slice 2 (backlog#939): the instance context flows down the whole
|
||||
// object graph — ECStore, its Sets, and their SetDisks must all carry the
|
||||
// same `Arc<InstanceContext>` in a single-instance deployment.
|
||||
#[tokio::test]
|
||||
async fn instance_context_flows_through_object_graph() {
|
||||
let store = new_read_lock_test_store().await;
|
||||
|
||||
let sets = store.pools.first().expect("test store has one pool");
|
||||
assert!(
|
||||
std::sync::Arc::ptr_eq(&store.ctx, sets.instance_ctx()),
|
||||
"Sets must carry the store's instance context"
|
||||
);
|
||||
|
||||
let set_disks = sets.disk_set.first().expect("pool has one set");
|
||||
assert!(
|
||||
std::sync::Arc::ptr_eq(sets.instance_ctx(), set_disks.instance_ctx()),
|
||||
"SetDisks must carry the Sets' instance context"
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 5 Slice 3 (backlog#939): a SetDisks sources its lock manager from
|
||||
// its instance context (not an independent process lookup), and in a
|
||||
// single-instance build that context aliases the process lock-manager
|
||||
// singleton — so the lock namespace is unchanged.
|
||||
#[tokio::test]
|
||||
async fn set_disks_lock_manager_comes_from_instance_context() {
|
||||
let store = new_read_lock_test_store().await;
|
||||
let set_disks = store.pools[0].disk_set.first().expect("pool has one set");
|
||||
|
||||
assert!(
|
||||
std::sync::Arc::ptr_eq(set_disks.local_lock_manager_for_test(), &set_disks.instance_ctx().lock_manager()),
|
||||
"SetDisks lock manager must be sourced from its instance context"
|
||||
);
|
||||
assert!(
|
||||
std::sync::Arc::ptr_eq(set_disks.local_lock_manager_for_test(), &rustfs_lock::get_global_lock_manager()),
|
||||
"single-instance lock manager must alias the process singleton"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn acquired_read_lock_marks_metadata_cache_safe_for_set_layer() {
|
||||
let store = new_read_lock_test_store().await;
|
||||
|
||||
Reference in New Issue
Block a user