use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
use serde::{Deserialize, Serialize};
pub type SeriesId = u64;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SeriesKey {
pub metric: String,
pub tags: Vec<(String, String)>,
}
impl SeriesKey {
pub fn new(metric: impl Into<String>, mut tags: Vec<(String, String)>) -> Self {
tags.sort();
Self {
metric: metric.into(),
tags,
}
}
pub fn to_series_id(&self, rehash_attempt: u64) -> SeriesId {
let mut hasher = DefaultHasher::new();
rehash_attempt.hash(&mut hasher);
self.metric.hash(&mut hasher);
for (k, v) in &self.tags {
k.hash(&mut hasher);
v.hash(&mut hasher);
}
hasher.finish()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeriesCatalog {
entries: HashMap<SeriesId, (SeriesKey, u64)>,
}
impl SeriesCatalog {
pub fn new() -> Self {
Self::default()
}
pub fn resolve(&mut self, key: &SeriesKey) -> SeriesId {
self.resolve_detailed(key).id
}
pub fn resolve_detailed(&mut self, key: &SeriesKey) -> ResolvedSeries {
let mut rehash_attempt = 0u64;
loop {
let id = key.to_series_id(rehash_attempt);
match self.entries.get(&id) {
None => {
self.entries.insert(id, (key.clone(), rehash_attempt));
return ResolvedSeries {
id,
rehash_attempt,
newly_registered: true,
};
}
Some((existing_key, attempt)) if existing_key == key => {
return ResolvedSeries {
id,
rehash_attempt: *attempt,
newly_registered: false,
};
}
Some(_) => {
rehash_attempt += 1;
}
}
}
}
pub fn forget(&mut self, id: SeriesId) -> bool {
self.entries.remove(&id).is_some()
}
pub fn get(&self, id: SeriesId) -> Option<&SeriesKey> {
self.entries.get(&id).map(|(k, _)| k)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn memory_bytes(&self) -> usize {
self.entries
.iter()
.map(|(_, (key, _))| {
std::mem::size_of::<SeriesId>()
+ std::mem::size_of::<(SeriesKey, u64)>()
+ key.metric.len()
+ key
.tags
.iter()
.map(|(k, v)| k.len() + v.len() + 48)
.sum::<usize>()
+ 24
})
.sum()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedSeries {
pub id: SeriesId,
pub rehash_attempt: u64,
pub newly_registered: bool,
}
#[cfg(test)]
mod catalog_tests {
use super::*;
#[test]
fn series_key_sorted_tags() {
let k1 = SeriesKey::new(
"cpu",
vec![("host".into(), "a".into()), ("dc".into(), "us".into())],
);
let k2 = SeriesKey::new(
"cpu",
vec![("dc".into(), "us".into()), ("host".into(), "a".into())],
);
assert_eq!(k1, k2);
assert_eq!(k1.to_series_id(0), k2.to_series_id(0));
}
#[test]
fn series_catalog_resolve() {
let mut catalog = SeriesCatalog::new();
let k = SeriesKey::new("cpu", vec![("host".into(), "prod-1".into())]);
let id1 = catalog.resolve(&k);
let id2 = catalog.resolve(&k);
assert_eq!(id1, id2);
assert_eq!(catalog.len(), 1);
}
#[test]
fn series_catalog_different_keys() {
let mut catalog = SeriesCatalog::new();
let k1 = SeriesKey::new("cpu", vec![("host".into(), "a".into())]);
let k2 = SeriesKey::new("mem", vec![("host".into(), "a".into())]);
let id1 = catalog.resolve(&k1);
let id2 = catalog.resolve(&k2);
assert_ne!(id1, id2);
assert_eq!(catalog.len(), 2);
}
fn plant(catalog: &mut SeriesCatalog, id: SeriesId, key: SeriesKey, attempt: u64) {
catalog.entries.insert(id, (key, attempt));
}
#[test]
fn colliding_key_is_rehashed_to_a_distinct_id() {
let victim = SeriesKey::new("cpu", vec![("host".into(), "a".into())]);
let squatter = SeriesKey::new("unrelated", vec![]);
let natural = victim.to_series_id(0);
let mut catalog = SeriesCatalog::new();
plant(&mut catalog, natural, squatter.clone(), 0);
let resolved = catalog.resolve_detailed(&victim);
assert_ne!(
resolved.id, natural,
"a collision must not reuse the occupied ID"
);
assert_eq!(resolved.rehash_attempt, 1);
assert!(resolved.newly_registered);
assert_eq!(catalog.get(resolved.id), Some(&victim));
assert_eq!(catalog.get(natural), Some(&squatter));
}
#[test]
fn rehashed_key_keeps_its_id_and_reports_not_newly_registered() {
let victim = SeriesKey::new("cpu", vec![("host".into(), "a".into())]);
let mut catalog = SeriesCatalog::new();
plant(
&mut catalog,
victim.to_series_id(0),
SeriesKey::new("unrelated", vec![]),
0,
);
let first = catalog.resolve_detailed(&victim);
let second = catalog.resolve_detailed(&victim);
assert_eq!(first.id, second.id);
assert_eq!(second.rehash_attempt, 1);
assert!(
!second.newly_registered,
"a collision must be reported once, not on every row"
);
}
#[test]
fn forget_frees_the_slot_and_resolve_reproduces_the_id() {
let key = SeriesKey::new("cpu", vec![("host".into(), "a".into())]);
let mut catalog = SeriesCatalog::new();
let id = catalog.resolve(&key);
assert!(catalog.forget(id));
assert!(catalog.is_empty());
assert!(!catalog.forget(id), "forgetting twice is not an error");
assert_eq!(catalog.resolve(&key), id);
}
}
pub type LiteId = String;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BatteryState {
Normal,
Low,
Charging,
#[default]
Unknown,
}
impl BatteryState {
pub fn should_defer_flush(&self) -> bool {
matches!(self, Self::Low)
}
}