Skip to main content

kevy_embedded/
ops_hash_ttl.rs

1//! Embedded hash field-TTL facades (Redis 7.4 family).
2//!
3//! AOF: deadline writes log the canonical absolute `HPEXPIREAT`
4//! frame regardless of which facade set them (relative forms convert
5//! before logging — replay can't re-anchor); `hpersist` logs itself.
6
7use crate::KevyResult;
8use std::time::Duration;
9
10use kevy_store::{HExpireCode, HExpireCond, now_unix_ms};
11
12use crate::store::ensure_writable;
13use crate::store::{Store, commit_write, store_err};
14
15impl Store {
16    /// `HEXPIRE` — set a relative per-field TTL. One Redis code per
17    /// field (`-2` missing, `0` condition failed, `1` set, `2` deleted).
18    pub fn hexpire(
19        &self,
20        key: &[u8],
21        fields: &[&[u8]],
22        ttl: Duration,
23        cond: HExpireCond,
24    ) -> KevyResult<Vec<HExpireCode>> {
25        let deadline = now_unix_ms().saturating_add(ttl.as_millis() as u64);
26        self.hpexpire_at(key, fields, deadline, cond)
27    }
28
29    /// `HPEXPIREAT` — absolute unix-ms per-field deadline (the
30    /// canonical form; also what the AOF carries).
31    pub fn hpexpire_at(
32        &self,
33        key: &[u8],
34        fields: &[&[u8]],
35        deadline_ms: u64,
36        cond: HExpireCond,
37    ) -> KevyResult<Vec<HExpireCode>> {
38        ensure_writable(self)?;
39        let mut g = self.wshard(key);
40        let codes = g.store.hexpire_at(key, fields, deadline_ms, cond).map_err(store_err)?;
41        // Log only when something changed (set or immediate-delete).
42        if codes.iter().any(|&c| c == 1 || c == 2) {
43            let ms = deadline_ms.to_string();
44            let n = fields.len().to_string();
45            let mut argv: Vec<&[u8]> = Vec::with_capacity(5 + fields.len());
46            argv.push(b"HPEXPIREAT");
47            argv.push(key);
48            argv.push(ms.as_bytes());
49            argv.push(b"FIELDS");
50            argv.push(n.as_bytes());
51            argv.extend(fields.iter().copied());
52            commit_write(&mut g, &argv)?;
53        }
54        Ok(codes)
55    }
56
57    /// `HTTL` — remaining **seconds** per field (`-2` missing, `-1` no TTL).
58    ///
59    /// Rounded to the nearest second, as the wire verb and Redis both do. For
60    /// the unrounded value use [`Db::hpttl`]. Before 4.0 this method carried the
61    /// HTTL name and returned milliseconds, which is a thousand-fold error
62    /// waiting to happen in any caller that trusted the name.
63    pub fn httl(&self, key: &[u8], fields: &[&[u8]]) -> KevyResult<Vec<i64>> {
64        Ok(self
65            .hpttl(key, fields)?
66            .into_iter()
67            .map(|ms| if ms >= 0 { (ms + 500) / 1000 } else { ms })
68            .collect())
69    }
70
71    /// `HPTTL` — remaining **milliseconds** per field (`-2` missing, `-1` no
72    /// TTL).
73    pub fn hpttl(&self, key: &[u8], fields: &[&[u8]]) -> KevyResult<Vec<i64>> {
74        let mut g = self.wshard(key);
75        g.store.hpttl(key, fields).map_err(store_err)
76    }
77
78    /// `HPERSIST` — clear per-field TTLs (`-2` missing, `-1` had no
79    /// TTL, `1` cleared).
80    pub fn hpersist(&self, key: &[u8], fields: &[&[u8]]) -> KevyResult<Vec<HExpireCode>> {
81        ensure_writable(self)?;
82        let mut g = self.wshard(key);
83        let codes = g.store.hpersist(key, fields).map_err(store_err)?;
84        if codes.contains(&1) {
85            let n = fields.len().to_string();
86            let mut argv: Vec<&[u8]> = Vec::with_capacity(4 + fields.len());
87            argv.push(b"HPERSIST");
88            argv.push(key);
89            argv.push(b"FIELDS");
90            argv.push(n.as_bytes());
91            argv.extend(fields.iter().copied());
92            commit_write(&mut g, &argv)?;
93        }
94        Ok(codes)
95    }
96}