Skip to main content

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}