1#[cfg(not(feature = "std"))]
25use crate::nostd_prelude::*;
26use crate::{SmallBytes, Store, StoreError, Value, now_unix_ms};
27
28pub type HExpireCode = i8;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum HExpireCond {
36 #[default]
38 Always,
39 Nx,
41 Xx,
43 Gt,
46 Lt,
48}
49
50impl Store {
51 fn hash_has_field(&mut self, key: &[u8], field: &[u8]) -> Result<bool, StoreError> {
53 match self.tier_serve(key, crate::value::COLD_TAG_HASH)? {
54 None => Ok(false),
55 Some(e) => match &e.value {
56 Value::Hash(h) => Ok(h.get(field).is_some()),
57 Value::SmallHashInline(h) => Ok(h.get(field).is_some()),
58 _ => Err(StoreError::WrongType),
59 },
60 }
61 }
62
63 pub fn hexpire_at(
67 &mut self,
68 key: &[u8],
69 fields: &[&[u8]],
70 deadline_ms: u64,
71 cond: HExpireCond,
72 ) -> Result<Vec<HExpireCode>, StoreError> {
73 self.purge_hash_ttl(key);
74 let mut codes = Vec::with_capacity(fields.len());
75 let now = now_unix_ms();
76 for f in fields {
77 if !self.hash_has_field(key, f)? {
78 codes.push(-2);
79 continue;
80 }
81 let current = self
82 .hfttl
83 .get(key)
84 .and_then(|m| m.get(*f))
85 .copied();
86 let pass = match cond {
87 HExpireCond::Always => true,
88 HExpireCond::Nx => current.is_none(),
89 HExpireCond::Xx => current.is_some(),
90 HExpireCond::Gt => current.is_some_and(|c| deadline_ms > c),
91 HExpireCond::Lt => current.is_none_or(|c| deadline_ms < c),
92 };
93 if !pass {
94 codes.push(0);
95 continue;
96 }
97 if deadline_ms <= now {
98 if let Some(m) = self.hfttl.get_mut(key) {
99 m.remove(*f);
100 }
101 self.hdel(key, &[f])?;
102 codes.push(2);
103 continue;
104 }
105 hfttl_slot(&mut self.hfttl, key)
106 .insert(SmallBytes::from_slice(f), deadline_ms);
107 codes.push(1);
108 }
109 self.prune_hfttl_key(key);
110 Ok(codes)
111 }
112
113 pub fn hpttl(&mut self, key: &[u8], fields: &[&[u8]]) -> Result<Vec<i64>, StoreError> {
116 self.purge_hash_ttl(key);
117 let now = now_unix_ms();
118 let mut out = Vec::with_capacity(fields.len());
119 for f in fields {
120 if !self.hash_has_field(key, f)? {
121 out.push(-2);
122 continue;
123 }
124 match self.hfttl.get(key).and_then(|m| m.get(*f)) {
125 Some(&d) => out.push(d.saturating_sub(now) as i64),
126 None => out.push(-1),
127 }
128 }
129 Ok(out)
130 }
131
132 pub fn hpersist(&mut self, key: &[u8], fields: &[&[u8]]) -> Result<Vec<HExpireCode>, StoreError> {
134 self.purge_hash_ttl(key);
135 let mut out = Vec::with_capacity(fields.len());
136 for f in fields {
137 if !self.hash_has_field(key, f)? {
138 out.push(-2);
139 continue;
140 }
141 let had = self
142 .hfttl
143 .get_mut(key)
144 .and_then(|m| m.remove(*f))
145 .is_some();
146 out.push(if had { 1 } else { -1 });
147 }
148 self.prune_hfttl_key(key);
149 Ok(out)
150 }
151
152 pub(crate) fn purge_hash_ttl(&mut self, key: &[u8]) {
156 if self.hfttl.is_empty() {
157 return;
158 }
159 let now = now_unix_ms();
160 let due: Vec<Vec<u8>> = match self.hfttl.get(key) {
161 None => return,
162 Some(m) => m
163 .iter()
164 .filter(|(_, d)| **d <= now)
165 .map(|(f, _)| f.to_vec())
166 .collect(),
167 };
168 if due.is_empty() {
169 return;
170 }
171 if let Some(m) = self.hfttl.get_mut(key) {
174 for f in &due {
175 m.remove(f.as_slice());
176 }
177 }
178 self.prune_hfttl_key(key);
179 let due_refs: Vec<&[u8]> = due.iter().map(Vec::as_slice).collect();
180 let _ = self.hdel(key, &due_refs);
181 }
182
183 pub(crate) fn clear_hash_field_ttls(&mut self, key: &[u8], fields: &[&[u8]]) {
185 if self.hfttl.is_empty() {
186 return;
187 }
188 if let Some(m) = self.hfttl.get_mut(key) {
189 for f in fields {
190 m.remove(*f);
191 }
192 }
193 self.prune_hfttl_key(key);
194 }
195
196 pub(crate) fn clear_hash_key_ttls(&mut self, key: &[u8]) {
198 if self.hfttl.is_empty() {
199 return;
200 }
201 self.hfttl.remove(key);
202 }
203
204 fn prune_hfttl_key(&mut self, key: &[u8]) {
205 if self.hfttl.get(key).is_some_and(kevy_map_is_empty) {
206 self.hfttl.remove(key);
207 }
208 }
209
210 pub fn tick_hash_ttl(&mut self, max_keys: usize) -> Vec<(Vec<u8>, Vec<Vec<u8>>)> {
213 if self.hfttl.is_empty() {
214 return Vec::new();
215 }
216 let now = now_unix_ms();
217 let candidates: Vec<Vec<u8>> = self
218 .hfttl
219 .iter()
220 .filter(|(_, m)| m.iter().any(|(_, d)| *d <= now))
221 .take(max_keys)
222 .map(|(k, _)| k.to_vec())
223 .collect();
224 let mut out = Vec::with_capacity(candidates.len());
225 for k in candidates {
226 let due: Vec<Vec<u8>> = self
227 .hfttl
228 .get(k.as_slice())
229 .map(|m| {
230 m.iter()
231 .filter(|(_, d)| **d <= now)
232 .map(|(f, _)| f.to_vec())
233 .collect()
234 })
235 .unwrap_or_default();
236 if due.is_empty() {
237 continue;
238 }
239 if let Some(m) = self.hfttl.get_mut(k.as_slice()) {
240 for f in &due {
241 m.remove(f.as_slice());
242 }
243 }
244 self.prune_hfttl_key(&k);
245 let due_refs: Vec<&[u8]> = due.iter().map(Vec::as_slice).collect();
246 let _ = self.hdel(&k, &due_refs);
247 out.push((k, due));
248 }
249 out
250 }
251
252 pub fn load_hash_field_ttl(&mut self, key: &[u8], field: &[u8], deadline_ms: u64) {
255 hfttl_slot(&mut self.hfttl, key)
256 .insert(SmallBytes::from_slice(field), deadline_ms);
257 }
258
259 pub fn hash_ttl_each<F: FnMut(&[u8], &[u8], u64)>(&self, mut f: F) {
261 for (k, m) in self.hfttl.iter() {
262 for (field, &d) in m.iter() {
263 f(k.as_slice(), field.as_slice(), d);
264 }
265 }
266 }
267}
268
269fn kevy_map_is_empty(m: &crate::KevyMap<SmallBytes, u64>) -> bool {
270 m.iter().next().is_none()
271}
272
273
274fn hfttl_slot<'a>(
278 hfttl: &'a mut crate::SideMap<SmallBytes, kevy_map::KevyMap<SmallBytes, u64>>,
279 key: &[u8],
280) -> &'a mut kevy_map::KevyMap<SmallBytes, u64> {
281 #[cfg(feature = "std")]
282 {
283 hfttl.entry(SmallBytes::from_slice(key)).or_default()
284 }
285 #[cfg(not(feature = "std"))]
286 {
287 if hfttl.get(key).is_none() {
288 hfttl.insert(SmallBytes::from_slice(key), kevy_map::KevyMap::default());
289 }
290 hfttl.get_mut(key).expect("inserted above")
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 fn h(s: &mut Store) {
299 s.hset(
300 b"h",
301 &[(b"a".as_slice(), b"1".as_slice()), (b"b".as_slice(), b"2".as_slice())],
302 )
303 .unwrap();
304 }
305
306 #[test]
307 fn hexpire_httl_hpersist_codes() {
308 let mut s = Store::new();
309 h(&mut s);
310 let far = now_unix_ms() + 100_000;
311 let codes = s
313 .hexpire_at(b"h", &[b"a", b"nope"], far, HExpireCond::Always)
314 .unwrap();
315 assert_eq!(codes, vec![1, -2]);
316 let ttls = s.hpttl(b"h", &[b"a", b"b", b"nope"]).unwrap();
317 assert!(ttls[0] > 90_000 && ttls[0] <= 100_000);
318 assert_eq!(&ttls[1..], &[-1, -2]);
319 assert_eq!(
321 s.hexpire_at(b"h", &[b"a"], far + 1, HExpireCond::Nx).unwrap(),
322 vec![0]
323 );
324 assert_eq!(
325 s.hexpire_at(b"h", &[b"b"], far, HExpireCond::Xx).unwrap(),
326 vec![0]
327 );
328 assert_eq!(
330 s.hexpire_at(b"h", &[b"a"], far + 500, HExpireCond::Gt).unwrap(),
331 vec![1]
332 );
333 assert_eq!(
334 s.hexpire_at(b"h", &[b"a"], far, HExpireCond::Gt).unwrap(),
335 vec![0]
336 );
337 assert_eq!(s.hpersist(b"h", &[b"a", b"b", b"nope"]).unwrap(), vec![1, -1, -2]);
339 assert_eq!(s.hpttl(b"h", &[b"a"]).unwrap(), vec![-1]);
340 }
341
342 #[test]
343 fn past_deadline_deletes_and_lazy_purge_enforces() {
344 let mut s = Store::new();
345 h(&mut s);
346 assert_eq!(
348 s.hexpire_at(b"h", &[b"a"], 1, HExpireCond::Always).unwrap(),
349 vec![2]
350 );
351 assert!(!s.hexists(b"h", b"a").unwrap());
352 let soon = now_unix_ms() + 30;
354 s.hexpire_at(b"h", &[b"b"], soon, HExpireCond::Always).unwrap();
355 std::thread::sleep(core::time::Duration::from_millis(50));
356 assert!(!s.hexists(b"h", b"b").unwrap(), "lazy purge on access");
357 assert_eq!(s.hlen(b"h").unwrap(), 0);
359 assert!(s.hfttl.is_empty());
360 }
361
362 #[test]
363 fn overwrite_clears_ttl_and_reaper_reports() {
364 let mut s = Store::new();
365 h(&mut s);
366 let soon = now_unix_ms() + 20;
367 s.hexpire_at(b"h", &[b"a", b"b"], soon, HExpireCond::Always).unwrap();
368 s.hset(b"h", &[(b"a".as_slice(), b"new".as_slice())]).unwrap();
370 assert_eq!(s.hpttl(b"h", &[b"a"]).unwrap(), vec![-1]);
371 std::thread::sleep(core::time::Duration::from_millis(40));
372 let swept = s.tick_hash_ttl(100);
374 assert_eq!(swept, vec![(b"h".to_vec(), vec![b"b".to_vec()])]);
375 assert!(s.hexists(b"h", b"a").unwrap(), "overwritten field survived");
376 let far = now_unix_ms() + 100_000;
378 s.hexpire_at(b"h", &[b"a"], far, HExpireCond::Always).unwrap();
379 s.del(&[b"h".as_slice()]);
380 assert!(s.hfttl.is_empty());
381 }
382}