Files
rustfs/rustfs/src/app/context/server_slot.rs
T

153 lines
6.2 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-server application-context slot (backlog#1052 S2).
//!
//! The S3 service starts serving before storage and IAM finish initializing,
//! so a server cannot hold its `AppContext` at construction time. This slot is
//! the late-bound carrier: startup creates one per server, hands it to the
//! request-path owners (the `FS` service), and installs the `AppContext` into
//! it once IAM bootstrap completes.
//!
//! Some legacy, non-request call sites still use the ambient compatibility
//! resolver. Request dispatch must instead use the strict installed-context
//! accessors: once a request carries a server slot, an empty slot means that
//! server is not ready rather than that another server's global context applies.
use super::global::{AppContext, get_global_app_context};
use crate::app::storage_api::context::{BootstrapLocalTarget, ECStore, InstanceContext};
use std::sync::{Arc, OnceLock};
/// Late-bound, per-server handle to the application context.
#[derive(Default)]
pub struct ServerContextSlot {
app_context: OnceLock<Arc<AppContext>>,
bootstrap_target: Option<BootstrapLocalTarget>,
heal_topology_fingerprint: Arc<tokio::sync::OnceCell<String>>,
}
// Manual Debug: AppContext is not Debug, so summarize installation state.
impl std::fmt::Debug for ServerContextSlot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerContextSlot")
.field("app_context_installed", &self.app_context.get().is_some())
.field("heal_topology_fingerprint_initialized", &self.heal_topology_fingerprint.get().is_some())
.finish()
}
}
impl ServerContextSlot {
pub fn new() -> Arc<Self> {
Arc::new(Self {
app_context: OnceLock::new(),
bootstrap_target: None,
heal_topology_fingerprint: Arc::new(tokio::sync::OnceCell::new()),
})
}
/// Bind the listener to its foundation before it can accept requests.
pub fn with_instance_context(ctx: Arc<InstanceContext>) -> Arc<Self> {
Arc::new(Self {
bootstrap_target: Some(BootstrapLocalTarget::new(ctx)),
..Self::default()
})
}
/// Install this server's application context (once). Returns `false` if
/// the slot was already installed; the first installation wins, matching
/// the process-global singleton's `get_or_init` semantics.
pub fn install(&self, context: Arc<AppContext>) -> bool {
self.try_install(context).is_ok()
}
/// Claim the slot before any process-global application publication.
/// Repeated installation, even of the same Arc, is an explicit conflict.
pub fn try_install(&self, context: Arc<AppContext>) -> std::io::Result<()> {
if self
.bootstrap_target
.as_ref()
.is_some_and(|target| !target.is_for_store(&context.object_store()))
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"application context does not belong to this server foundation",
));
}
self.app_context.set(context).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::AlreadyExists, "server application context is already installed")
})
}
/// Immutable, restricted startup capability; never resolves an ambient store.
pub fn bootstrap_target(&self) -> Option<BootstrapLocalTarget> {
self.bootstrap_target.clone()
}
/// This server's installed application context, if startup has completed.
///
/// Request-scoped resolution must use this accessor so an empty slot never
/// resolves through another server's ambient context.
pub fn installed_app_context(&self) -> Option<Arc<AppContext>> {
self.app_context.get().cloned()
}
/// This server's installed object store, if startup has completed.
pub fn installed_object_store(&self) -> Option<Arc<ECStore>> {
self.installed_app_context().map(|context| context.object_store())
}
/// This server's application context: the installed one, or the
/// process-global singleton as the legacy compatibility default.
///
/// This accessor is for callers without request-slot semantics. Request
/// dispatch must use [`Self::installed_app_context`] instead.
pub fn app_context(&self) -> Option<Arc<AppContext>> {
self.installed_app_context().or_else(get_global_app_context)
}
/// This server's object store, resolved through [`Self::app_context`].
pub fn object_store(&self) -> Option<Arc<ECStore>> {
self.app_context().map(|context| context.object_store())
}
pub fn heal_topology_fingerprint(&self) -> Arc<tokio::sync::OnceCell<String>> {
Arc::clone(&self.heal_topology_fingerprint)
}
}
#[cfg(test)]
mod tests {
use super::{ServerContextSlot, get_global_app_context};
use std::sync::Arc;
// Before anything is installed — and with no global AppContext in this
// process (nextest runs each test in its own process) — resolution yields
// nothing: the same "not ready yet" answer the ambient path gives before
// IAM bootstrap completes.
#[test]
fn empty_slot_retains_ambient_compatibility() {
let slot = ServerContextSlot::new();
assert_eq!(slot.app_context().is_some(), get_global_app_context().is_some());
assert_eq!(slot.object_store().is_some(), get_global_app_context().is_some());
}
#[test]
fn heal_topology_cache_is_owned_by_each_server_slot() {
let first = ServerContextSlot::new();
let second = ServerContextSlot::new();
assert!(!Arc::ptr_eq(&first.heal_topology_fingerprint(), &second.heal_topology_fingerprint()));
}
}