use std::{
hash::{Hash, Hasher},
sync::Arc,
time::SystemTime,
};
use dashmap::DashMap;
use tokio::sync::OnceCell;
use super::Dicfuse;
use crate::util::config;
pub struct DicfuseManager;
static GLOBAL_DICFUSE: OnceCell<Arc<Dicfuse>> = OnceCell::const_new();
static DICFUSE_CACHE: OnceCell<DashMap<DicfuseCacheKey, Arc<OnceCell<Arc<Dicfuse>>>>> =
OnceCell::const_new();
#[derive(Debug, Clone, Eq)]
struct DicfuseCacheKey {
store_root: String,
base_path: String,
}
impl PartialEq for DicfuseCacheKey {
fn eq(&self, other: &Self) -> bool {
self.store_root == other.store_root && self.base_path == other.base_path
}
}
impl Hash for DicfuseCacheKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.store_root.hash(state);
self.base_path.hash(state);
}
}
fn normalize_base_path(base_path: &str) -> String {
if base_path.is_empty() || base_path == "/" {
"/".to_string()
} else {
base_path.trim_end_matches('/').to_string()
}
}
impl DicfuseManager {
pub async fn global() -> Arc<Dicfuse> {
GLOBAL_DICFUSE
.get_or_init(|| async {
let dicfuse = Arc::new(Dicfuse::new().await);
dicfuse.start_import();
dicfuse
})
.await
.clone()
}
pub async fn for_base_path(base_path: &str) -> Arc<Dicfuse> {
let store_root = config::store_path().to_string();
Self::for_base_path_with_store_root(base_path, &store_root).await
}
pub async fn for_base_path_with_store_root(base_path: &str, store_root: &str) -> Arc<Dicfuse> {
let normalized = normalize_base_path(base_path);
if normalized == "/" && store_root == config::store_path() {
return Self::global().await;
}
let cache = DICFUSE_CACHE.get_or_init(|| async { DashMap::new() }).await;
let key = DicfuseCacheKey {
store_root: store_root.to_string(),
base_path: normalized.clone(),
};
let cell = cache
.entry(key)
.or_insert_with(|| Arc::new(OnceCell::new()))
.clone();
cell.get_or_init(|| async move {
let store_path =
super::compute_store_dir_for_base_path_with_store_root(store_root, &normalized);
let _ = std::fs::create_dir_all(&store_path);
let dicfuse = Arc::new(
Dicfuse::new_with_base_path_and_store_path(&normalized, &store_path).await,
);
dicfuse.start_import();
dicfuse
})
.await
.clone()
}
#[allow(clippy::new_ret_no_self)]
pub async fn new() -> Arc<Dicfuse> {
let nanos = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let tmp_root = format!("/tmp/scorpio_dicfuse_isolated_{}", nanos);
let _ = std::fs::create_dir_all(&tmp_root);
Arc::new(Dicfuse::new_with_store_path(&tmp_root).await)
}
}
#[cfg(test)]
mod tests {
use serial_test::serial;
use super::*;
#[tokio::test]
#[serial] #[ignore = "Requires exclusive access to sled DB path; may fail locally if another scorpio/dicfuse process holds the lock"]
async fn test_global_returns_same_instance() {
let dic1 = DicfuseManager::global().await;
let dic2 = DicfuseManager::global().await;
assert!(Arc::ptr_eq(&dic1, &dic2));
}
#[tokio::test]
#[serial] #[ignore = "Requires exclusive access to sled DB path; may fail locally if another scorpio/dicfuse process holds the lock"]
async fn test_new_returns_different_instance() {
let global = DicfuseManager::global().await;
let isolated = DicfuseManager::new().await;
assert!(
!Arc::ptr_eq(&global, &isolated),
"new() should return a different instance than global()"
);
}
#[tokio::test]
#[serial] #[ignore = "Requires exclusive access to sled DB path; may fail locally if another scorpio/dicfuse process holds the lock"]
async fn test_concurrent_global_access() {
use tokio::task;
let handles: Vec<_> = (0..10)
.map(|_| task::spawn(async { DicfuseManager::global().await }))
.collect();
let results: Vec<_> = futures::future::join_all(handles).await;
let instances: Vec<_> = results.into_iter().map(|r| r.unwrap()).collect();
for i in 1..instances.len() {
assert!(Arc::ptr_eq(&instances[0], &instances[i]));
}
}
}