use crate::common::{
VSDB,
engine::{Engine, EngineSizing, write_file_durable},
error::{Result, VsdbError},
vsdb_freeze_base_dir, vsdb_get_base_dir,
};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::{
cell::RefCell,
collections::HashMap,
fmt, fs,
path::{Component, Path, PathBuf},
str::FromStr,
sync::{Arc, LazyLock},
time::{SystemTime, UNIX_EPOCH},
};
pub type NsId = u64;
pub const DEFAULT_NS_ID: NsId = 0;
const NS_REGISTRY_REL_PATH: &str = "__SYSTEM__/__namespaces__";
const NS_DERIVED_DIR: &str = "__NAMESPACES__";
const DEFAULT_NS_SHARDS: usize = 4;
const DEFAULT_NS_BUDGET_MB: usize = 512;
#[derive(
Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct InstanceId {
pub map_id: u64,
pub ns: Option<NsId>,
}
impl From<u64> for InstanceId {
fn from(map_id: u64) -> Self {
Self { map_id, ns: None }
}
}
impl fmt::Display for InstanceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.ns {
None => write!(f, "{}", self.map_id),
Some(ns) => write!(f, "{}@{}", self.map_id, ns),
}
}
}
impl FromStr for InstanceId {
type Err = VsdbError;
fn from_str(s: &str) -> Result<Self> {
let parse = |v: &str, what: &str| {
v.parse::<u64>().map_err(|_| VsdbError::Decode {
detail: format!("invalid InstanceId {what}: {v:?}"),
})
};
match s.split_once('@') {
None => Ok(Self {
map_id: parse(s, "map_id")?,
ns: None,
}),
Some((m, n)) => Ok(Self {
map_id: parse(m, "map_id")?,
ns: Some(parse(n, "ns_id")?),
}),
}
}
}
#[derive(Clone, Debug)]
pub struct NamespaceOpts {
pub path: Option<PathBuf>,
pub shards: usize,
pub mem_budget_mb: Option<usize>,
}
impl Default for NamespaceOpts {
fn default() -> Self {
Self {
path: None,
shards: DEFAULT_NS_SHARDS,
mem_budget_mb: None,
}
}
}
#[derive(Clone, Debug)]
pub struct NsInfo {
pub id: NsId,
pub path: PathBuf,
pub pinned: bool,
pub shards: usize,
pub created_at: u64,
}
#[derive(Serialize, Deserialize, Clone)]
struct NsRecord {
id: NsId,
path: Option<String>,
shards: u32,
mem_budget_mb: Option<u64>,
created_at: u64,
}
#[derive(Serialize, Deserialize)]
struct RegistryFile {
next_id: NsId,
entries: Vec<NsRecord>,
}
impl Default for RegistryFile {
fn default() -> Self {
Self {
next_id: 1,
entries: Vec::new(),
}
}
}
static REGISTRY_LOCK: Mutex<()> = Mutex::new(());
static OPEN_NAMESPACES: LazyLock<Mutex<HashMap<NsId, Namespace>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static DEFAULT_NS: LazyLock<Namespace> = LazyLock::new(|| {
Namespace(Arc::new(NsInner {
id: DEFAULT_NS_ID,
path: vsdb_get_base_dir(),
engine: &LazyLock::force(&VSDB).db,
}))
});
thread_local! {
static NS_STACK: RefCell<Vec<Namespace>> = const { RefCell::new(Vec::new()) };
}
fn registry_path() -> PathBuf {
vsdb_get_base_dir().join(NS_REGISTRY_REL_PATH)
}
fn load_registry() -> Result<RegistryFile> {
match fs::read(registry_path()) {
Ok(bytes) => Ok(postcard::from_bytes(&bytes)?),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Ok(RegistryFile::default())
}
Err(e) => Err(e.into()),
}
}
fn save_registry(reg: &RegistryFile) -> Result<()> {
let path = registry_path();
fs::create_dir_all(path.parent().expect("has parent"))?;
let bytes = postcard::to_allocvec(reg)?;
write_file_durable(&path, &bytes).map_err(VsdbError::from)
}
fn resolve_root(base: &Path, rec: &NsRecord) -> PathBuf {
match &rec.path {
Some(p) => PathBuf::from(p),
None => base.join(NS_DERIVED_DIR).join(format!("{:016x}", rec.id)),
}
}
fn path_contains(outer: &Path, inner: &Path) -> bool {
let o: Vec<Component<'_>> = outer.components().collect();
let i: Vec<Component<'_>> = inner.components().collect();
i.len() >= o.len() && i[..o.len()] == o[..]
}
fn ns_err(detail: impl Into<String>) -> VsdbError {
VsdbError::Namespace {
detail: detail.into(),
}
}
fn validate_explicit_root(
base: &Path,
reg: &RegistryFile,
candidate: &Path,
) -> Result<()> {
if !candidate.is_absolute() {
return Err(ns_err(format!(
"namespace path must be absolute: {}",
candidate.display()
)));
}
if candidate.as_os_str().to_str().is_none() {
return Err(ns_err(format!(
"namespace path must be valid UTF-8: {}",
candidate.display()
)));
}
if path_contains(base, candidate) || path_contains(candidate, base) {
return Err(ns_err(format!(
"namespace path {} overlaps the default base dir {}",
candidate.display(),
base.display()
)));
}
for rec in ®.entries {
let other = resolve_root(base, rec);
if path_contains(&other, candidate) || path_contains(candidate, &other) {
return Err(ns_err(format!(
"namespace path {} overlaps namespace {}'s root {}",
candidate.display(),
rec.id,
other.display()
)));
}
}
Ok(())
}
fn sizing_for(mem_budget_mb: Option<usize>) -> EngineSizing {
EngineSizing::from_budget_mb(mem_budget_mb.unwrap_or(DEFAULT_NS_BUDGET_MB))
}
struct NsInner {
id: NsId,
path: PathBuf,
engine: &'static Engine,
}
#[derive(Clone)]
pub struct Namespace(Arc<NsInner>);
impl fmt::Debug for Namespace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Namespace")
.field("id", &self.0.id)
.field("path", &self.0.path)
.finish()
}
}
impl Namespace {
pub fn default_ns() -> Namespace {
DEFAULT_NS.clone()
}
pub fn create() -> Result<Namespace> {
Self::create_with(NamespaceOpts::default())
}
pub fn create_with(opts: NamespaceOpts) -> Result<Namespace> {
vsdb_freeze_base_dir();
let base = vsdb_get_base_dir();
let shards = opts.shards.clamp(1, 64);
let _g = REGISTRY_LOCK.lock();
let mut reg = load_registry()?;
if let Some(p) = &opts.path {
validate_explicit_root(&base, ®, p)?;
}
let id = reg.next_id;
let rec = NsRecord {
id,
path: opts
.path
.as_ref()
.map(|p| p.to_str().expect("validated UTF-8").to_owned()),
shards: shards as u32,
mem_budget_mb: opts.mem_budget_mb.map(|v| v as u64),
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
};
let root = resolve_root(&base, &rec);
reg.next_id += 1;
reg.entries.push(rec.clone());
save_registry(®)?;
match open_record_locked(&base, &rec, &root) {
Ok(ns) => Ok(ns),
Err(e) => {
reg.entries.retain(|r| r.id != rec.id);
let _ = save_registry(®);
Err(e)
}
}
}
pub fn open(id: NsId) -> Result<Namespace> {
if id == DEFAULT_NS_ID {
return Ok(Self::default_ns());
}
if let Some(ns) = OPEN_NAMESPACES.lock().get(&id) {
return Ok(ns.clone());
}
let base = vsdb_get_base_dir();
let _g = REGISTRY_LOCK.lock();
if let Some(ns) = OPEN_NAMESPACES.lock().get(&id) {
return Ok(ns.clone());
}
let reg = load_registry()?;
let rec = reg.entries.iter().find(|r| r.id == id).ok_or_else(|| {
ns_err(format!(
"namespace {id} is not registered (destroyed, or from \
another universe)"
))
})?;
let root = resolve_root(&base, rec);
open_record_locked(&base, rec, &root)
}
pub fn scope<R>(&self, f: impl FnOnce() -> R) -> R {
struct PopGuard;
impl Drop for PopGuard {
fn drop(&mut self) {
NS_STACK.with(|s| {
s.borrow_mut().pop();
});
}
}
NS_STACK.with(|s| s.borrow_mut().push(self.clone()));
let _guard = PopGuard;
f()
}
pub fn current() -> Namespace {
NS_STACK
.with(|s| s.borrow().last().cloned())
.unwrap_or_else(Self::default_ns)
}
pub fn id(&self) -> NsId {
self.0.id
}
pub fn path(&self) -> &Path {
&self.0.path
}
pub fn system_dir(&self) -> PathBuf {
self.0.path.join("__SYSTEM__")
}
pub fn meta_dir(&self) -> PathBuf {
self.system_dir().join("__instance_meta__")
}
pub(crate) fn meta_path(&self, map_id: u64) -> PathBuf {
let mut p = self.meta_dir();
p.push(format!("{:016x}", map_id));
p
}
pub fn flush(&self) {
self.0.engine.flush()
}
#[inline(always)]
pub(crate) fn engine(&self) -> &'static Engine {
self.0.engine
}
}
fn open_record_locked(_base: &Path, rec: &NsRecord, root: &Path) -> Result<Namespace> {
let sizing = sizing_for(rec.mem_budget_mb.map(|v| v as usize));
let engine =
Engine::open_at(root, rec.shards as usize, sizing).map_err(VsdbError::from)?;
let ns = Namespace(Arc::new(NsInner {
id: rec.id,
path: root.to_path_buf(),
engine: Box::leak(Box::new(engine)),
}));
OPEN_NAMESPACES.lock().insert(rec.id, ns.clone());
Ok(ns)
}
pub fn vsdb_ns_list() -> Result<Vec<NsInfo>> {
let base = vsdb_get_base_dir();
let _g = REGISTRY_LOCK.lock();
let reg = load_registry()?;
Ok(reg
.entries
.iter()
.map(|rec| NsInfo {
id: rec.id,
path: resolve_root(&base, rec),
pinned: rec.path.is_some(),
shards: rec.shards as usize,
created_at: rec.created_at,
})
.collect())
}
pub fn vsdb_ns_destroy(id: NsId) -> Result<()> {
if id == DEFAULT_NS_ID {
return Err(ns_err("the default namespace cannot be destroyed"));
}
if OPEN_NAMESPACES.lock().contains_key(&id) {
return Err(ns_err(format!(
"namespace {id} is open in this process; destroy requires a \
not-open target"
)));
}
let base = vsdb_get_base_dir();
let _g = REGISTRY_LOCK.lock();
let mut reg = load_registry()?;
let Some(pos) = reg.entries.iter().position(|r| r.id == id) else {
return Err(ns_err(format!("namespace {id} is not registered")));
};
let root = resolve_root(&base, ®.entries[pos]);
reg.entries.remove(pos);
save_registry(®)?;
match fs::remove_dir_all(&root) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e.into()),
}
}
pub fn vsdb_ns_relocate(id: NsId, new_path: impl AsRef<Path>) -> Result<()> {
if id == DEFAULT_NS_ID {
return Err(ns_err(
"the default namespace's root is the base dir; relocate it \
via VSDB_BASE_DIR / vsdb_set_base_dir before first use",
));
}
if OPEN_NAMESPACES.lock().contains_key(&id) {
return Err(ns_err(format!(
"namespace {id} is open in this process; relocate requires a \
not-open target"
)));
}
let base = vsdb_get_base_dir();
let new_path = new_path.as_ref();
let _g = REGISTRY_LOCK.lock();
let mut reg = load_registry()?;
if !reg.entries.iter().any(|r| r.id == id) {
return Err(ns_err(format!("namespace {id} is not registered")));
}
let mut probe = reg.clone_without(id);
validate_explicit_root(&base, &probe, new_path)?;
drop(probe.entries.drain(..));
let rec = reg
.entries
.iter_mut()
.find(|r| r.id == id)
.expect("checked above");
rec.path = Some(
new_path
.to_str()
.expect("validated UTF-8 in validate_explicit_root")
.to_owned(),
);
save_registry(®)
}
impl RegistryFile {
fn clone_without(&self, id: NsId) -> RegistryFile {
RegistryFile {
next_id: self.next_id,
entries: self
.entries
.iter()
.filter(|r| r.id != id)
.cloned()
.collect(),
}
}
}
pub(crate) fn flush_all_open() {
for ns in OPEN_NAMESPACES.lock().values() {
ns.flush();
}
}