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