mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-05 21:07:43 +00:00
91dec123d9
* 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).
233 lines
9.6 KiB
Rust
233 lines
9.6 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.
|
|
|
|
//! Per-instance runtime state ("instance context").
|
|
//!
|
|
//! Phase 5 of the global-singleton consolidation (backlog#939). State that
|
|
//! carries an `ECStore` instance's identity/runtime is being moved out of the
|
|
//! process-level statics in [`super::global`] into this context so that
|
|
//! multiple `ECStore` instances can coexist in one process without
|
|
//! cross-contaminating each other's state.
|
|
//!
|
|
//! ## Isolation carrier
|
|
//!
|
|
//! Multi-instance disambiguation is carried by the **object graph**
|
|
//! (`ECStore` → `Vec<Arc<Sets>>` → `SetDisks`), each holding an
|
|
//! `Arc<InstanceContext>`. Instance-scoped reads resolve through `self.ctx`,
|
|
//! so no call-site signatures change. (A `task_local` was rejected during
|
|
//! design review: it does not propagate across the many internal
|
|
//! `tokio::spawn` boundaries in the data/background paths.)
|
|
//!
|
|
//! ## Backward compatibility
|
|
//!
|
|
//! The legacy free-function facade in [`super::global`]
|
|
//! (`is_erasure`/`update_erasure_type`/…) resolves the *current* instance via
|
|
//! the object-store resolver and falls back to the process-level
|
|
//! [`bootstrap_ctx`] when no store is published yet. Because `ECStore::new`
|
|
//! **adopts** the same bootstrap `Arc` (rather than minting a fresh one),
|
|
//! startup writes and post-construction reads hit the same cell and
|
|
//! single-instance behavior is byte-for-byte unchanged.
|
|
|
|
use crate::layout::endpoints::SetupType;
|
|
use rustfs_lock::{GlobalLockManager, get_global_lock_manager};
|
|
use std::sync::{Arc, OnceLock};
|
|
use tokio::sync::RwLock;
|
|
|
|
/// Runtime state owned by a single `ECStore` instance.
|
|
///
|
|
/// This is intentionally minimal in the first migration slice; subsequent
|
|
/// slices move additional identity/runtime state (topology, disk registry,
|
|
/// service handles, cancellation token) into this struct.
|
|
#[derive(Debug)]
|
|
pub struct InstanceContext {
|
|
/// The deployment's erasure setup type.
|
|
///
|
|
/// Single source of truth for the derived `is_erasure` /
|
|
/// `is_dist_erasure` / `is_erasure_sd` predicates, replacing the three
|
|
/// previously-independent process-global erasure-mode bools (which could
|
|
/// drift out of sync). Stored as one value so the three predicates can
|
|
/// never observe a torn intermediate state.
|
|
erasure_kind: RwLock<SetupType>,
|
|
/// This instance's namespace lock manager (Phase 5 Slice 3, backlog#939).
|
|
///
|
|
/// Owned per-instance so two instances no longer share a lock namespace
|
|
/// (which would make same-named objects in different instances falsely
|
|
/// contend). Single-instance aliases the process singleton, so behavior is
|
|
/// unchanged; see [`bootstrap_ctx`].
|
|
lock_manager: Arc<GlobalLockManager>,
|
|
}
|
|
|
|
impl InstanceContext {
|
|
/// Create a fresh instance context in the initial [`SetupType::Unknown`]
|
|
/// state with its own lock manager — byte-for-byte equivalent to the old
|
|
/// all-`false` erasure globals.
|
|
pub fn new() -> Self {
|
|
Self::with_lock_manager(Arc::new(GlobalLockManager::new()))
|
|
}
|
|
|
|
/// Build a context bound to a specific lock manager.
|
|
///
|
|
/// [`bootstrap_ctx`] uses this to alias the process-global lock manager so
|
|
/// single-instance deployments keep one shared namespace; `new` mints a
|
|
/// fresh manager for a genuinely independent instance.
|
|
fn with_lock_manager(lock_manager: Arc<GlobalLockManager>) -> Self {
|
|
Self {
|
|
erasure_kind: RwLock::new(SetupType::Unknown),
|
|
lock_manager,
|
|
}
|
|
}
|
|
|
|
/// This instance's namespace lock manager.
|
|
pub fn lock_manager(&self) -> Arc<GlobalLockManager> {
|
|
self.lock_manager.clone()
|
|
}
|
|
|
|
/// Update this instance's erasure setup type.
|
|
pub async fn update_erasure_type(&self, setup_type: SetupType) {
|
|
*self.erasure_kind.write().await = setup_type;
|
|
}
|
|
|
|
/// Whether this instance uses erasure coding.
|
|
///
|
|
/// True for both single-node ([`SetupType::Erasure`]) and distributed
|
|
/// ([`SetupType::DistErasure`]) erasure setups, matching the original
|
|
/// `update_erasure_type` derivation where `DistErasure` implied
|
|
/// `is_erasure == true`.
|
|
pub async fn is_erasure(&self) -> bool {
|
|
matches!(*self.erasure_kind.read().await, SetupType::Erasure | SetupType::DistErasure)
|
|
}
|
|
|
|
/// Whether this instance uses distributed erasure coding.
|
|
pub async fn is_dist_erasure(&self) -> bool {
|
|
*self.erasure_kind.read().await == SetupType::DistErasure
|
|
}
|
|
|
|
/// Whether this instance uses single-drive erasure coding.
|
|
pub async fn is_erasure_sd(&self) -> bool {
|
|
*self.erasure_kind.read().await == SetupType::ErasureSD
|
|
}
|
|
}
|
|
|
|
impl Default for InstanceContext {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Process-level bootstrap instance context.
|
|
///
|
|
/// Storage startup writes erasure state before any `ECStore` exists (see
|
|
/// `init_startup_storage_foundation`), so the context must exist ahead of the
|
|
/// object graph. `ECStore::new` adopts this same `Arc` via [`bootstrap_ctx`],
|
|
/// guaranteeing that startup writes and post-construction reads share one cell.
|
|
static BOOTSTRAP_CTX: OnceLock<Arc<InstanceContext>> = OnceLock::new();
|
|
|
|
/// Return the process-level bootstrap instance context, creating it on first
|
|
/// access.
|
|
///
|
|
/// This is the single-instance default that the legacy free-function facade
|
|
/// falls back to before an `ECStore` is published, and the `Arc` that
|
|
/// `ECStore::new` adopts as its own `ctx`.
|
|
pub fn bootstrap_ctx() -> Arc<InstanceContext> {
|
|
BOOTSTRAP_CTX
|
|
.get_or_init(|| Arc::new(InstanceContext::with_lock_manager(get_global_lock_manager())))
|
|
.clone()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// The SetupType inputs must derive the exact (is_erasure,
|
|
// is_dist_erasure, is_erasure_sd) triples that the original three
|
|
// process-global erasure bools produced via update_erasure_type().
|
|
#[tokio::test]
|
|
async fn erasure_predicates_match_legacy_derivation() {
|
|
let cases = [
|
|
// (input, is_erasure, is_dist_erasure, is_erasure_sd)
|
|
(SetupType::Unknown, false, false, false),
|
|
(SetupType::FS, false, false, false),
|
|
(SetupType::Erasure, true, false, false),
|
|
// DistErasure implies is_erasure == true (legacy global.rs:267-269).
|
|
(SetupType::DistErasure, true, true, false),
|
|
(SetupType::ErasureSD, false, false, true),
|
|
];
|
|
|
|
for (input, want_erasure, want_dist, want_sd) in cases {
|
|
let ctx = InstanceContext::new();
|
|
ctx.update_erasure_type(input.clone()).await;
|
|
assert_eq!(ctx.is_erasure().await, want_erasure, "is_erasure for {input:?}");
|
|
assert_eq!(ctx.is_dist_erasure().await, want_dist, "is_dist_erasure for {input:?}");
|
|
assert_eq!(ctx.is_erasure_sd().await, want_sd, "is_erasure_sd for {input:?}");
|
|
}
|
|
}
|
|
|
|
// A fresh context (before any update) reflects the initial all-false state.
|
|
#[tokio::test]
|
|
async fn fresh_context_is_all_false() {
|
|
let ctx = InstanceContext::new();
|
|
assert!(!ctx.is_erasure().await);
|
|
assert!(!ctx.is_dist_erasure().await);
|
|
assert!(!ctx.is_erasure_sd().await);
|
|
}
|
|
|
|
// bootstrap_ctx() is a stable process singleton: repeated calls return the
|
|
// same Arc, so a startup write is visible to a later read through it.
|
|
#[tokio::test]
|
|
async fn bootstrap_ctx_is_stable_singleton() {
|
|
let a = bootstrap_ctx();
|
|
let b = bootstrap_ctx();
|
|
assert!(Arc::ptr_eq(&a, &b), "bootstrap_ctx must return the same Arc");
|
|
}
|
|
|
|
// Two independent contexts do not share erasure state — the property that
|
|
// lets two ECStore instances (each carrying its own ctx) stay isolated.
|
|
#[tokio::test]
|
|
async fn distinct_contexts_do_not_share_state() {
|
|
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;
|
|
|
|
assert!(ctx_a.is_dist_erasure().await && ctx_a.is_erasure().await);
|
|
assert!(!ctx_a.is_erasure_sd().await);
|
|
|
|
assert!(ctx_b.is_erasure_sd().await);
|
|
assert!(!ctx_b.is_erasure().await && !ctx_b.is_dist_erasure().await);
|
|
}
|
|
|
|
// Single-instance zero-change: the bootstrap context's lock manager is the
|
|
// very same Arc the process singleton hands out, so the lock namespace is
|
|
// unchanged from before Slice 3.
|
|
#[tokio::test]
|
|
async fn bootstrap_lock_manager_aliases_process_singleton() {
|
|
assert!(
|
|
Arc::ptr_eq(&bootstrap_ctx().lock_manager(), &get_global_lock_manager()),
|
|
"bootstrap context must alias the process lock-manager singleton"
|
|
);
|
|
}
|
|
|
|
// Two independent contexts own distinct lock managers — the property that
|
|
// stops two instances from falsely contending on same-named objects.
|
|
#[tokio::test]
|
|
async fn distinct_contexts_have_distinct_lock_managers() {
|
|
let ctx_a = InstanceContext::new();
|
|
let ctx_b = InstanceContext::new();
|
|
assert!(
|
|
!Arc::ptr_eq(&ctx_a.lock_manager(), &ctx_b.lock_manager()),
|
|
"fresh contexts must not share a lock manager"
|
|
);
|
|
}
|
|
}
|