diff --git a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs index c6982a2ca..50a9db555 100644 --- a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs +++ b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs @@ -51,7 +51,6 @@ use crate::store::ECStore; use async_channel::{Receiver as A_Receiver, Sender as A_Sender, bounded}; use futures::Future; use http::HeaderMap; -use lazy_static::lazy_static; use rustfs_common::metrics::{ IlmAction, Metrics, ScannerLifecycleExpiryStateUpdate, ScannerLifecycleTransitionStateUpdate, global_metrics, }; @@ -119,17 +118,15 @@ const DEFAULT_STALE_UPLOADS_CLEANUP_INTERVAL: StdDuration = StdDuration::from_se const DATE_EXPIRY_EXISTING_OBJECTS_GRACE_SECS: i64 = 5; const EXPIRY_WORKER_QUEUE_CAPACITY: usize = 1000; -lazy_static! { - pub static ref GLOBAL_EXPIRY_STATE: Arc> = ExpiryState::new(); - pub static ref GLOBAL_TRANSITION_STATE: Arc = TransitionState::new(); -} - +// Phase 5 (backlog#939): lifecycle expiry/transition state moved into the +// per-instance `InstanceContext`; these owner helpers forward to the current +// instance's context (lazily materialized, shared for single-instance). pub fn get_global_expiry_state() -> Arc> { - GLOBAL_EXPIRY_STATE.clone() + crate::runtime::global::current_ctx().expiry_state() } pub fn get_global_transition_state() -> Arc { - GLOBAL_TRANSITION_STATE.clone() + crate::runtime::global::current_ctx().transition_state() } fn resolve_transition_worker_count() -> (i64, i64, i64) { diff --git a/crates/ecstore/src/runtime/instance.rs b/crates/ecstore/src/runtime/instance.rs index e0c823f94..95981b7f5 100644 --- a/crates/ecstore/src/runtime/instance.rs +++ b/crates/ecstore/src/runtime/instance.rs @@ -39,6 +39,7 @@ //! startup writes and post-construction reads hit the same cell and //! single-instance behavior is byte-for-byte unchanged. +use crate::bucket::lifecycle::bucket_lifecycle_ops::{ExpiryState, TransitionState}; use crate::layout::endpoints::{EndpointServerPools, SetupType}; use crate::services::event_notification::EventNotifier; use crate::services::tier::tier::TierConfigMgr; @@ -96,6 +97,16 @@ pub struct InstanceContext { /// Lazily materialized like the tier config manager; holds the instance's /// notification target list. Replaces the eager process static. event_notifier: OnceLock>>, + /// This instance's lifecycle expiry state (Phase 5 Slice 9, backlog#939). + /// + /// Lazily materialized; holds the instance's expiry worker channels/stats. + /// Replaces the eager process static. + expiry_state: OnceLock>>, + /// This instance's lifecycle transition state (Phase 5 Slice 9, backlog#939). + /// + /// Lazily materialized; holds the instance's transition workers/stats. + /// Replaces the eager process static. + transition_state: OnceLock>, } impl InstanceContext { @@ -120,6 +131,8 @@ impl InstanceContext { endpoints: OnceLock::new(), tier_config_mgr: OnceLock::new(), event_notifier: OnceLock::new(), + expiry_state: OnceLock::new(), + transition_state: OnceLock::new(), } } @@ -185,6 +198,18 @@ impl InstanceContext { self.event_notifier.get_or_init(EventNotifier::new).clone() } + /// This instance's lifecycle expiry state, materializing it on first + /// access. Shared for the lifetime of the instance. + pub fn expiry_state(&self) -> Arc> { + self.expiry_state.get_or_init(ExpiryState::new).clone() + } + + /// This instance's lifecycle transition state, materializing it on first + /// access. Shared for the lifetime of the instance. + pub fn transition_state(&self) -> Arc { + self.transition_state.get_or_init(TransitionState::new).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; @@ -228,6 +253,8 @@ impl std::fmt::Debug for InstanceContext { .field("endpoints", &self.endpoints) .field("tier_config_mgr_set", &self.tier_config_mgr.get().is_some()) .field("event_notifier_set", &self.event_notifier.get().is_some()) + .field("expiry_state_set", &self.expiry_state.get().is_some()) + .field("transition_state_set", &self.transition_state.get().is_some()) .finish_non_exhaustive() } } @@ -489,4 +516,18 @@ mod tests { "fresh contexts must not share an event notifier" ); } + + // Lifecycle expiry/transition state are each stable within an instance and + // distinct across instances. + #[tokio::test] + async fn lifecycle_state_is_stable_within_and_distinct_across_instances() { + let ctx_a = InstanceContext::new(); + let ctx_b = InstanceContext::new(); + + assert!(Arc::ptr_eq(&ctx_a.expiry_state(), &ctx_a.expiry_state())); + assert!(Arc::ptr_eq(&ctx_a.transition_state(), &ctx_a.transition_state())); + + assert!(!Arc::ptr_eq(&ctx_a.expiry_state(), &ctx_b.expiry_state())); + assert!(!Arc::ptr_eq(&ctx_a.transition_state(), &ctx_b.transition_state())); + } } diff --git a/crates/ecstore/src/runtime/sources.rs b/crates/ecstore/src/runtime/sources.rs index 406bd6e2b..cba0ff787 100644 --- a/crates/ecstore/src/runtime/sources.rs +++ b/crates/ecstore/src/runtime/sources.rs @@ -21,7 +21,7 @@ use std::{ use crate::bucket::bandwidth::monitor::Monitor; use crate::disk::endpoint::Endpoint; use crate::{ - bucket::lifecycle::bucket_lifecycle_ops::{ExpiryState, GLOBAL_EXPIRY_STATE, GLOBAL_TRANSITION_STATE, TransitionState}, + bucket::lifecycle::bucket_lifecycle_ops::{ExpiryState, TransitionState}, bucket::metadata_sys::{BucketMetadataSys, get_global_bucket_metadata_sys}, bucket::replication::{ DynReplicationPool, ReplicationStats, @@ -391,11 +391,11 @@ pub(crate) fn tier_config_mgr_handle() -> Arc> { } pub fn expiry_state_handle() -> Arc> { - GLOBAL_EXPIRY_STATE.clone() + crate::runtime::global::current_ctx().expiry_state() } pub fn transition_state_handle() -> Arc { - GLOBAL_TRANSITION_STATE.clone() + crate::runtime::global::current_ctx().transition_state() } pub(crate) fn event_notifier_handle() -> Arc> {