use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use super::versioned::ConfigVersion;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum OverrideValue {
String(String),
Int(i64),
UInt(u64),
Float(f64),
Bool(bool),
Duration(Duration),
}
impl OverrideValue {
pub const fn type_str(&self) -> &'static str {
match self {
OverrideValue::String(_) => "string",
OverrideValue::Int(_) => "int",
OverrideValue::UInt(_) => "uint",
OverrideValue::Float(_) => "float",
OverrideValue::Bool(_) => "bool",
OverrideValue::Duration(_) => "duration",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TenantConfigOverride {
pub tenant_id: String,
pub version: ConfigVersion,
pub overrides: BTreeMap<String, OverrideValue>,
}
impl TenantConfigOverride {
pub fn new(tenant_id: impl Into<String>, version: ConfigVersion) -> Self {
Self {
tenant_id: tenant_id.into(),
version,
overrides: BTreeMap::new(),
}
}
pub fn set(&mut self, key: impl Into<String>, value: OverrideValue) {
self.overrides.insert(key.into(), value);
}
pub fn unset(&mut self, key: &str) -> Option<OverrideValue> {
self.overrides.remove(key)
}
pub fn get(&self, key: &str) -> Option<&OverrideValue> {
self.overrides.get(key)
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.overrides.keys()
}
pub fn is_empty(&self) -> bool {
self.overrides.is_empty()
}
pub fn len(&self) -> usize {
self.overrides.len()
}
}
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum TenantConfigError {
#[error("tenant `{0}` has no overrides")]
NotFound(String),
#[error("stale version for tenant `{tenant}`: incoming `{incoming}`, observed `{observed}`")]
StaleVersion {
tenant: String,
incoming: ConfigVersion,
observed: ConfigVersion,
},
}
pub trait TenantConfigStore: Send + Sync {
fn upsert(&self, ov: TenantConfigOverride) -> Result<(), TenantConfigError>;
fn get(&self, tenant_id: &str) -> Option<TenantConfigOverride>;
fn list(&self) -> Vec<String>;
fn delete(&self, tenant_id: &str) -> bool;
}
#[derive(Default, Clone)]
pub struct InMemoryTenantConfigStore {
inner: Arc<RwLock<BTreeMap<String, TenantConfigOverride>>>,
}
impl InMemoryTenantConfigStore {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.inner.read().len()
}
pub fn is_empty(&self) -> bool {
self.inner.read().is_empty()
}
}
impl TenantConfigStore for InMemoryTenantConfigStore {
fn upsert(&self, ov: TenantConfigOverride) -> Result<(), TenantConfigError> {
let mut g = self.inner.write();
if let Some(existing) = g.get(&ov.tenant_id) {
if ov.version <= existing.version {
return Err(TenantConfigError::StaleVersion {
tenant: ov.tenant_id.clone(),
incoming: ov.version,
observed: existing.version,
});
}
}
g.insert(ov.tenant_id.clone(), ov);
Ok(())
}
fn get(&self, tenant_id: &str) -> Option<TenantConfigOverride> {
self.inner.read().get(tenant_id).cloned()
}
fn list(&self) -> Vec<String> {
self.inner.read().keys().cloned().collect()
}
fn delete(&self, tenant_id: &str) -> bool {
self.inner.write().remove(tenant_id).is_some()
}
}
pub fn resolve<'a>(overrides: &'a TenantConfigOverride, key: &str) -> Option<&'a OverrideValue> {
overrides.get(key)
}
pub fn resolve_opt<'a>(
bundle: Option<&'a TenantConfigOverride>,
key: &str,
) -> Option<&'a OverrideValue> {
bundle.and_then(|b| b.get(key))
}
#[cfg(test)]
mod tests {
use super::*;
fn ov_at(v: u64) -> TenantConfigOverride {
let mut o = TenantConfigOverride::new("acme", ConfigVersion(v));
o.set(
"admission.max_concurrent_expanded_recall",
OverrideValue::UInt(8),
);
o
}
#[test]
fn override_value_type_str_pinned() {
assert_eq!(OverrideValue::String("x".into()).type_str(), "string");
assert_eq!(OverrideValue::Int(0).type_str(), "int");
assert_eq!(OverrideValue::UInt(0).type_str(), "uint");
assert_eq!(OverrideValue::Float(0.0).type_str(), "float");
assert_eq!(OverrideValue::Bool(true).type_str(), "bool");
assert_eq!(
OverrideValue::Duration(Duration::ZERO).type_str(),
"duration"
);
}
#[test]
fn override_set_get_round_trip() {
let mut o = TenantConfigOverride::new("acme", ConfigVersion(1));
o.set("k", OverrideValue::Int(42));
assert_eq!(o.get("k"), Some(&OverrideValue::Int(42)));
}
#[test]
fn override_unset_removes_and_returns_old() {
let mut o = TenantConfigOverride::new("acme", ConfigVersion(1));
o.set("k", OverrideValue::Int(42));
assert_eq!(o.unset("k"), Some(OverrideValue::Int(42)));
assert!(o.get("k").is_none());
}
#[test]
fn override_keys_iterate_in_sorted_order() {
let mut o = TenantConfigOverride::new("acme", ConfigVersion(1));
o.set("zzz", OverrideValue::Bool(true));
o.set("aaa", OverrideValue::Bool(false));
o.set("mmm", OverrideValue::Bool(true));
let keys: Vec<&String> = o.keys().collect();
assert_eq!(keys, vec!["aaa", "mmm", "zzz"]);
}
#[test]
fn store_upsert_first_writes_through() {
let s = InMemoryTenantConfigStore::new();
s.upsert(ov_at(1)).unwrap();
let got = s.get("acme").unwrap();
assert_eq!(got.version, ConfigVersion(1));
}
#[test]
fn store_upsert_advances_version() {
let s = InMemoryTenantConfigStore::new();
s.upsert(ov_at(1)).unwrap();
s.upsert(ov_at(2)).unwrap();
assert_eq!(s.get("acme").unwrap().version, ConfigVersion(2));
}
#[test]
fn store_rejects_stale_version() {
let s = InMemoryTenantConfigStore::new();
s.upsert(ov_at(2)).unwrap();
let err = s.upsert(ov_at(1)).unwrap_err();
assert!(matches!(err, TenantConfigError::StaleVersion { .. }));
let err2 = s.upsert(ov_at(2)).unwrap_err();
assert!(matches!(err2, TenantConfigError::StaleVersion { .. }));
}
#[test]
fn store_list_returns_all_tenants_sorted() {
let s = InMemoryTenantConfigStore::new();
s.upsert(TenantConfigOverride::new("zzz", ConfigVersion(1)))
.unwrap();
s.upsert(TenantConfigOverride::new("aaa", ConfigVersion(1)))
.unwrap();
s.upsert(TenantConfigOverride::new("mmm", ConfigVersion(1)))
.unwrap();
let listed = s.list();
assert_eq!(listed, vec!["aaa".to_string(), "mmm".into(), "zzz".into()]);
}
#[test]
fn store_delete_returns_existed_flag() {
let s = InMemoryTenantConfigStore::new();
s.upsert(ov_at(1)).unwrap();
assert!(s.delete("acme"));
assert!(!s.delete("acme")); }
#[test]
fn store_get_unknown_returns_none() {
let s = InMemoryTenantConfigStore::new();
assert!(s.get("ghost").is_none());
}
#[test]
fn resolve_returns_set_value() {
let mut o = TenantConfigOverride::new("acme", ConfigVersion(1));
o.set("k", OverrideValue::Int(42));
let v = resolve(&o, "k");
assert_eq!(v, Some(&OverrideValue::Int(42)));
}
#[test]
fn resolve_returns_none_for_unset_key() {
let o = TenantConfigOverride::new("acme", ConfigVersion(1));
assert!(resolve(&o, "unset_key").is_none());
}
#[test]
fn resolve_opt_handles_missing_bundle() {
let v: Option<&OverrideValue> = resolve_opt(None, "k");
assert!(v.is_none());
let o = TenantConfigOverride::new("acme", ConfigVersion(1));
assert!(resolve_opt(Some(&o), "k").is_none());
}
#[test]
fn store_dyn_dispatch() {
let store: Arc<dyn TenantConfigStore> = Arc::new(InMemoryTenantConfigStore::new());
store.upsert(ov_at(1)).unwrap();
assert!(store.get("acme").is_some());
}
#[test]
fn override_versions_are_independent_per_tenant() {
let s = InMemoryTenantConfigStore::new();
let mut a = TenantConfigOverride::new("a", ConfigVersion(5));
a.set("k", OverrideValue::Int(1));
let mut b = TenantConfigOverride::new("b", ConfigVersion(1));
b.set("k", OverrideValue::Int(2));
s.upsert(a).unwrap();
s.upsert(b).unwrap();
assert_eq!(s.get("a").unwrap().version, ConfigVersion(5));
assert_eq!(s.get("b").unwrap().version, ConfigVersion(1));
}
#[test]
fn override_serialization_round_trip() {
let mut o = TenantConfigOverride::new("acme", ConfigVersion(7));
o.set("k1", OverrideValue::Int(42));
o.set("k2", OverrideValue::Bool(true));
o.set("k3", OverrideValue::Duration(Duration::from_secs(60)));
let json = serde_json::to_string(&o).unwrap();
let back: TenantConfigOverride = serde_json::from_str(&json).unwrap();
assert_eq!(o, back);
}
}