yo_kv/ttl.rs
1//! Deadlines on individual fields, which is the `HEXPIRE` family.
2//!
3//! Y20 makes hash field TTL the carve-out from ordinary expiry: a key's deadline
4//! is an int64 in its value header, but a field's cannot be, because there is no
5//! header per field and adding one would cost every hash that never expires a
6//! field. `08` section 3 spends it instead on a side array allocated the first
7//! time any field of that hash is given a deadline, and nothing before then.
8//!
9//! So a hash with no field TTL carries one empty `Vec`, which is three words in
10//! the hash and no allocation, and G8's sixteen bytes a field is untouched. A
11//! hash with one field TTL carries eight bytes for every field it has. That is
12//! the trade, and it is the right way round, because a hash with a field TTL is
13//! the rare one.
14//!
15//! # Indexed by the row, not by a ttl_idx
16//!
17//! `08` section 3 says the array is indexed by a `ttl_idx` held in the row. This
18//! is indexed by the row position itself, which is a divergence and worth the
19//! sentence.
20//!
21//! A `ttl_idx` is four bytes in every row, always, to point into an array holding
22//! eight bytes only for the fields that have deadlines. Against a dense array of
23//! eight bytes a row, it saves memory only when fewer than half the fields have a
24//! deadline, and it costs a second dependent load on every read, because the read
25//! has the row and needs the idx before it can have the deadline. It also has to
26//! carry a free list, since a persisted field's slot has to go back somewhere.
27//!
28//! The dense array has none of that. The row position is already in hand the
29//! moment the probe finishes, so the deadline is one indexed load from a base
30//! pointer, and the whole structure is a `Vec`. When there is a hash type to
31//! measure, the density where the two cross is a fact rather than an argument,
32//! and this can be revisited then.
33//!
34//! # Staying in step
35//!
36//! [`Elements`](crate::Elements) swap removes: the last row moves into the hole.
37//! A parallel array has to make the same move or every deadline after the hole
38//! belongs to the wrong field, which is a bug that reads correct on the first
39//! `HTTL` after it and wrong on all of them. That is what [`Deadlines::inserted`]
40//! and [`Deadlines::removed`] are, and a hash has to call them for every row it
41//! adds or takes out. `deadlines_follow_the_table_through_a_swap_remove` is the
42//! test that says so.
43//!
44//! # Lazy, like the rest of expiry
45//!
46//! A field past its deadline is gone when something looks at it, and until then
47//! it is still sitting there. [`Deadlines::soonest`] exists so that the active
48//! cycle in M5 can skip a whole hash without walking it, which is the same thing
49//! Redis registers in its global HFE structure.
50
51/// No deadline.
52///
53/// Redis spells this `EB_EXPIRE_TIME_INVALID`, one past its 48 bit maximum. Ours
54/// is `u64::MAX` for the same reason: it is a value a real deadline cannot take,
55/// so no field needs a second byte saying whether the first eight mean anything.
56const NONE: u64 = u64::MAX;
57
58/// The largest deadline a field can be given, in unix milliseconds.
59///
60/// Redis's `HFE_MAX_ABS_TIME_MSEC`, which is `EB_EXPIRE_TIME_MAX >> 2` where the
61/// maximum is 48 bits. It lands in the year 4200 or so. Anything past it is
62/// rejected by the command with `invalid expire time`, before any field is
63/// touched, so a command that names ten fields either sets all ten or errors.
64pub const MAX_AT: u64 = 0x0000_3FFF_FFFF_FFFF;
65
66/// Whether a deadline is one a field is allowed to be given.
67///
68/// The command layer checks this before it starts, because Redis rejects the
69/// whole command rather than failing field by field.
70#[must_use]
71pub const fn valid_at(ms: u64) -> bool {
72 ms <= MAX_AT
73}
74
75/// The condition on a deadline being set, which is `NX`, `XX`, `GT` or `LT`.
76///
77/// A hash field command takes exactly one of the four keywords. `EXPIRE` on a
78/// whole key takes a set of them and accepts two, `XX GT` and `XX LT`, which is
79/// where [`Cond::LessAndSet`] comes from. `XX GT` is not a sixth case because
80/// `GT` already refuses a key with no deadline, so it means the same thing as
81/// `GT` alone.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83pub enum Cond {
84 /// No condition. Set it whatever was there.
85 #[default]
86 Always,
87 /// Only if the field has no deadline now.
88 NotSet,
89 /// Only if the field has one now.
90 AlreadySet,
91 /// Only if it moves the deadline later, or there was none.
92 Greater,
93 /// Only if it moves the deadline earlier, or there was none.
94 Less,
95 /// Only if there is one now and this moves it earlier.
96 ///
97 /// `XX LT` on a key. It is a separate case because `LT` on its own accepts a
98 /// key with no deadline, on the reading that no deadline is infinitely far
99 /// away, and `XX` is what takes that reading away.
100 LessAndSet,
101}
102
103/// What setting a deadline did.
104///
105/// Not called `Set`, because [`crate::Set`] is a set and two types with that
106/// name in one crate is a trap for whoever reads it next.
107///
108/// The numbers are Redis's, from the `SetExRes` enum in `t_hash.c`, and they are
109/// what `HEXPIRE` puts in its reply array. They are here rather than in the
110/// protocol layer because they are semantics: whether a past deadline deletes the
111/// field or stores it is a decision about the data structure, and the reply just
112/// reports it.
113///
114/// `EXPIRE` on a whole key produces the same four outcomes and reports them with
115/// two numbers instead of four, so [`Keyspace::expire`] answers this and the wire
116/// folds it. `EXPIRE` cannot tell you whether the key went away or the deadline
117/// went on, and this can.
118///
119/// [`Keyspace::expire`]: crate::Keyspace::expire
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum Applied {
122 /// No such field, or no such key. Redis replies -2 for a field, and does the
123 /// same for a hash that is not there at all, because a missing key and an
124 /// empty hash are the same thing.
125 Missing = -2,
126 /// The condition was not met, so nothing changed. Redis replies 0.
127 NotMet = 0,
128 /// Set or updated. Redis replies 1.
129 Ok = 1,
130 /// The deadline was already in the past, so the field is gone rather than
131 /// expiring later. Redis replies 2, and the caller has to actually remove the
132 /// field, because this structure holds deadlines and not fields.
133 Deleted = 2,
134}
135
136/// What asking about a deadline found.
137///
138/// Redis's `HFE_GET_*` and `HFE_PERSIST_*` codes, which agree with each other on
139/// -2 and -1 and are the same two questions.
140///
141/// `TTL` and `PTTL` on a whole key ask the same three way question and answer it
142/// with the same two sentinels, so this says field or key rather than field. A
143/// key that is not there is -2 and a key with no deadline is -1, which is what
144/// makes the hash field commands and the key commands one shape and not two.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum Ask {
147 /// No such field, or no such key. Redis replies -2.
148 Missing,
149 /// It is there and has no deadline. Redis replies -1.
150 NoDeadline,
151 /// It is there and has one.
152 At(u64),
153}
154
155impl Ask {
156 /// The number Redis puts in the reply, given the moment being asked at.
157 ///
158 /// `HTTL` and `HPTTL` want what is left rather than when it falls due, and a
159 /// deadline that has passed reads as no field, because the field is about to
160 /// be reclaimed by whatever touched it.
161 #[must_use]
162 pub const fn remaining_ms(self, now: u64) -> i64 {
163 match self {
164 Ask::Missing => -2,
165 Ask::NoDeadline => -1,
166 Ask::At(at) if at <= now => -2,
167 // The arm above leaves `at > now`, and a deadline is under 2^46, so
168 // this neither underflows nor overruns an i64.
169 Ask::At(at) => (at - now) as i64,
170 }
171 }
172}
173
174/// The deadlines of one collection's fields, indexed by row position.
175///
176/// Empty and unallocated until a field is given one.
177#[derive(Debug, Clone, Default, PartialEq, Eq)]
178pub struct Deadlines {
179 /// One entry per row once allocated, [`NONE`] for a field without a deadline,
180 /// and empty while no field has ever had one.
181 at: Vec<u64>,
182 /// How many rows the table has, tracked so the array can be allocated at the
183 /// right length the moment it is first needed.
184 rows: usize,
185 /// How many of them carry a deadline.
186 live: usize,
187 /// A lower bound on the earliest deadline here, or [`NONE`].
188 ///
189 /// A bound and not the answer: it goes down when a deadline is set and it
190 /// does not go back up when that field is persisted or removed, because
191 /// finding the new earliest would mean a walk on every removal.
192 /// [`Deadlines::soonest`] is therefore never later than the truth, which is
193 /// the direction that keeps the active cycle correct: it can wake early and
194 /// find nothing, but it cannot sleep through an expiry.
195 soonest: u64,
196}
197
198impl Deadlines {
199 /// A collection with no deadlines on anything, which allocates nothing.
200 #[must_use]
201 pub const fn new() -> Deadlines {
202 Deadlines {
203 at: Vec::new(),
204 rows: 0,
205 live: 0,
206 soonest: NONE,
207 }
208 }
209
210 /// Whether any field here has a deadline.
211 ///
212 /// False means every read path can skip this structure entirely, which is the
213 /// case worth being fast and is the usual one.
214 #[inline]
215 #[must_use]
216 pub const fn is_empty(&self) -> bool {
217 self.live == 0
218 }
219
220 /// How many fields carry a deadline.
221 #[inline]
222 #[must_use]
223 pub const fn len(&self) -> usize {
224 self.live
225 }
226
227 /// Whether the array is there at all, which says the collection has been
228 /// given a deadline at some point rather than that it has one now.
229 ///
230 /// The active cycle asks this and not [`Deadlines::is_empty`], because what
231 /// it is deciding is whether to keep this collection on the list of ones
232 /// worth looking at, and a hash whose only deadline was just persisted can
233 /// be given another one without the list hearing about it.
234 #[inline]
235 #[must_use]
236 pub const fn armed(&self) -> bool {
237 !self.at.is_empty()
238 }
239
240 /// A lower bound on the earliest deadline, or `None` if nothing has one.
241 ///
242 /// What M5's active cycle registers so it can pass over a whole hash without
243 /// walking its fields. Never later than the real earliest deadline, so acting
244 /// on it can waste a walk but cannot miss an expiry.
245 #[inline]
246 #[must_use]
247 pub const fn soonest(&self) -> Option<u64> {
248 if self.soonest == NONE {
249 None
250 } else {
251 Some(self.soonest)
252 }
253 }
254
255 /// Recompute [`Deadlines::soonest`] exactly.
256 ///
257 /// The bound only ever drifts early, and a hash whose earliest field has been
258 /// persisted or expired keeps waking the cycle up for nothing until someone
259 /// pays for this walk. The active cycle is the one to pay it, once it has
260 /// walked the fields anyway and knows the bound was stale.
261 pub fn refresh_soonest(&mut self) {
262 self.soonest = self.at.iter().copied().min().unwrap_or(NONE);
263 }
264
265 /// A row was added to the table, so add a slot for it.
266 ///
267 /// Must be called for every row the table gains, including ones that will
268 /// never have a deadline, or the array stops lining up with the rows.
269 #[inline]
270 pub fn inserted(&mut self) {
271 self.rows += 1;
272 if !self.at.is_empty() {
273 self.at.push(NONE);
274 }
275 self.check();
276 }
277
278 /// The array is either not there at all or exactly as long as the table.
279 ///
280 /// Anything else means a hash has added or removed a row without telling this
281 /// structure, and the deadlines from that point on belong to the wrong
282 /// fields. Debug only, because it is one comparison on a path that runs per
283 /// field, but a release build that has drifted is a release build that was
284 /// already wrong in a test.
285 #[inline]
286 fn check(&self) {
287 debug_assert!(
288 self.at.is_empty() || self.at.len() == self.rows,
289 "the deadlines have drifted from the table: {} against {} rows",
290 self.at.len(),
291 self.rows
292 );
293 }
294
295 /// A row was taken out at `row`, the same way the table takes one out.
296 ///
297 /// [`Elements::remove_at`](crate::Elements::remove_at) moves the last row into
298 /// the hole, so this does exactly that to the deadlines. Any other order and
299 /// the deadlines after the hole belong to the wrong fields.
300 pub fn removed(&mut self, row: usize) {
301 debug_assert!(row < self.rows, "removing a row that is not there");
302 self.rows -= 1;
303 if !self.at.is_empty() {
304 if self.at.swap_remove(row) != NONE {
305 self.live -= 1;
306 }
307 if self.live == 0 {
308 // Nothing left to hold, so give the memory back rather than
309 // keeping an array of NONE for a hash that has stopped using
310 // deadlines. The next set reallocates, which is the same price it
311 // paid the first time.
312 self.at = Vec::new();
313 self.soonest = NONE;
314 }
315 }
316 self.check();
317 }
318
319 /// Everything went, so forget everything.
320 pub fn cleared(&mut self) {
321 self.at = Vec::new();
322 self.rows = 0;
323 self.live = 0;
324 self.soonest = NONE;
325 }
326
327 /// The deadline on a row, if it has one.
328 ///
329 /// The read on the hot path, and the reason the array is indexed by the row
330 /// position: after a probe the position is already in a register.
331 #[inline]
332 #[must_use]
333 pub fn get(&self, row: usize) -> Option<u64> {
334 match self.at.get(row) {
335 Some(&NONE) | None => None,
336 Some(&at) => Some(at),
337 }
338 }
339
340 /// Whether a row is past its deadline at `now`.
341 ///
342 /// The lazy expiry check, which every read of a field has to make. A hash
343 /// with no deadlines answers it without touching memory beyond the counter.
344 #[inline]
345 #[must_use]
346 pub fn is_expired(&self, row: usize, now: u64) -> bool {
347 if self.live == 0 {
348 return false;
349 }
350 match self.at.get(row) {
351 Some(&at) => at != NONE && at <= now,
352 None => false,
353 }
354 }
355
356 /// What `HTTL` and `HPERSIST` want to know about a row.
357 ///
358 /// The caller has already established the field exists, so this never answers
359 /// [`Ask::Missing`]. That case belongs to the table, not here.
360 #[inline]
361 #[must_use]
362 pub fn ask(&self, row: usize) -> Ask {
363 match self.get(row) {
364 Some(at) => Ask::At(at),
365 None => Ask::NoDeadline,
366 }
367 }
368
369 /// Put a deadline on a row, or find out why not.
370 ///
371 /// `at` is absolute unix milliseconds, which is what `HEXPIRE` and its
372 /// relatives all turn into before they get here, and the command layer has
373 /// already checked it against [`MAX_AT`].
374 ///
375 /// [`Applied::Deleted`] means the deadline had already passed. The deadline is not
376 /// stored and the caller has to remove the field, which is Redis's behaviour
377 /// and is why `HEXPIRE key 0 FIELDS 1 f` is a roundabout `HDEL`.
378 pub fn set(&mut self, row: usize, at: u64, cond: Cond, now: u64) -> Applied {
379 debug_assert!(
380 row < self.rows,
381 "setting a deadline on a row that is not there"
382 );
383 self.check();
384 let prev = self.get(row);
385 match decide(prev, at, cond, now) {
386 Applied::Ok => {}
387 // Redis clears nothing on a past deadline: it deletes the field, and
388 // the field taking its deadline with it is the caller calling
389 // removed().
390 other => return other,
391 }
392 if self.at.is_empty() {
393 // First deadline in this collection. This is the allocation Y20 is
394 // about, and everything above this line has happened without one.
395 self.at = vec![NONE; self.rows];
396 }
397 if prev.is_none() {
398 self.live += 1;
399 }
400 self.at[row] = at;
401 self.soonest = self.soonest.min(at);
402 Applied::Ok
403 }
404
405 /// Take a row's deadline off, which is `HPERSIST`.
406 ///
407 /// [`Ask::NoDeadline`] means there was nothing to take off, which Redis
408 /// replies -1 to, and [`Ask::At`] hands back the deadline that was there.
409 pub fn clear(&mut self, row: usize) -> Ask {
410 let Some(was) = self.get(row) else {
411 return Ask::NoDeadline;
412 };
413 self.at[row] = NONE;
414 self.live -= 1;
415 if self.live == 0 {
416 self.at = Vec::new();
417 self.soonest = NONE;
418 }
419 Ask::At(was)
420 }
421
422 /// What this costs, for `MEMORY USAGE` and for G8's per field number.
423 ///
424 /// Zero until a field is given a deadline, which is the entire point.
425 #[must_use]
426 pub fn memory_bytes(&self) -> usize {
427 self.at.capacity() * size_of::<u64>()
428 }
429}
430
431/// What setting this deadline over that one does, before anything is stored.
432///
433/// Public because a hash in the listpack band keeps its deadlines in the
434/// listpack rather than in a [`Deadlines`], and the rule about what `NX`, `XX`,
435/// `GT` and `LT` allow has to be one rule and not one per band. The band that
436/// has a [`Deadlines`] reaches it through [`Deadlines::set`], and the band that
437/// does not calls this and then writes the number itself.
438///
439/// Never answers [`Applied::Missing`], since establishing that the field exists
440/// is what the caller did to have a `prev` to pass in.
441#[must_use]
442pub const fn decide(prev: Option<u64>, at: u64, cond: Cond, now: u64) -> Applied {
443 if !allowed(prev, at, cond) {
444 return Applied::NotMet;
445 }
446 // The condition is checked first, so HEXPIRE 0 XX on a field with no
447 // deadline is 0 and not 2, and the field survives it.
448 if at <= now {
449 return Applied::Deleted;
450 }
451 Applied::Ok
452}
453
454/// Whether the condition lets this deadline replace that one.
455///
456/// Straight off `hashTypeSetExpiryListpack` in Redis 8.10.1. The asymmetry in the
457/// first arm is theirs and is easy to get backwards: against a field with no
458/// deadline, `XX` and `GT` fail, while `LT` passes, on the reading that no
459/// deadline is infinitely far away and so anything is less than it.
460const fn allowed(prev: Option<u64>, at: u64, cond: Cond) -> bool {
461 match (prev, cond) {
462 (_, Cond::Always) => true,
463 (None, Cond::AlreadySet | Cond::Greater | Cond::LessAndSet) => false,
464 (None, Cond::NotSet | Cond::Less) => true,
465 (Some(_), Cond::NotSet) => false,
466 (Some(_), Cond::AlreadySet) => true,
467 (Some(p), Cond::Greater) => p < at,
468 (Some(p), Cond::Less | Cond::LessAndSet) => p > at,
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use crate::Elements;
476
477 /// A `Deadlines` for a collection that already has `n` rows.
478 fn with_rows(n: usize) -> Deadlines {
479 let mut d = Deadlines::new();
480 for _ in 0..n {
481 d.inserted();
482 }
483 d
484 }
485
486 #[test]
487 fn a_collection_with_no_field_ttl_costs_nothing() {
488 let mut d = with_rows(1000);
489 assert!(d.is_empty());
490 assert_eq!(d.memory_bytes(), 0);
491 assert_eq!(d.soonest(), None);
492 assert!(!d.is_expired(0, u64::MAX));
493 assert_eq!(d.ask(7), Ask::NoDeadline);
494
495 // And the first deadline is what pays for the array.
496 assert_eq!(d.set(7, 5000, Cond::Always, 0), Applied::Ok);
497 assert_eq!(d.memory_bytes(), 8000);
498 assert_eq!(d.len(), 1);
499 }
500
501 #[test]
502 fn a_deadline_goes_on_and_comes_off() {
503 let mut d = with_rows(4);
504 assert_eq!(d.set(2, 900, Cond::Always, 100), Applied::Ok);
505 assert_eq!(d.get(2), Some(900));
506 assert_eq!(d.ask(2), Ask::At(900));
507 assert_eq!(d.get(1), None);
508
509 assert_eq!(d.clear(2), Ask::At(900));
510 assert_eq!(d.get(2), None);
511 assert_eq!(
512 d.clear(2),
513 Ask::NoDeadline,
514 "twice is not an error, it is -1"
515 );
516 assert!(d.is_empty());
517 assert_eq!(d.memory_bytes(), 0, "the last one off gives the array back");
518 }
519
520 /// A field is gone when something looks at it and not before.
521 #[test]
522 fn a_field_is_expired_only_once_its_moment_has_passed() {
523 let mut d = with_rows(2);
524 d.set(0, 1000, Cond::Always, 0);
525 assert!(!d.is_expired(0, 999));
526 assert!(d.is_expired(0, 1000), "the deadline itself has passed");
527 assert!(d.is_expired(0, 1001));
528 assert!(!d.is_expired(1, u64::MAX), "no deadline is not expired");
529 }
530
531 /// Redis replies 2 and deletes the field rather than storing a deadline that
532 /// has already gone, which is what makes `HEXPIRE key 0` an `HDEL`.
533 #[test]
534 fn a_deadline_in_the_past_deletes_instead_of_being_stored() {
535 let mut d = with_rows(2);
536 assert_eq!(
537 d.set(0, 500, Cond::Always, 500),
538 Applied::Deleted,
539 "now counts"
540 );
541 assert_eq!(d.set(0, 499, Cond::Always, 500), Applied::Deleted);
542 assert_eq!(d.get(0), None, "nothing was stored");
543 assert_eq!(d.memory_bytes(), 0, "and nothing was allocated for it");
544 }
545
546 /// The conditions, against the C in `hashTypeSetExpiryListpack`. The first
547 /// group is the one that is easy to get backwards.
548 #[test]
549 fn the_conditions_are_the_ones_redis_applies() {
550 // No deadline yet: XX and GT fail, NX and LT pass.
551 assert!(!allowed(None, 100, Cond::AlreadySet));
552 assert!(!allowed(None, 100, Cond::Greater));
553 assert!(allowed(None, 100, Cond::NotSet));
554 assert!(allowed(None, 100, Cond::Less));
555 assert!(allowed(None, 100, Cond::Always));
556
557 // Already has one: NX fails, XX passes.
558 assert!(!allowed(Some(50), 100, Cond::NotSet));
559 assert!(allowed(Some(50), 100, Cond::AlreadySet));
560
561 // GT wants strictly later, LT strictly earlier, and equal fails both.
562 assert!(allowed(Some(50), 100, Cond::Greater));
563 assert!(!allowed(Some(50), 20, Cond::Greater));
564 assert!(!allowed(Some(50), 50, Cond::Greater));
565 assert!(allowed(Some(50), 20, Cond::Less));
566 assert!(!allowed(Some(50), 100, Cond::Less));
567 assert!(!allowed(Some(50), 50, Cond::Less));
568 }
569
570 #[test]
571 fn a_condition_that_fails_changes_nothing() {
572 let mut d = with_rows(2);
573 d.set(0, 1000, Cond::Always, 0);
574 assert_eq!(d.set(0, 500, Cond::Greater, 0), Applied::NotMet);
575 assert_eq!(d.get(0), Some(1000));
576 assert_eq!(d.set(1, 500, Cond::AlreadySet, 0), Applied::NotMet);
577 assert_eq!(d.get(1), None);
578 assert_eq!(d.len(), 1);
579 }
580
581 /// The condition is checked before the past deadline is, so `HEXPIRE ... 0 XX`
582 /// on a field with no deadline is 0 and not 2, and the field survives.
583 #[test]
584 fn a_failed_condition_beats_a_past_deadline() {
585 let mut d = with_rows(1);
586 assert_eq!(d.set(0, 0, Cond::AlreadySet, 100), Applied::NotMet);
587 }
588
589 /// The one that would silently corrupt every deadline after a hole.
590 #[test]
591 fn deadlines_follow_the_table_through_a_swap_remove() {
592 let mut table: Elements<u32> = Elements::new();
593 let mut d = Deadlines::new();
594 for i in 0..5u32 {
595 table.insert(format!("f{i}").as_bytes(), i).expect("room");
596 d.inserted();
597 }
598 // Every field gets its index as its deadline, so a deadline that has
599 // drifted onto the wrong field is visible rather than plausible.
600 for i in 0..5 {
601 d.set(i, 1000 + i as u64, Cond::Always, 0);
602 }
603
604 // Take out the middle one, which moves the last row into its place.
605 let at = table.iter().position(|(n, _)| n == b"f1").expect("there");
606 table.remove_at(at).expect("there");
607 d.removed(at);
608
609 assert_eq!(table.len(), 4);
610 for (row, (name, _)) in table.iter().enumerate() {
611 let i: u64 = String::from_utf8_lossy(&name[1..]).parse().expect("f<n>");
612 assert_eq!(
613 d.get(row),
614 Some(1000 + i),
615 "field {} kept someone else's deadline",
616 String::from_utf8_lossy(name)
617 );
618 }
619 }
620
621 #[test]
622 fn removing_the_last_field_with_a_deadline_gives_the_array_back() {
623 let mut d = with_rows(3);
624 d.set(1, 1000, Cond::Always, 0);
625 assert_eq!(d.memory_bytes(), 24);
626 d.removed(1);
627 assert!(d.is_empty());
628 assert_eq!(d.memory_bytes(), 0);
629 assert_eq!(d.soonest(), None);
630 }
631
632 /// The bound is allowed to be early and is not allowed to be late, because
633 /// early wastes a walk and late misses an expiry.
634 #[test]
635 fn the_soonest_deadline_is_a_bound_and_leans_early() {
636 let mut d = with_rows(3);
637 d.set(0, 5000, Cond::Always, 0);
638 d.set(1, 3000, Cond::Always, 0);
639 d.set(2, 9000, Cond::Always, 0);
640 assert_eq!(d.soonest(), Some(3000));
641
642 // Persisting the earliest leaves the bound behind, which is allowed.
643 d.clear(1);
644 assert_eq!(
645 d.soonest(),
646 Some(3000),
647 "still early, which is the safe way"
648 );
649 d.refresh_soonest();
650 assert_eq!(
651 d.soonest(),
652 Some(5000),
653 "and exact once someone pays for it"
654 );
655 }
656
657 #[test]
658 fn what_is_left_is_what_redis_replies() {
659 assert_eq!(Ask::Missing.remaining_ms(0), -2);
660 assert_eq!(Ask::NoDeadline.remaining_ms(0), -1);
661 assert_eq!(Ask::At(5000).remaining_ms(1000), 4000);
662 assert_eq!(Ask::At(5000).remaining_ms(5000), -2, "due now is gone");
663 assert_eq!(Ask::At(5000).remaining_ms(9000), -2);
664 }
665
666 #[test]
667 fn the_ceiling_is_the_one_redis_enforces() {
668 assert_eq!(MAX_AT, 0x0000_FFFF_FFFF_FFFF >> 2);
669 assert!(valid_at(MAX_AT));
670 assert!(!valid_at(MAX_AT + 1));
671 assert!(valid_at(0));
672 }
673
674 #[test]
675 fn clearing_the_collection_forgets_everything() {
676 let mut d = with_rows(3);
677 d.set(0, 1000, Cond::Always, 0);
678 d.cleared();
679 assert!(d.is_empty());
680 assert_eq!(d.memory_bytes(), 0);
681 assert_eq!(d.soonest(), None);
682 d.inserted();
683 assert_eq!(d.set(0, 1000, Cond::Always, 0), Applied::Ok);
684 }
685}