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///
13/// ```
14/// let s = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
15/// s.set(b"k", b"v")?;
16/// let info = s.info();
17/// assert_eq!(info.keys, 1);
18/// assert!(info.tiering.is_none());
19/// # Ok::<(), kevy_embedded::KevyError>(())
20/// ```
21#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub struct KevyInfo {
24 /// Live key count (`DBSIZE`).
25 ///
26 /// ```
27 /// let s = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
28 /// s.set(b"a", b"1")?;
29 /// s.set(b"b", b"2")?;
30 /// assert_eq!(s.info().keys, 2);
31 /// # Ok::<(), kevy_embedded::KevyError>(())
32 /// ```
33 pub keys: usize,
34 /// Estimated resident bytes (`INFO memory: used_memory`).
35 ///
36 /// ```
37 /// let s = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
38 /// let before = s.info().used_memory;
39 /// s.set(b"blob", &[0u8; 64 * 1024])?;
40 /// assert!(s.info().used_memory >= before + 64 * 1024);
41 /// # Ok::<(), kevy_embedded::KevyError>(())
42 /// ```
43 pub used_memory: u64,
44 /// Current on-disk AOF size in bytes (0 when persistence is off).
45 ///
46 /// ```
47 /// use kevy_embedded::{AppendFsync, Config, Store};
48 ///
49 /// assert_eq!(Store::open(Config::default())?.info().aof_bytes, 0); // in-memory
50 /// let dir = kevy_tmpdir::TmpDir::new("info-aof-bytes");
51 /// let cfg = Config::default().with_persist(dir.path()).with_appendfsync(AppendFsync::Always);
52 /// let s = Store::open(cfg)?;
53 /// s.set(b"k", b"v")?;
54 /// assert!(s.info().aof_bytes > 0);
55 /// # Ok::<(), kevy_embedded::KevyError>(())
56 /// ```
57 pub aof_bytes: u64,
58 /// Live keys carrying a TTL — the expire-set size. A `0` here when you
59 /// expected TTLs is the tell that the TTL subsystem didn't register them.
60 ///
61 /// ```
62 /// let s = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
63 /// s.set(b"session", b"x")?;
64 /// s.set(b"forever", b"y")?;
65 /// s.expire(b"session", std::time::Duration::from_secs(60))?;
66 /// assert_eq!(s.info().expire_pending, 1);
67 /// # Ok::<(), kevy_embedded::KevyError>(())
68 /// ```
69 pub expire_pending: usize,
70 /// Total keys evicted by `maxmemory` so far.
71 ///
72 /// ```
73 /// use kevy_embedded::{Config, EvictionPolicy, Store};
74 ///
75 /// let cfg = Config::default()
76 /// .with_max_memory(256 << 10)
77 /// .with_eviction(EvictionPolicy::AllKeysLru);
78 /// let s = Store::open(cfg)?;
79 /// assert_eq!(s.info().evictions, 0);
80 /// for i in 0..2000 {
81 /// s.set(format!("k{i}").as_bytes(), &[0u8; 512])?;
82 /// }
83 /// assert!(s.info().evictions > 0);
84 /// # Ok::<(), kevy_embedded::KevyError>(())
85 /// ```
86 pub evictions: u64,
87 /// Total keys expired (lazy + active reaper) so far.
88 ///
89 /// ```
90 /// use std::time::Duration;
91 /// use kevy_embedded::{Config, Store};
92 ///
93 /// let s = Store::open(Config::default().with_ttl_reaper_manual())?;
94 /// s.set_with_ttl(b"k", b"v", Duration::from_millis(1))?;
95 /// std::thread::sleep(Duration::from_millis(20));
96 /// assert_eq!(s.get(b"k")?, None); // reads as gone, but a read reclaims nothing
97 /// assert_eq!(s.info().expired_keys, 0);
98 /// // the active reaper samples, so a few ticks catch the one expired key
99 /// for _ in 0..50 { s.tick(); }
100 /// assert_eq!(s.info().expired_keys, 1);
101 /// # Ok::<(), kevy_embedded::KevyError>(())
102 /// ```
103 pub expired_keys: u64,
104 /// Tiering gauges (the `# Tiering` INFO section). `None` when
105 /// tiering is off — the untiered snapshot is unchanged.
106 ///
107 /// ```
108 /// use kevy_embedded::{Config, Store};
109 ///
110 /// assert!(Store::open(Config::default())?.info().tiering.is_none());
111 /// let dir = kevy_tmpdir::TmpDir::new("info-tiering");
112 /// let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
113 /// assert_eq!(s.info().tiering.map(|t| t.tier_budget_bytes), Some(1 << 20));
114 /// # Ok::<(), kevy_embedded::KevyError>(())
115 /// ```
116 pub tiering: Option<KevyTierInfo>,
117}
118
119/// The `# Tiering` gauge set (B12) — summed across shards; field names
120/// mirror the server's `INFO # Tiering` section one-to-one.
121///
122/// ```
123/// let s = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
124/// assert!(s.info().tiering.is_none()); // tiering off: no gauges
125/// # Ok::<(), kevy_embedded::KevyError>(())
126/// ```
127#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
128#[non_exhaustive]
129pub struct KevyTierInfo {
130 /// The resolved RAM budget (whole store — Σ per-shard slices).
131 ///
132 /// ```
133 /// # use kevy_embedded::{Config, Store};
134 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
135 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
136 /// # s.set(b"cold", &[b'x'; 4096])?;
137 /// # assert!(s.debug_force_demote(b"cold"));
138 /// let t = s.info().tiering.unwrap();
139 /// assert_eq!(t.tier_budget_bytes, 1 << 20); // the budget the config named
140 /// # Ok::<(), kevy_embedded::KevyError>(())
141 /// ```
142 pub tier_budget_bytes: u64,
143 /// The demote target `used_memory` is held to (`budget·19/20 − index
144 /// floor`, saturating). **0 = the index floor alone exceeds the budget.**
145 ///
146 /// ```
147 /// # use kevy_embedded::{Config, Store};
148 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
149 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
150 /// # s.set(b"cold", &[b'x'; 4096])?;
151 /// # assert!(s.debug_force_demote(b"cold"));
152 /// let t = s.info().tiering.unwrap();
153 /// // a cold key is charged once, inside used_memory: the target stays put
154 /// assert_eq!(t.tier_effective_target, (1 << 20) * 19 / 20);
155 /// # Ok::<(), kevy_embedded::KevyError>(())
156 /// ```
157 pub tier_effective_target: u64,
158 /// Currently-cold keys.
159 ///
160 /// ```
161 /// # use kevy_embedded::{Config, Store};
162 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
163 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
164 /// # s.set(b"cold", &[b'x'; 4096])?;
165 /// # assert!(s.debug_force_demote(b"cold"));
166 /// assert_eq!(s.info().tiering.unwrap().cold_keys, 1);
167 /// s.get(b"cold")?; // the first read serves from disk and leaves it cold
168 /// s.get(b"cold")?; // the second promotes it back to RAM
169 /// assert_eq!(s.info().tiering.unwrap().cold_keys, 0);
170 /// # Ok::<(), kevy_embedded::KevyError>(())
171 /// ```
172 pub cold_keys: u64,
173 /// Σ original weights of currently-cold values.
174 ///
175 /// ```
176 /// # use kevy_embedded::{Config, Store};
177 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
178 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
179 /// # s.set(b"cold", &[b'x'; 4096])?;
180 /// # assert!(s.debug_force_demote(b"cold"));
181 /// let t = s.info().tiering.unwrap();
182 /// assert!(t.cold_bytes >= 4096); // the value's original weight, not its disk size
183 /// # Ok::<(), kevy_embedded::KevyError>(())
184 /// ```
185 pub cold_bytes: u64,
186 /// RAM the cold stubs cost (Σ `ENTRY_OVERHEAD + key heap bytes`).
187 ///
188 /// ```
189 /// # use kevy_embedded::{Config, Store};
190 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
191 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
192 /// # s.set(b"cold", &[b'x'; 4096])?;
193 /// # assert!(s.debug_force_demote(b"cold"));
194 /// let t = s.info().tiering.unwrap();
195 /// assert!(t.stub_bytes > 0 && t.stub_bytes < 4096); // the stub left in RAM is small
196 /// # Ok::<(), kevy_embedded::KevyError>(())
197 /// ```
198 pub stub_bytes: u64,
199 /// The index/view memory floor fed on the reaper tick.
200 ///
201 /// ```
202 /// # use kevy_embedded::{Config, Store};
203 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
204 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
205 /// # s.set(b"cold", &[b'x'; 4096])?;
206 /// # assert!(s.debug_force_demote(b"cold"));
207 /// assert_eq!(s.info().tiering.unwrap().index_reserved_bytes, 0); // no indexes yet
208 /// s.hset(b"row:1", &[(b"n", b"1")])?;
209 /// use kevy_embedded::{IndexKind, IndexValType};
210 /// s.idx_create(b"by_n", b"row:", b"n", IndexValType::I64, IndexKind::Range)?;
211 /// s.tick(); // the tick feeds the index floor
212 /// assert!(s.info().tiering.unwrap().index_reserved_bytes > 0);
213 /// # Ok::<(), kevy_embedded::KevyError>(())
214 /// ```
215 pub index_reserved_bytes: u64,
216 /// Vlog bytes on disk.
217 ///
218 /// ```
219 /// # use kevy_embedded::{Config, Store};
220 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
221 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
222 /// # s.set(b"cold", &[b'x'; 4096])?;
223 /// # assert!(s.debug_force_demote(b"cold"));
224 /// let t = s.info().tiering.unwrap();
225 /// assert!(t.vlog_size_bytes > 0 && t.vlog_size_bytes < 4096); // a run of one byte compresses
226 /// # Ok::<(), kevy_embedded::KevyError>(())
227 /// ```
228 pub vlog_size_bytes: u64,
229 /// Vlog live (non-dead) bytes.
230 ///
231 /// ```
232 /// # use kevy_embedded::{Config, Store};
233 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
234 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
235 /// # s.set(b"cold", &[b'x'; 4096])?;
236 /// # assert!(s.debug_force_demote(b"cold"));
237 /// let t = s.info().tiering.unwrap();
238 /// assert_eq!(t.vlog_live_bytes, t.vlog_size_bytes); // nothing has died yet
239 /// # Ok::<(), kevy_embedded::KevyError>(())
240 /// ```
241 pub vlog_live_bytes: u64,
242 /// Vlog file count.
243 ///
244 /// ```
245 /// # use kevy_embedded::{Config, Store};
246 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
247 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
248 /// # s.set(b"cold", &[b'x'; 4096])?;
249 /// # assert!(s.debug_force_demote(b"cold"));
250 /// assert_eq!(s.info().tiering.unwrap().vlog_files, 1);
251 /// # Ok::<(), kevy_embedded::KevyError>(())
252 /// ```
253 pub vlog_files: u64,
254 /// Vlog compaction epoch (retired files).
255 ///
256 /// ```
257 /// # use kevy_embedded::{Config, Store};
258 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
259 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
260 /// # s.set(b"cold", &[b'x'; 4096])?;
261 /// # assert!(s.debug_force_demote(b"cold"));
262 /// assert_eq!(s.info().tiering.unwrap().vlog_epoch, 0); // no file retired yet
263 /// # Ok::<(), kevy_embedded::KevyError>(())
264 /// ```
265 pub vlog_epoch: u64,
266 /// Keys demoted since boot.
267 ///
268 /// ```
269 /// # use kevy_embedded::{Config, Store};
270 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
271 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
272 /// # s.set(b"cold", &[b'x'; 4096])?;
273 /// # assert!(s.debug_force_demote(b"cold"));
274 /// assert_eq!(s.info().tiering.unwrap().demotions_total, 1);
275 /// # Ok::<(), kevy_embedded::KevyError>(())
276 /// ```
277 pub demotions_total: u64,
278 /// Keys promoted back since boot.
279 ///
280 /// ```
281 /// # use kevy_embedded::{Config, Store};
282 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
283 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
284 /// # s.set(b"cold", &[b'x'; 4096])?;
285 /// # assert!(s.debug_force_demote(b"cold"));
286 /// assert_eq!(s.info().tiering.unwrap().promotions_total, 0);
287 /// assert_eq!(s.get(b"cold")?.map(|v| v.len()), Some(4096));
288 /// assert_eq!(s.info().tiering.unwrap().promotions_total, 0, "one read is not enough");
289 /// s.get(b"cold")?;
290 /// assert_eq!(s.info().tiering.unwrap().promotions_total, 1);
291 /// # Ok::<(), kevy_embedded::KevyError>(())
292 /// ```
293 pub promotions_total: u64,
294 /// No-promote peek record reads — one per cold row swept by
295 /// hydration / backfill / digest / export.
296 ///
297 /// ```
298 /// use kevy_embedded::{Config, IndexKind, IndexValType, Store};
299 ///
300 /// let dir = kevy_tmpdir::TmpDir::new("info-peek");
301 /// let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
302 /// let pad = [b'x'; 4096];
303 /// s.hset(b"row:1", &[(&b"n"[..], &b"7"[..]), (&b"pad"[..], &pad[..])])?;
304 /// assert!(s.debug_force_demote(b"row:1"));
305 /// // the backfill reads the cold row without promoting it
306 /// s.idx_create(b"by_n", b"row:", b"n", IndexValType::I64, IndexKind::Range)?;
307 /// let t = s.info().tiering.unwrap();
308 /// assert_eq!((t.peek_preads_total, t.cold_keys), (1, 1));
309 /// # Ok::<(), kevy_embedded::KevyError>(())
310 /// ```
311 pub peek_preads_total: u64,
312 /// Batched cold-read submissions — one per peeked page.
313 ///
314 /// ```
315 /// # use kevy_embedded::{Config, Store};
316 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
317 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
318 /// # s.set(b"cold", &[b'x'; 4096])?;
319 /// # assert!(s.debug_force_demote(b"cold"));
320 /// // the embedded store peeks cold rows one at a time; batched pages are
321 /// // the server's hydration path
322 /// assert_eq!(s.info().tiering.unwrap().batch_submissions_total, 0);
323 /// # Ok::<(), kevy_embedded::KevyError>(())
324 /// ```
325 pub batch_submissions_total: u64,
326}
327
328/// The value log's compression accounting, summed across shards; field
329/// names mirror the last four lines of the server's `INFO # Tiering`.
330/// Payload plus frame headers against raw bytes is the ratio; the
331/// dictionaries are memory, one per vlog file.
332/// # Examples
333///
334/// ```
335/// use kevy_embedded::{Config, Store};
336/// // untiered: the section does not exist, so neither do its terms
337/// let s = Store::open(Config::default()).unwrap();
338/// assert!(s.tier_compression().is_none());
339/// ```
340#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
341#[non_exhaustive]
342pub struct KevyTierCompression {
343 /// Value bytes before encoding, over the vlog files on disk.
344 ///
345 /// ```
346 /// # use kevy_embedded::{Config, Store};
347 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
348 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
349 /// # s.set(b"cold", &[b'x'; 4096])?;
350 /// # assert!(s.debug_force_demote(b"cold"));
351 /// assert_eq!(s.tier_compression().unwrap().vlog_raw_bytes, 4096);
352 /// # Ok::<(), kevy_embedded::KevyError>(())
353 /// ```
354 pub vlog_raw_bytes: u64,
355 /// Encoded payload bytes on disk, frame headers excluded.
356 ///
357 /// ```
358 /// # use kevy_embedded::{Config, Store};
359 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
360 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
361 /// # s.set(b"cold", &[b'x'; 4096])?;
362 /// # assert!(s.debug_force_demote(b"cold"));
363 /// let c = s.tier_compression().unwrap();
364 /// assert!(c.vlog_payload_bytes < c.vlog_raw_bytes); // compressed
365 /// # Ok::<(), kevy_embedded::KevyError>(())
366 /// ```
367 pub vlog_payload_bytes: u64,
368 /// Frame header bytes on disk (tag + LEB128 length per record).
369 ///
370 /// ```
371 /// # use kevy_embedded::{Config, Store};
372 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
373 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
374 /// # s.set(b"cold", &[b'x'; 4096])?;
375 /// # assert!(s.debug_force_demote(b"cold"));
376 /// // a tag byte, and 4096 as two LEB128 bytes
377 /// assert_eq!(s.tier_compression().unwrap().vlog_frame_header_bytes, 3);
378 /// # Ok::<(), kevy_embedded::KevyError>(())
379 /// ```
380 pub vlog_frame_header_bytes: u64,
381 /// Dictionary bytes in memory.
382 ///
383 /// ```
384 /// # use kevy_embedded::{Config, Store};
385 /// # let dir = kevy_tmpdir::TmpDir::new("tier-info-doc");
386 /// # let s = Store::open(Config::default().with_persist(dir.path()).with_tier_budget(1 << 20))?;
387 /// # s.set(b"cold", &[b'x'; 4096])?;
388 /// # assert!(s.debug_force_demote(b"cold"));
389 /// // the first file has nothing to train a dictionary on
390 /// assert_eq!(s.tier_compression().unwrap().vlog_dict_bytes, 0);
391 /// # Ok::<(), kevy_embedded::KevyError>(())
392 /// ```
393 pub vlog_dict_bytes: u64,
394}
395
396impl Store {
397 /// One-shot snapshot of the store's introspection counters. See
398 /// [`KevyInfo`]. Takes the embedded mutex once; safe to call from a
399 /// health endpoint.
400 pub fn info(&self) -> KevyInfo {
401 KevyInfo {
402 keys: self.sum_shards(|i| i.store.dbsize()),
403 used_memory: self.sum_shards_u64(|i| i.store.used_memory()),
404 #[cfg(feature = "persist")]
405 aof_bytes: self
406 .sum_shards_u64(|i| i.aof.as_ref().map_or(0, kevy_persist::Aof::size_bytes)),
407 #[cfg(not(feature = "persist"))]
408 aof_bytes: 0,
409 expire_pending: self.sum_shards(|i| i.store.ttl_pending_count()),
410 evictions: self.sum_shards_u64(|i| i.store.evictions_total()),
411 expired_keys: self.sum_shards_u64(|i| i.store.expired_keys_total()),
412 tiering: self.tier_info(),
413 }
414 }
415
416 /// The `# Tiering` gauges summed across shards, or `None` when
417 /// tiering is off (the section is absent, not zeroed — INFO
418 /// stability for untiered stores).
419 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
420 pub fn tier_info(&self) -> Option<KevyTierInfo> {
421 self.config.tier_budget?;
422 let mut t = KevyTierInfo::default();
423 for shard in self.shards.iter() {
424 let g = crate::store::lock_read(shard);
425 let s = g.store.tier_stats();
426 t.tier_budget_bytes += s.budget;
427 t.tier_effective_target += s.effective_target;
428 t.cold_keys += s.cold_keys;
429 t.cold_bytes += s.cold_bytes;
430 t.stub_bytes += s.stub_bytes;
431 t.index_reserved_bytes += s.reserved_bytes;
432 t.vlog_size_bytes += s.vlog_bytes;
433 t.vlog_live_bytes += s.vlog_live_bytes;
434 t.vlog_files += s.vlog_files;
435 t.vlog_epoch += s.vlog_epoch;
436 t.demotions_total += s.demotions_total;
437 t.promotions_total += s.promotions_total;
438 t.peek_preads_total += s.peek_preads_total;
439 t.batch_submissions_total += s.batch_submissions_total;
440 }
441 Some(t)
442 }
443
444 /// No tier backend compiled in — never a section.
445 #[cfg(not(all(feature = "tier", not(target_arch = "wasm32"))))]
446 pub fn tier_info(&self) -> Option<KevyTierInfo> {
447 None
448 }
449
450 /// The value log's compression accounting summed across shards, or
451 /// `None` when tiering is off. See [`KevyTierCompression`].
452 /// # Examples
453 ///
454 /// ```
455 /// use kevy_embedded::{Config, Store};
456 /// let dir = kevy_tmpdir::TmpDir::new("tier-compression-doc");
457 /// let cfg = Config::default().with_persist(dir.path()).with_tier_budget(1 << 20);
458 /// let s = Store::open(cfg).unwrap();
459 /// s.set(b"cold", &[b'x'; 4096]).unwrap();
460 /// assert!(s.debug_force_demote(b"cold"));
461 /// let c = s.tier_compression().unwrap();
462 /// assert_eq!(c.vlog_raw_bytes, 4096);
463 /// // a run of one byte keeps a small fraction of itself
464 /// assert!(c.vlog_payload_bytes < 4096);
465 /// ```
466 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
467 pub fn tier_compression(&self) -> Option<KevyTierCompression> {
468 self.config.tier_budget?;
469 let mut t = KevyTierCompression::default();
470 for shard in self.shards.iter() {
471 let c = crate::store::lock_read(shard).store.tier_compression();
472 t.vlog_raw_bytes += c.raw_bytes;
473 t.vlog_payload_bytes += c.payload_bytes;
474 t.vlog_frame_header_bytes += c.frame_header_bytes;
475 t.vlog_dict_bytes += c.dict_bytes;
476 }
477 Some(t)
478 }
479
480 /// No tier backend compiled in — never a section.
481 #[cfg(not(all(feature = "tier", not(target_arch = "wasm32"))))]
482 pub fn tier_compression(&self) -> Option<KevyTierCompression> {
483 None
484 }
485
486 /// Number of live keys that currently carry a TTL (the expire-set size,
487 /// summed across shards).
488 pub fn expire_pending_count(&self) -> usize {
489 self.sum_shards(|i| i.store.ttl_pending_count())
490 }
491
492 /// Remaining TTL for `key` as a [`Duration`], or `None` when the key is
493 /// absent or has no TTL (persistent). For the raw Redis `PTTL` sentinels
494 /// (`-2` no key, `-1` no TTL) use [`Store::ttl_ms`].
495 pub fn ttl(&self, key: &[u8]) -> Option<Duration> {
496 let ms = self.wshard(key).store.pttl(key);
497 if ms < 0 { None } else { Some(Duration::from_millis(ms as u64)) }
498 }
499}