use std::collections::HashMap;
use nodedb_mem::EngineId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct EngineConfig {
pub vector_budget_fraction: f64,
pub graph_budget_fraction: f64,
pub document_schemaless_budget_fraction: f64,
pub document_strict_budget_fraction: f64,
pub kv_budget_fraction: f64,
pub columnar_budget_fraction: f64,
pub timeseries_budget_fraction: f64,
pub spatial_budget_fraction: f64,
pub array_budget_fraction: f64,
pub fts_budget_fraction: f64,
pub sparse_budget_fraction: f64,
pub crdt_budget_fraction: f64,
pub query_budget_fraction: f64,
pub wal_budget_fraction: f64,
pub bridge_budget_fraction: f64,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
vector_budget_fraction: 0.25,
graph_budget_fraction: 0.02,
document_schemaless_budget_fraction: 0.02,
document_strict_budget_fraction: 0.02,
kv_budget_fraction: 0.02,
columnar_budget_fraction: 0.03,
timeseries_budget_fraction: 0.09,
spatial_budget_fraction: 0.02,
array_budget_fraction: 0.02,
fts_budget_fraction: 0.03,
sparse_budget_fraction: 0.14,
crdt_budget_fraction: 0.09,
query_budget_fraction: 0.16,
wal_budget_fraction: 0.02,
bridge_budget_fraction: 0.01,
}
}
}
impl EngineConfig {
fn fraction_for(&self, engine: EngineId) -> f64 {
match engine {
EngineId::Vector => self.vector_budget_fraction,
EngineId::Graph => self.graph_budget_fraction,
EngineId::DocumentSchemaless => self.document_schemaless_budget_fraction,
EngineId::DocumentStrict => self.document_strict_budget_fraction,
EngineId::Kv => self.kv_budget_fraction,
EngineId::Columnar => self.columnar_budget_fraction,
EngineId::Timeseries => self.timeseries_budget_fraction,
EngineId::Spatial => self.spatial_budget_fraction,
EngineId::Array => self.array_budget_fraction,
EngineId::Fts => self.fts_budget_fraction,
EngineId::Sparse => self.sparse_budget_fraction,
EngineId::Crdt => self.crdt_budget_fraction,
EngineId::Query => self.query_budget_fraction,
EngineId::Wal => self.wal_budget_fraction,
EngineId::Bridge => self.bridge_budget_fraction,
}
}
pub fn total_fraction(&self) -> f64 {
EngineId::ALL.iter().map(|&e| self.fraction_for(e)).sum()
}
pub fn validate(&self) -> crate::Result<()> {
for &engine in EngineId::ALL {
let f = self.fraction_for(engine);
if !f.is_finite() || f <= 0.0 {
return Err(crate::Error::Config {
detail: format!(
"engine budget fraction for {engine} must be a positive finite \
number, got {f}"
),
});
}
}
let total = self.total_fraction();
if total > 1.0 {
return Err(crate::Error::Config {
detail: format!("engine budget fractions sum to {total:.3}, must be <= 1.0"),
});
}
Ok(())
}
pub fn to_byte_budgets(&self, global_limit: usize) -> EngineByteBudgets {
let gl = global_limit as f64;
let per_engine = EngineId::ALL
.iter()
.map(|&e| (e, (gl * self.fraction_for(e)) as usize))
.collect();
EngineByteBudgets { per_engine }
}
}
#[derive(Debug, Clone)]
pub struct EngineByteBudgets {
per_engine: HashMap<EngineId, usize>,
}
impl EngineByteBudgets {
pub fn from_map(per_engine: HashMap<EngineId, usize>) -> Self {
Self { per_engine }
}
pub fn get(&self, engine: EngineId) -> usize {
self.per_engine.get(&engine).copied().unwrap_or(0)
}
pub fn total(&self) -> usize {
self.per_engine.values().copied().sum()
}
pub fn into_engine_limits(self) -> HashMap<EngineId, usize> {
self.per_engine
}
pub fn as_engine_limits(&self) -> &HashMap<EngineId, usize> {
&self.per_engine
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_fractions_valid() {
let cfg = EngineConfig::default();
cfg.validate().unwrap();
assert!(cfg.total_fraction() <= 1.0);
}
#[test]
fn default_covers_every_engine_with_a_positive_fraction() {
let cfg = EngineConfig::default();
for &engine in EngineId::ALL {
assert!(
cfg.fraction_for(engine) > 0.0,
"{engine} has a non-positive default budget fraction"
);
}
}
#[test]
fn over_budget_rejected() {
let cfg = EngineConfig {
vector_budget_fraction: 0.90,
..EngineConfig::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn negative_budget_rejected() {
let cfg = EngineConfig {
crdt_budget_fraction: -0.1,
..EngineConfig::default()
};
assert!(cfg.validate().is_err());
}
#[test]
fn zero_budget_rejected() {
let cfg = EngineConfig {
kv_budget_fraction: 0.0,
..EngineConfig::default()
};
assert!(
cfg.validate().is_err(),
"a zero budget would put the engine at Emergency pressure"
);
}
#[test]
fn byte_budgets_cover_every_engine() {
let cfg = EngineConfig::default();
let budgets = cfg.to_byte_budgets(1024 * 1024 * 1024); for &engine in EngineId::ALL {
assert!(budgets.get(engine) > 0, "{engine} has a zero byte budget");
}
let one_gib = 1024.0 * 1024.0 * 1024.0;
assert_eq!(
budgets.get(EngineId::Vector),
(one_gib * cfg.vector_budget_fraction) as usize
);
assert!(budgets.total() <= 1024 * 1024 * 1024);
}
#[test]
fn missing_toml_keys_fall_back_to_defaults() {
let cfg: EngineConfig = toml::from_str("").unwrap();
cfg.validate().unwrap();
let defaults = EngineConfig::default();
for &engine in EngineId::ALL {
assert_eq!(cfg.fraction_for(engine), defaults.fraction_for(engine));
}
}
}