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:
Zhengchao An
2026-07-08 15:01:40 +08:00
committed by GitHub
parent cda7688909
commit 91dec123d9
10 changed files with 444 additions and 37 deletions
+3
View File
@@ -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
+87 -1
View File
@@ -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
+40
View File
@@ -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;