Skip to main content

kevy_embedded/
info.rs

1//! Introspection on a live [`Store`] — the embedded-mode answer to Redis's
2//! `INFO` / `DBSIZE` / `TTL` / expire-set diagnostics. In-process mode has no
3//! TCP endpoint to point `redis-cli` at, so these expose the same signals as
4//! plain method calls on the `Store` handle.
5
6use std::time::Duration;
7
8use crate::store::Store;
9
10/// Snapshot of a store's runtime counters, returned by [`Store::info`]. A
11/// cheap aggregate (one mutex lock); fields mirror the individual accessors.
12#[derive(Debug, Clone)]
13pub struct KevyInfo {
14    /// Live key count (`DBSIZE`).
15    pub keys: usize,
16    /// Estimated resident bytes (`INFO memory: used_memory`).
17    pub used_memory: u64,
18    /// Current on-disk AOF size in bytes (0 when persistence is off).
19    pub aof_bytes: u64,
20    /// Live keys carrying a TTL — the expire-set size. A `0` here when you
21    /// expected TTLs is the tell that the TTL subsystem didn't register them.
22    pub expire_pending: usize,
23    /// Total keys evicted by `maxmemory` so far.
24    pub evictions: u64,
25    /// Total keys expired (lazy + active reaper) so far.
26    pub expired_keys: u64,
27    /// Tiering gauges (the `# Tiering` INFO section). `None` when
28    /// tiering is off — the untiered snapshot is unchanged.
29    pub tiering: Option<KevyTierInfo>,
30}
31
32/// The `# Tiering` gauge set (B12) — summed across shards; field names
33/// mirror the server's `INFO # Tiering` section one-to-one.
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct KevyTierInfo {
36    /// The resolved RAM budget (whole store — Σ per-shard slices).
37    pub tier_budget_bytes: u64,
38    /// The unified demote target (`budget·19/20 − index floor − stub
39    /// floor`, saturating). **0 = the floor alone exceeds the budget.**
40    pub tier_effective_target: u64,
41    /// Currently-cold keys.
42    pub cold_keys: u64,
43    /// Σ original weights of currently-cold values.
44    pub cold_bytes: u64,
45    /// RAM the cold stubs cost (Σ `ENTRY_OVERHEAD + key heap bytes`).
46    pub stub_bytes: u64,
47    /// The index/view memory floor fed on the reaper tick.
48    pub index_reserved_bytes: u64,
49    /// Vlog bytes on disk.
50    pub vlog_size_bytes: u64,
51    /// Vlog live (non-dead) bytes.
52    pub vlog_live_bytes: u64,
53    /// Vlog file count.
54    pub vlog_files: u64,
55    /// Vlog compaction epoch (retired files).
56    pub vlog_epoch: u64,
57    /// Keys demoted since boot.
58    pub demotions_total: u64,
59    /// Keys promoted back since boot.
60    pub promotions_total: u64,
61    /// No-promote peek record reads — one per cold row swept by
62    /// hydration / backfill / digest / export.
63    pub peek_preads_total: u64,
64    /// Batched cold-read submissions — one per peeked page.
65    pub batch_submissions_total: u64,
66}
67
68impl Store {
69    /// One-shot snapshot of the store's introspection counters. See
70    /// [`KevyInfo`]. Takes the embedded mutex once; safe to call from a
71    /// health endpoint.
72    pub fn info(&self) -> KevyInfo {
73        KevyInfo {
74            keys: self.sum_shards(|i| i.store.dbsize()),
75            used_memory: self.sum_shards_u64(|i| i.store.used_memory()),
76            #[cfg(feature = "persist")]
77            aof_bytes: self.sum_shards_u64(|i| i.aof.as_ref().map_or(0, kevy_persist::Aof::size_bytes)),
78            #[cfg(not(feature = "persist"))]
79            aof_bytes: 0,
80            expire_pending: self.sum_shards(|i| i.store.ttl_pending_count()),
81            evictions: self.sum_shards_u64(|i| i.store.evictions_total()),
82            expired_keys: self.sum_shards_u64(|i| i.store.expired_keys_total()),
83            tiering: self.tier_info(),
84        }
85    }
86
87    /// The `# Tiering` gauges summed across shards, or `None` when
88    /// tiering is off (the section is absent, not zeroed — INFO
89    /// stability for untiered stores).
90    #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
91    pub fn tier_info(&self) -> Option<KevyTierInfo> {
92        self.config.tier_budget?;
93        let mut t = KevyTierInfo::default();
94        for shard in self.shards.iter() {
95            let g = crate::store::lock_read(shard);
96            let s = g.store.tier_stats();
97            t.tier_budget_bytes += s.budget;
98            t.tier_effective_target += s.effective_target;
99            t.cold_keys += s.cold_keys;
100            t.cold_bytes += s.cold_bytes;
101            t.stub_bytes += s.stub_bytes;
102            t.index_reserved_bytes += s.reserved_bytes;
103            t.vlog_size_bytes += s.vlog_bytes;
104            t.vlog_live_bytes += s.vlog_live_bytes;
105            t.vlog_files += s.vlog_files;
106            t.vlog_epoch += s.vlog_epoch;
107            t.demotions_total += s.demotions_total;
108            t.promotions_total += s.promotions_total;
109            t.peek_preads_total += s.peek_preads_total;
110            t.batch_submissions_total += s.batch_submissions_total;
111        }
112        Some(t)
113    }
114
115    /// No tier backend compiled in — never a section.
116    #[cfg(not(all(feature = "tier", not(target_arch = "wasm32"))))]
117    pub fn tier_info(&self) -> Option<KevyTierInfo> {
118        None
119    }
120
121    /// Number of live keys that currently carry a TTL (the expire-set size,
122    /// summed across shards).
123    pub fn expire_pending_count(&self) -> usize {
124        self.sum_shards(|i| i.store.ttl_pending_count())
125    }
126
127    /// Remaining TTL for `key` as a [`Duration`], or `None` when the key is
128    /// absent or has no TTL (persistent). For the raw Redis `PTTL` sentinels
129    /// (`-2` no key, `-1` no TTL) use [`Store::ttl_ms`].
130    pub fn ttl(&self, key: &[u8]) -> Option<Duration> {
131        let ms = self.wshard(key).store.pttl(key);
132        if ms < 0 {
133            None
134        } else {
135            Some(Duration::from_millis(ms as u64))
136        }
137    }
138}