use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant};
use lru::LruCache;
use tokio::sync::Mutex;
use tracing::{debug, warn};
use crate::block::router::{rpc_endpoint, WorkerRouter};
use crate::client::WorkerClientPool;
use crate::config::GoosefsConfig;
use crate::error::{Error, Result};
use crate::metrics::{self, name};
use crate::proto::proto::security::Capability;
use super::{AccessHint, LocalBlockReader, ShortCircuitError};
pub trait CapabilityProvider: Send + Sync {
fn capability_for(&self, block_id: i64) -> Option<Capability>;
}
#[derive(Debug, Clone)]
pub struct ShortCircuitConfig {
pub enabled: bool,
pub cache_capacity: usize,
pub cache_ttl: Duration,
pub neg_cache_ttl: Duration,
pub advise: AccessHint,
pub prefetch_enabled: bool,
pub prefetch_coalesce_gap: usize,
pub prefetch_max_batch: usize,
pub min_block_size: i64,
pub sigbus_handler: bool,
pub thp: bool,
}
impl ShortCircuitConfig {
pub fn from_config(cfg: &GoosefsConfig) -> Self {
Self {
enabled: cfg.short_circuit_enabled,
cache_capacity: cfg.short_circuit_cache_capacity.max(1),
cache_ttl: cfg.short_circuit_cache_ttl,
neg_cache_ttl: cfg.short_circuit_neg_cache_ttl,
advise: AccessHint::from_advise_str(&cfg.short_circuit_advise),
prefetch_enabled: cfg.short_circuit_prefetch_enabled,
prefetch_coalesce_gap: cfg.short_circuit_prefetch_coalesce_gap,
prefetch_max_batch: cfg.short_circuit_prefetch_max_batch.max(1),
min_block_size: cfg.short_circuit_min_block_size.max(0),
sigbus_handler: cfg.short_circuit_sigbus_handler,
thp: cfg.short_circuit_thp,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScDecisionCtx {
pub source_is_local: bool,
pub process_sc_disabled: bool,
pub negative_cached: bool,
pub block_size: i64,
}
pub fn should_use_short_circuit(cfg: &ShortCircuitConfig, ctx: &ScDecisionCtx) -> bool {
cfg.enabled
&& ctx.source_is_local
&& !ctx.process_sc_disabled
&& !ctx.negative_cached
&& ctx.block_size >= cfg.min_block_size
}
struct CachedReader {
reader: Arc<LocalBlockReader>,
inserted: Instant,
}
pub struct ShortCircuitFactory {
worker_pool: Arc<WorkerClientPool>,
router: Arc<WorkerRouter>,
cache: OnceLock<Mutex<LruCache<i64, CachedReader>>>,
neg_cache: OnceLock<Mutex<LruCache<i64, Instant>>>,
process_sc_disabled: AtomicBool,
capability_provider: Option<Arc<dyn CapabilityProvider>>,
cfg: ShortCircuitConfig,
}
impl ShortCircuitFactory {
pub fn new(
worker_pool: Arc<WorkerClientPool>,
router: Arc<WorkerRouter>,
cfg: ShortCircuitConfig,
) -> Self {
super::sigbus::install_if_enabled(cfg.sigbus_handler);
Self {
worker_pool,
router,
cache: OnceLock::new(),
neg_cache: OnceLock::new(),
process_sc_disabled: AtomicBool::new(false),
capability_provider: None,
cfg,
}
}
fn cache(&self) -> &Mutex<LruCache<i64, CachedReader>> {
self.cache.get_or_init(|| {
let cap = NonZeroUsize::new(self.cfg.cache_capacity.max(1)).unwrap();
Mutex::new(LruCache::new(cap))
})
}
fn neg_cache_cell(&self) -> &Mutex<LruCache<i64, Instant>> {
self.neg_cache.get_or_init(|| {
let neg_cap = NonZeroUsize::new(self.cfg.cache_capacity.max(1).max(64)).unwrap();
Mutex::new(LruCache::new(neg_cap))
})
}
#[cfg(test)]
fn cache_is_uninitialised(&self) -> bool {
self.cache.get().is_none()
}
#[cfg(test)]
fn neg_cache_is_uninitialised(&self) -> bool {
self.neg_cache.get().is_none()
}
pub fn with_capability_provider(mut self, provider: Arc<dyn CapabilityProvider>) -> Self {
self.capability_provider = Some(provider);
self
}
pub fn config(&self) -> &ShortCircuitConfig {
&self.cfg
}
pub fn is_process_disabled(&self) -> bool {
self.process_sc_disabled.load(Ordering::Relaxed)
}
pub async fn should_use(&self, block_id: i64, block_size: i64) -> bool {
if !self.cfg.enabled || self.is_process_disabled() {
return false;
}
let ctx = ScDecisionCtx {
source_is_local: self.router.is_block_source_local(block_id).await,
process_sc_disabled: self.is_process_disabled(),
negative_cached: self.is_negative_cached(block_id).await,
block_size,
};
should_use_short_circuit(&self.cfg, &ctx)
}
pub async fn get_or_open(
&self,
block_id: i64,
block_size: i64,
) -> std::result::Result<Arc<LocalBlockReader>, ShortCircuitError> {
if let Some(reader) = self.cache_get_fresh(block_id).await {
metrics::counter(name::CLIENT_SC_CACHE_HITS).inc(1);
return Ok(reader);
}
let worker = self
.acquire_worker(block_id)
.await
.map_err(|e| ShortCircuitError::OpenLocalBlock(Box::new(e)))?;
let capability = self
.capability_provider
.as_ref()
.and_then(|p| p.capability_for(block_id));
let open_result = LocalBlockReader::open(
&worker,
block_id,
block_size,
capability,
self.cfg.advise,
self.cfg.thp,
)
.await;
match open_result {
Ok(reader) => {
let reader = Arc::new(reader);
self.cache_put(block_id, reader.clone()).await;
Ok(reader)
}
Err(e) => {
if let ShortCircuitError::FileOpen(io) = &e {
if io.kind() == std::io::ErrorKind::PermissionDenied {
warn!(
block_id = block_id,
"short-circuit File::open EACCES — disabling SC for this process"
);
self.process_sc_disabled.store(true, Ordering::Relaxed);
}
}
self.mark_failure(block_id).await;
Err(e)
}
}
}
pub async fn mark_failure(&self, block_id: i64) {
let mut neg = self.neg_cache_cell().lock().await;
neg.put(block_id, Instant::now());
}
pub async fn invalidate(&self, block_id: i64) {
let mut cache = self.cache().lock().await;
cache.pop(&block_id);
}
async fn cache_get_fresh(&self, block_id: i64) -> Option<Arc<LocalBlockReader>> {
let mut cache = self.cache().lock().await;
if let Some(entry) = cache.get(&block_id) {
if entry.inserted.elapsed() < self.cfg.cache_ttl {
return Some(entry.reader.clone());
}
cache.pop(&block_id);
metrics::counter(name::CLIENT_SC_CACHE_EVICTIONS).inc(1);
}
None
}
async fn cache_put(&self, block_id: i64, reader: Arc<LocalBlockReader>) {
let mut cache = self.cache().lock().await;
let was_full = cache.len() == cache.cap().get() && cache.peek(&block_id).is_none();
cache.put(
block_id,
CachedReader {
reader,
inserted: Instant::now(),
},
);
if was_full {
metrics::counter(name::CLIENT_SC_CACHE_EVICTIONS).inc(1);
}
}
async fn is_negative_cached(&self, block_id: i64) -> bool {
let mut neg = self.neg_cache_cell().lock().await;
if let Some(t) = neg.get(&block_id) {
if t.elapsed() < self.cfg.neg_cache_ttl {
metrics::counter(name::CLIENT_SC_NEG_CACHE_HITS).inc(1);
return true;
}
neg.pop(&block_id);
}
false
}
async fn acquire_worker(&self, block_id: i64) -> Result<crate::client::WorkerClient> {
let worker_info = self.router.select_worker(block_id).await?;
let addr = worker_info
.address
.as_ref()
.ok_or_else(|| Error::Internal {
message: "short-circuit: worker has no address".to_string(),
source: None,
})?;
let worker_addr = rpc_endpoint(addr);
debug!(block_id = block_id, worker = %worker_addr, "short-circuit acquiring local worker");
self.worker_pool.acquire(&worker_addr).await
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_cfg() -> ShortCircuitConfig {
ShortCircuitConfig {
enabled: true,
cache_capacity: 64,
cache_ttl: Duration::from_secs(30),
neg_cache_ttl: Duration::from_secs(5),
advise: AccessHint::Random,
prefetch_enabled: true,
prefetch_coalesce_gap: 64 * 1024,
prefetch_max_batch: 1024,
min_block_size: 0,
sigbus_handler: false,
thp: false,
}
}
#[test]
fn decision_kill_switch() {
let mut cfg = test_cfg();
cfg.enabled = false;
let ctx = ScDecisionCtx {
source_is_local: true,
process_sc_disabled: false,
negative_cached: false,
block_size: 1 << 20,
};
assert!(!should_use_short_circuit(&cfg, &ctx));
}
#[test]
fn decision_requires_local_source() {
let cfg = test_cfg();
let ctx = ScDecisionCtx {
source_is_local: false,
process_sc_disabled: false,
negative_cached: false,
block_size: 1 << 20,
};
assert!(!should_use_short_circuit(&cfg, &ctx));
}
#[test]
fn decision_sticky_disable_and_neg_cache() {
let cfg = test_cfg();
let base = ScDecisionCtx {
source_is_local: true,
process_sc_disabled: false,
negative_cached: false,
block_size: 1 << 20,
};
assert!(should_use_short_circuit(&cfg, &base));
let disabled = ScDecisionCtx {
process_sc_disabled: true,
..base
};
assert!(!should_use_short_circuit(&cfg, &disabled));
let neg = ScDecisionCtx {
negative_cached: true,
..base
};
assert!(!should_use_short_circuit(&cfg, &neg));
}
#[test]
fn decision_min_block_size() {
let mut cfg = test_cfg();
cfg.min_block_size = 2 * 1024 * 1024;
let small = ScDecisionCtx {
source_is_local: true,
process_sc_disabled: false,
negative_cached: false,
block_size: 1024,
};
assert!(!should_use_short_circuit(&cfg, &small));
let big = ScDecisionCtx {
block_size: 4 * 1024 * 1024,
..small
};
assert!(should_use_short_circuit(&cfg, &big));
}
#[test]
fn config_from_goosefs_config_defaults() {
let cfg = GoosefsConfig::new("127.0.0.1:9200");
let sc = ShortCircuitConfig::from_config(&cfg);
assert!(
!sc.enabled,
"SC must be OFF by default (P2-B) — flip on via env/storage-options/API to opt in"
);
let cfg_on = GoosefsConfig::new("127.0.0.1:9200").with_short_circuit_enabled(true);
let sc_on = ShortCircuitConfig::from_config(&cfg_on);
assert!(sc_on.enabled);
assert_eq!(sc_on.cache_capacity, 64);
assert_eq!(sc_on.cache_ttl, Duration::from_secs(30));
assert_eq!(sc_on.neg_cache_ttl, Duration::from_secs(5));
assert_eq!(sc_on.advise, AccessHint::Random);
assert!(sc_on.prefetch_enabled);
}
#[tokio::test]
async fn test_factory_caches_are_lazy_initialised() {
use crate::block::router::WorkerRouter;
use crate::client::WorkerClientPool;
let pool = WorkerClientPool::new_shared(GoosefsConfig::new("127.0.0.1:9200"));
let router = Arc::new(WorkerRouter::new());
let cfg = GoosefsConfig::new("127.0.0.1:9200").with_short_circuit_enabled(true);
let sc_cfg = ShortCircuitConfig::from_config(&cfg);
let factory = ShortCircuitFactory::new(pool, router, sc_cfg);
assert!(
factory.cache_is_uninitialised(),
"hot-block LRU must stay uninitialised on the happy path (no reads)"
);
assert!(
factory.neg_cache_is_uninitialised(),
"negative cache must stay uninitialised on the happy path (no reads)"
);
let _ = factory.should_use(1, 1024).await;
assert!(!factory.neg_cache_is_uninitialised());
assert!(
factory.cache_is_uninitialised(),
"hot-block LRU must stay uninitialised — should_use does not touch it"
);
}
}