use crate::registry::SliceSet;
use zenkey::grammar::{self, BlobTier, Class, ClassOrPlane, Origin, Plane, StructuralKey};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyFacts {
pub shape: KeyShape,
pub registration: Registration,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyShape {
V1(Box<V1Facts>),
NotUnderBase,
Unparsed { reason: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct V1Facts {
pub origin: String,
pub origin_kind: OriginKind,
pub class: String,
pub class_kind: ClassKind,
pub producer: Option<String>,
pub instance: Option<u32>,
pub blob_tier: Option<String>,
pub subject: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OriginKind {
Host,
Service,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClassKind {
Telemetry,
State,
Events,
Rpc,
Media,
Blob,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Registration {
Unknown,
NoSliceForProducer,
Unregistered,
Registered(Box<SubjectFacts>),
NotApplicable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubjectFacts {
pub path: String,
pub type_name: String,
pub vars: Vec<(String, String)>,
pub unit: Option<String>,
pub qos: Option<String>,
pub encoding: Option<String>,
pub ttl_s: Option<i64>,
}
impl KeyFacts {
pub fn project(base: &str, wire_key: &str) -> KeyFacts {
let Some(relative) = grammar::strip_base(base, wire_key) else {
return KeyFacts {
shape: KeyShape::NotUnderBase,
registration: Registration::NotApplicable,
};
};
match grammar::parse(relative) {
Ok(parsed) => {
let facts = V1Facts::from_parsed(&parsed);
let registration = if facts.class_kind.is_data_class() {
Registration::Unknown
} else {
Registration::NotApplicable
};
KeyFacts {
shape: KeyShape::V1(Box::new(facts)),
registration,
}
}
Err(e) => KeyFacts {
shape: KeyShape::Unparsed {
reason: e.to_string(),
},
registration: Registration::NotApplicable,
},
}
}
pub fn resolve(&mut self, slices: &SliceSet) {
let KeyShape::V1(facts) = &self.shape else {
return;
};
if !facts.class_kind.is_data_class() {
return;
}
let producer = match facts.origin_kind {
OriginKind::Host => facts.producer.clone(),
OriginKind::Service => slices
.by_service_origin(&facts.origin)
.map(|s| s.name.clone()),
};
let Some(producer) = producer else {
self.registration = Registration::NoSliceForProducer;
return;
};
if slices.get(&producer).is_none() {
self.registration = Registration::NoSliceForProducer;
return;
}
let tail: Vec<&str> = facts.subject.iter().map(String::as_str).collect();
self.registration = match slices.refine(&producer, &facts.class, &tail) {
Some((decl, vars)) => Registration::Registered(Box::new(SubjectFacts {
path: decl.path.clone(),
type_name: decl.type_name.clone(),
vars,
unit: decl.unit.clone(),
qos: decl.qos.clone(),
encoding: decl.encoding.clone(),
ttl_s: decl.ttl_s,
})),
None => Registration::Unregistered,
};
}
pub fn type_name(&self) -> Option<&str> {
match &self.registration {
Registration::Registered(s) => Some(&s.type_name),
_ => None,
}
}
}
const EVICT_FRACTION: usize = 16;
struct Entry {
facts: KeyFacts,
seen: u64,
}
impl std::fmt::Debug for Entry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Entry").field("seen", &self.seen).finish()
}
}
#[derive(Debug)]
pub struct FactsCache {
entries: std::collections::HashMap<String, Entry>,
max_keys: usize,
inserted: u64,
evicted: u64,
seq: u64,
}
impl Default for FactsCache {
fn default() -> Self {
FactsCache::with_capacity(crate::stats::DEFAULT_MAX_KEYS)
}
}
impl FactsCache {
pub fn with_capacity(max_keys: usize) -> FactsCache {
FactsCache {
entries: std::collections::HashMap::new(),
max_keys: max_keys.max(1),
inserted: 0,
evicted: 0,
seq: 0,
}
}
pub fn ensure(&mut self, base: &str, key: &str, slices: Option<&SliceSet>) {
self.seq += 1;
let seq = self.seq;
if let Some(entry) = self.entries.get_mut(key) {
entry.seen = seq;
return;
}
if self.entries.len() >= self.max_keys {
self.evict();
}
let mut facts = KeyFacts::project(base, key);
if let Some(slices) = slices {
facts.resolve(slices);
}
self.entries
.insert(key.to_string(), Entry { facts, seen: seq });
self.inserted += 1;
}
pub fn get(&self, key: &str) -> Option<&KeyFacts> {
self.entries.get(key).map(|e| &e.facts)
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.entries.keys().map(String::as_str)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn max_keys(&self) -> usize {
self.max_keys
}
pub fn evicted(&self) -> u64 {
self.evicted
}
pub fn inserted(&self) -> u64 {
self.inserted
}
pub fn resolve_all(&mut self, slices: &SliceSet) {
for entry in self.entries.values_mut() {
entry.facts.resolve(slices);
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.inserted = 0;
self.evicted = 0;
self.seq = 0;
}
fn evict(&mut self) {
let target = self.max_keys - (self.max_keys / EVICT_FRACTION).max(1);
let mut seen: Vec<(u64, String)> = self
.entries
.iter()
.map(|(k, e)| (e.seen, k.clone()))
.collect();
seen.sort_unstable_by_key(|(seen, _)| *seen);
for (_, key) in seen.into_iter().take(self.entries.len() - target) {
self.entries.remove(&key);
self.evicted += 1;
}
}
}
impl V1Facts {
fn from_parsed(parsed: &StructuralKey<'_>) -> V1Facts {
let (origin, origin_kind) = match &parsed.origin {
Origin::Host(id) => (id.as_str().to_string(), OriginKind::Host),
Origin::Service(s) => (s.clone(), OriginKind::Service),
};
let (class, class_kind) = match parsed.class {
ClassOrPlane::Class(c) => (c.chunk().to_string(), ClassKind::from_class(c)),
ClassOrPlane::Plane(p) => (p.chunk().to_string(), ClassKind::from_plane(p)),
};
V1Facts {
origin,
origin_kind,
class,
class_kind,
producer: parsed.producer.as_ref().map(|p| p.name().to_string()),
instance: parsed.producer.as_ref().and_then(|p| p.instance()),
blob_tier: parsed.blob_tier.map(|t| tier_chunk(t).to_string()),
subject: parsed.subject.iter().map(|s| (*s).to_string()).collect(),
}
}
}
fn tier_chunk(tier: BlobTier) -> &'static str {
tier.chunk()
}
impl ClassKind {
fn from_class(c: Class) -> ClassKind {
match c {
Class::Telemetry => ClassKind::Telemetry,
Class::State => ClassKind::State,
Class::Events => ClassKind::Events,
}
}
fn from_plane(p: Plane) -> ClassKind {
match p {
Plane::Rpc => ClassKind::Rpc,
Plane::Media => ClassKind::Media,
Plane::Blob => ClassKind::Blob,
}
}
pub fn is_data_class(self) -> bool {
matches!(
self,
ClassKind::Telemetry | ClassKind::State | ClassKind::Events
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyDescription {
pub key: String,
pub facts: KeyFacts,
}
pub fn describe_key(base: &str, key: &str, slices: Option<&SliceSet>) -> KeyDescription {
let mut facts = KeyFacts::project(base, key);
if let Some(slices) = slices {
facts.resolve(slices);
}
KeyDescription {
key: key.to_string(),
facts,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v1(facts: &KeyFacts) -> &V1Facts {
match &facts.shape {
KeyShape::V1(f) => f,
other => panic!("expected a v1 key, got {other:?}"),
}
}
#[test]
fn projects_a_host_telemetry_key() {
let f = KeyFacts::project(
"zensight",
"zensight/v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu/usage",
);
let v = v1(&f);
assert_eq!(v.origin, "h-3fa9c2d41b7e");
assert_eq!(v.origin_kind, OriginKind::Host);
assert_eq!(v.class, "telemetry");
assert_eq!(v.producer.as_deref(), Some("sysinfo"));
assert_eq!(v.instance, None);
assert_eq!(v.subject, ["cpu", "usage"]);
assert_eq!(f.registration, Registration::Unknown);
}
#[test]
fn positions_are_base_relative_never_absolute() {
let subject = "v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu/usage";
let cases = [
("", subject.to_string()),
("zensight", format!("zensight/{subject}")),
("acme/fleet-a", format!("acme/fleet-a/{subject}")),
];
let projected: Vec<V1Facts> = cases
.iter()
.map(|(base, key)| v1(&KeyFacts::project(base, key)).clone())
.collect();
assert_eq!(projected[0], projected[1]);
assert_eq!(projected[1], projected[2]);
assert_eq!(projected[0].producer.as_deref(), Some("sysinfo"));
}
#[test]
fn origin_chunk_alone_decides_whether_chunk_five_is_a_producer() {
let host = KeyFacts::project("", "v1/h-3fa9c2d41b7e/state/sysinfo/health");
assert_eq!(v1(&host).producer.as_deref(), Some("sysinfo"));
assert_eq!(v1(&host).subject, ["health"]);
let service = KeyFacts::project("", "v1/@catalog/state/entity/x");
assert_eq!(v1(&service).origin_kind, OriginKind::Service);
assert_eq!(v1(&service).origin, "@catalog");
assert_eq!(v1(&service).producer, None);
assert_eq!(v1(&service).subject, ["entity", "x"]);
}
#[test]
fn parses_a_producer_instance_suffix() {
let f = KeyFacts::project("", "v1/h-3fa9c2d41b7e/telemetry/snmp-2/if/eth0/in");
assert_eq!(v1(&f).producer.as_deref(), Some("snmp"));
assert_eq!(v1(&f).instance, Some(2));
}
#[test]
fn blob_tier_occupies_the_producer_position() {
let f = KeyFacts::project("", "v1/h-3fa9c2d41b7e/@blob/store/sha256/abcdef01");
let v = v1(&f);
assert_eq!(v.class_kind, ClassKind::Blob);
assert_eq!(v.producer, None);
assert_eq!(v.blob_tier.as_deref(), Some("store"));
assert_eq!(f.registration, Registration::NotApplicable);
}
#[test]
fn a_key_under_another_base_is_a_fact_not_an_error() {
let f = KeyFacts::project("zensight", "other/v1/h-3fa9c2d41b7e/state/sysinfo/health");
assert_eq!(f.shape, KeyShape::NotUnderBase);
}
#[test]
fn empty_base_makes_not_under_base_unreachable() {
for key in [
"v1/h-3fa9c2d41b7e/state/sysinfo/health",
"zensight/v1/h-3fa9c2d41b7e/state/sysinfo/health",
"demo/example/foo",
"",
] {
assert_ne!(
KeyFacts::project("", key).shape,
KeyShape::NotUnderBase,
"{key}"
);
}
}
#[test]
fn arbitrary_keys_degrade_to_a_stated_reason() {
for key in ["demo/example/foo", "v2/h-3fa9c2d41b7e/state/x/y", "a", ""] {
let f = KeyFacts::project("", key);
match f.shape {
KeyShape::Unparsed { reason } => assert!(!reason.is_empty(), "{key}"),
other => panic!("{key} should be unparsed, got {other:?}"),
}
assert_eq!(f.registration, Registration::NotApplicable);
}
}
#[test]
fn foreign_keys_with_verbatim_chunks_are_merely_unparsed() {
let f = KeyFacts::project("", "demo/@thing/foo");
assert!(matches!(f.shape, KeyShape::Unparsed { .. }));
}
#[test]
fn unknown_registration_is_not_unregistered() {
assert_ne!(Registration::Unknown, Registration::Unregistered);
}
#[test]
fn describe_key_prefers_the_literal_over_the_variable() {
use zenkey::slice::{RegistrySlice, SubjectDecl};
let subject = |path: &str| SubjectDecl {
path: path.to_string(),
class: "telemetry".to_string(),
type_name: if path.contains('{') {
"VarPoint"
} else {
"SpecialPoint"
}
.to_string(),
common: None,
since: None,
description: None,
qos: None,
ttl_s: None,
unit: None,
rate: None,
cardinality: None,
encoding: None,
};
let slice = RegistrySlice {
version: "1.0".into(),
app: "test".into(),
convention: 1,
name: "flowd".into(),
service_origin: None,
description: None,
subjects: vec![subject("flow/{q}"), subject("flow/special")],
procedures: vec![],
blob: vec![],
media: vec![],
deprecated: vec![],
};
let slices = SliceSet::from_slices(vec![slice]);
let d = describe_key(
"",
"v1/h-3fa9c2d41b7e/telemetry/flowd/flow/special",
Some(&slices),
);
match &d.facts.registration {
Registration::Registered(s) => {
assert_eq!(s.path, "flow/special", "literal must beat {{var}}");
assert_eq!(s.type_name, "SpecialPoint");
}
other => panic!("expected Registered, got {other:?}"),
}
let d = describe_key(
"",
"v1/h-3fa9c2d41b7e/telemetry/flowd/flow/p95",
Some(&slices),
);
match &d.facts.registration {
Registration::Registered(s) => assert_eq!(s.path, "flow/{q}"),
other => panic!("expected Registered, got {other:?}"),
}
}
#[test]
fn describe_key_never_fails() {
for key in ["demo/example/foo", "", "v2/x", "@weird/key"] {
let d = describe_key("", key, None);
assert_eq!(d.key, key);
assert!(matches!(d.facts.shape, KeyShape::Unparsed { .. }), "{key}");
}
let d = describe_key("zensight", "other/v1/h-3fa9c2d41b7e/state/x/y", None);
assert_eq!(d.facts.shape, KeyShape::NotUnderBase);
}
}
#[cfg(test)]
mod cache_tests {
use super::*;
fn key(i: usize) -> String {
format!("v1/h-3fa9c2d41b7e/telemetry/sysinfo/k{i}")
}
#[test]
fn the_bound_holds_and_every_drop_is_counted() {
let mut cache = FactsCache::with_capacity(100);
for i in 0..1_000 {
cache.ensure("", &key(i), None);
}
assert!(cache.len() <= 100, "held {}", cache.len());
assert!(cache.evicted() > 0, "the fixture must trip the bound");
assert_eq!(cache.inserted(), 1_000, "every key here was distinct");
assert_eq!(cache.len() as u64 + cache.evicted(), cache.inserted());
}
#[test]
fn a_re_observed_eviction_is_projected_again() {
let mut cache = FactsCache::with_capacity(2);
for i in 0..10 {
cache.ensure("", &key(i), None);
}
let after_first_pass = cache.inserted();
for i in 0..10 {
cache.ensure("", &key(i), None);
}
assert!(
cache.inserted() > after_first_pass,
"a second pass over evicted keys re-projects them"
);
assert_eq!(cache.len() as u64 + cache.evicted(), cache.inserted());
}
#[test]
fn the_least_recently_observed_is_the_one_that_goes() {
let mut cache = FactsCache::with_capacity(4);
for i in 0..4 {
cache.ensure("", &key(i), None);
}
cache.ensure("", &key(0), None);
cache.ensure("", &key(99), None);
assert!(cache.get(&key(0)).is_some(), "the re-observed key survives");
assert!(cache.get(&key(1)).is_none(), "the oldest went instead");
}
#[test]
fn ensure_is_idempotent_and_does_not_reproject() {
let slices = SliceSet::default();
let mut cache = FactsCache::with_capacity(10);
cache.ensure("", &key(0), None);
let before = cache.get(&key(0)).cloned();
cache.ensure("", &key(0), Some(&slices));
assert_eq!(
cache.get(&key(0)).cloned(),
before,
"a second ensure must not re-resolve behind the caller's back"
);
assert_eq!(cache.len(), 1);
}
#[test]
fn resolve_all_reaches_entries_projected_before_the_registry_arrived() {
let mut cache = FactsCache::with_capacity(10);
cache.ensure("", &key(0), None);
assert_eq!(
cache.get(&key(0)).map(|f| f.registration.clone()),
Some(Registration::Unknown)
);
cache.resolve_all(&SliceSet::default());
assert_ne!(
cache.get(&key(0)).map(|f| f.registration.clone()),
Some(Registration::Unknown),
"a registry that arrives late still reaches what was already cached"
);
}
#[test]
fn clearing_keeps_the_bound_and_forgets_the_count() {
let mut cache = FactsCache::with_capacity(4);
for i in 0..40 {
cache.ensure("", &key(i), None);
}
assert!(cache.evicted() > 0);
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.max_keys(), 4, "the bound is a setting, not a state");
assert_eq!(
cache.evicted(),
0,
"retirements under another deployment are not this one's"
);
assert_eq!(cache.inserted(), 0);
}
#[test]
fn a_degenerate_bound_is_still_a_bound() {
let mut cache = FactsCache::with_capacity(0);
for i in 0..10 {
cache.ensure("", &key(i), None);
}
assert_eq!(cache.max_keys(), 1);
assert!(cache.len() <= 1);
}
}