nodedb_types/timeseries/series.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! Series identity and catalog.
4
5use std::collections::HashMap;
6use std::hash::{DefaultHasher, Hash, Hasher};
7
8use serde::{Deserialize, Serialize};
9
10/// Unique identifier for a timeseries (hash of metric name + sorted tag set).
11pub type SeriesId = u64;
12
13/// The canonical key for a series — used for collision detection in the
14/// series catalog. Two `SeriesKey`s that hash to the same `SeriesId` are
15/// a collision; the catalog rehashes with a salt.
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct SeriesKey {
18 pub metric: String,
19 pub tags: Vec<(String, String)>,
20}
21
22impl SeriesKey {
23 pub fn new(metric: impl Into<String>, mut tags: Vec<(String, String)>) -> Self {
24 tags.sort();
25 Self {
26 metric: metric.into(),
27 tags,
28 }
29 }
30
31 /// Compute the SeriesId for this key with an optional collision salt.
32 pub fn to_series_id(&self, salt: u64) -> SeriesId {
33 let mut hasher = DefaultHasher::new();
34 salt.hash(&mut hasher);
35 self.metric.hash(&mut hasher);
36 for (k, v) in &self.tags {
37 k.hash(&mut hasher);
38 v.hash(&mut hasher);
39 }
40 hasher.finish()
41 }
42}
43
44/// Persistent catalog that maps SeriesId → SeriesKey with collision detection.
45///
46/// On insert, if the SeriesId already maps to a *different* SeriesKey, the
47/// catalog rehashes with incrementing salts until it finds a free slot.
48/// This is one lookup per new series (not per row).
49#[derive(Debug, Default, Serialize, Deserialize)]
50pub struct SeriesCatalog {
51 /// SeriesId → (SeriesKey, salt used to produce this ID).
52 entries: HashMap<SeriesId, (SeriesKey, u64)>,
53}
54
55impl SeriesCatalog {
56 pub fn new() -> Self {
57 Self::default()
58 }
59
60 /// Resolve a SeriesKey to its SeriesId, registering it if new.
61 ///
62 /// Returns the SeriesId (potentially rehashed if the natural hash collided).
63 pub fn resolve(&mut self, key: &SeriesKey) -> SeriesId {
64 let mut salt = 0u64;
65 loop {
66 let id = key.to_series_id(salt);
67 match self.entries.get(&id) {
68 None => {
69 self.entries.insert(id, (key.clone(), salt));
70 return id;
71 }
72 Some((existing_key, _)) if existing_key == key => {
73 return id;
74 }
75 Some(_) => {
76 salt += 1;
77 }
78 }
79 }
80 }
81
82 /// Look up a SeriesId to get its canonical key.
83 pub fn get(&self, id: SeriesId) -> Option<&SeriesKey> {
84 self.entries.get(&id).map(|(k, _)| k)
85 }
86
87 /// Number of registered series.
88 pub fn len(&self) -> usize {
89 self.entries.len()
90 }
91
92 pub fn is_empty(&self) -> bool {
93 self.entries.is_empty()
94 }
95}
96
97/// Persistent identity of a NodeDB-Lite database instance.
98///
99/// Generated as a CUID2 on first `open()`, stored in redb metadata.
100/// Scope = one database file. Not a device ID, user ID, or app ID.
101pub type LiteId = String;
102
103/// Battery state reported by the host application for battery-aware flushing.
104#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
105#[non_exhaustive]
106pub enum BatteryState {
107 /// Battery level is sufficient (>50%) or device is on AC power.
108 Normal,
109 /// Battery is low (<20%) and not charging. Defer non-critical I/O.
110 Low,
111 /// Device is currently charging. Safe to flush.
112 Charging,
113 /// Battery state unknown (desktop, non-mobile). Treat as Normal.
114 #[default]
115 Unknown,
116}
117
118impl BatteryState {
119 /// Whether flushing should be deferred in battery-aware mode.
120 pub fn should_defer_flush(&self) -> bool {
121 matches!(self, Self::Low)
122 }
123}