Skip to main content

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    /// A lower bound on the earliest deadline, or `None` if nothing has one.
228    ///
229    /// What M5's active cycle registers so it can pass over a whole hash without
230    /// walking its fields. Never later than the real earliest deadline, so acting
231    /// on it can waste a walk but cannot miss an expiry.
232    #[inline]
233    #[must_use]
234    pub const fn soonest(&self) -> Option<u64> {
235        if self.soonest == NONE {
236            None
237        } else {
238            Some(self.soonest)
239        }
240    }
241
242    /// Recompute [`Deadlines::soonest`] exactly.
243    ///
244    /// The bound only ever drifts early, and a hash whose earliest field has been
245    /// persisted or expired keeps waking the cycle up for nothing until someone
246    /// pays for this walk. The active cycle is the one to pay it, once it has
247    /// walked the fields anyway and knows the bound was stale.
248    pub fn refresh_soonest(&mut self) {
249        self.soonest = self.at.iter().copied().min().unwrap_or(NONE);
250    }
251
252    /// A row was added to the table, so add a slot for it.
253    ///
254    /// Must be called for every row the table gains, including ones that will
255    /// never have a deadline, or the array stops lining up with the rows.
256    #[inline]
257    pub fn inserted(&mut self) {
258        self.rows += 1;
259        if !self.at.is_empty() {
260            self.at.push(NONE);
261        }
262        self.check();
263    }
264
265    /// The array is either not there at all or exactly as long as the table.
266    ///
267    /// Anything else means a hash has added or removed a row without telling this
268    /// structure, and the deadlines from that point on belong to the wrong
269    /// fields. Debug only, because it is one comparison on a path that runs per
270    /// field, but a release build that has drifted is a release build that was
271    /// already wrong in a test.
272    #[inline]
273    fn check(&self) {
274        debug_assert!(
275            self.at.is_empty() || self.at.len() == self.rows,
276            "the deadlines have drifted from the table: {} against {} rows",
277            self.at.len(),
278            self.rows
279        );
280    }
281
282    /// A row was taken out at `row`, the same way the table takes one out.
283    ///
284    /// [`Elements::remove_at`](crate::Elements::remove_at) moves the last row into
285    /// the hole, so this does exactly that to the deadlines. Any other order and
286    /// the deadlines after the hole belong to the wrong fields.
287    pub fn removed(&mut self, row: usize) {
288        debug_assert!(row < self.rows, "removing a row that is not there");
289        self.rows -= 1;
290        if !self.at.is_empty() {
291            if self.at.swap_remove(row) != NONE {
292                self.live -= 1;
293            }
294            if self.live == 0 {
295                // Nothing left to hold, so give the memory back rather than
296                // keeping an array of NONE for a hash that has stopped using
297                // deadlines. The next set reallocates, which is the same price it
298                // paid the first time.
299                self.at = Vec::new();
300                self.soonest = NONE;
301            }
302        }
303        self.check();
304    }
305
306    /// Everything went, so forget everything.
307    pub fn cleared(&mut self) {
308        self.at = Vec::new();
309        self.rows = 0;
310        self.live = 0;
311        self.soonest = NONE;
312    }
313
314    /// The deadline on a row, if it has one.
315    ///
316    /// The read on the hot path, and the reason the array is indexed by the row
317    /// position: after a probe the position is already in a register.
318    #[inline]
319    #[must_use]
320    pub fn get(&self, row: usize) -> Option<u64> {
321        match self.at.get(row) {
322            Some(&NONE) | None => None,
323            Some(&at) => Some(at),
324        }
325    }
326
327    /// Whether a row is past its deadline at `now`.
328    ///
329    /// The lazy expiry check, which every read of a field has to make. A hash
330    /// with no deadlines answers it without touching memory beyond the counter.
331    #[inline]
332    #[must_use]
333    pub fn is_expired(&self, row: usize, now: u64) -> bool {
334        if self.live == 0 {
335            return false;
336        }
337        match self.at.get(row) {
338            Some(&at) => at != NONE && at <= now,
339            None => false,
340        }
341    }
342
343    /// What `HTTL` and `HPERSIST` want to know about a row.
344    ///
345    /// The caller has already established the field exists, so this never answers
346    /// [`Ask::Missing`]. That case belongs to the table, not here.
347    #[inline]
348    #[must_use]
349    pub fn ask(&self, row: usize) -> Ask {
350        match self.get(row) {
351            Some(at) => Ask::At(at),
352            None => Ask::NoDeadline,
353        }
354    }
355
356    /// Put a deadline on a row, or find out why not.
357    ///
358    /// `at` is absolute unix milliseconds, which is what `HEXPIRE` and its
359    /// relatives all turn into before they get here, and the command layer has
360    /// already checked it against [`MAX_AT`].
361    ///
362    /// [`Applied::Deleted`] means the deadline had already passed. The deadline is not
363    /// stored and the caller has to remove the field, which is Redis's behaviour
364    /// and is why `HEXPIRE key 0 FIELDS 1 f` is a roundabout `HDEL`.
365    pub fn set(&mut self, row: usize, at: u64, cond: Cond, now: u64) -> Applied {
366        debug_assert!(
367            row < self.rows,
368            "setting a deadline on a row that is not there"
369        );
370        self.check();
371        let prev = self.get(row);
372        match decide(prev, at, cond, now) {
373            Applied::Ok => {}
374            // Redis clears nothing on a past deadline: it deletes the field, and
375            // the field taking its deadline with it is the caller calling
376            // removed().
377            other => return other,
378        }
379        if self.at.is_empty() {
380            // First deadline in this collection. This is the allocation Y20 is
381            // about, and everything above this line has happened without one.
382            self.at = vec![NONE; self.rows];
383        }
384        if prev.is_none() {
385            self.live += 1;
386        }
387        self.at[row] = at;
388        self.soonest = self.soonest.min(at);
389        Applied::Ok
390    }
391
392    /// Take a row's deadline off, which is `HPERSIST`.
393    ///
394    /// [`Ask::NoDeadline`] means there was nothing to take off, which Redis
395    /// replies -1 to, and [`Ask::At`] hands back the deadline that was there.
396    pub fn clear(&mut self, row: usize) -> Ask {
397        let Some(was) = self.get(row) else {
398            return Ask::NoDeadline;
399        };
400        self.at[row] = NONE;
401        self.live -= 1;
402        if self.live == 0 {
403            self.at = Vec::new();
404            self.soonest = NONE;
405        }
406        Ask::At(was)
407    }
408
409    /// What this costs, for `MEMORY USAGE` and for G8's per field number.
410    ///
411    /// Zero until a field is given a deadline, which is the entire point.
412    #[must_use]
413    pub fn memory_bytes(&self) -> usize {
414        self.at.capacity() * size_of::<u64>()
415    }
416}
417
418/// What setting this deadline over that one does, before anything is stored.
419///
420/// Public because a hash in the listpack band keeps its deadlines in the
421/// listpack rather than in a [`Deadlines`], and the rule about what `NX`, `XX`,
422/// `GT` and `LT` allow has to be one rule and not one per band. The band that
423/// has a [`Deadlines`] reaches it through [`Deadlines::set`], and the band that
424/// does not calls this and then writes the number itself.
425///
426/// Never answers [`Applied::Missing`], since establishing that the field exists
427/// is what the caller did to have a `prev` to pass in.
428#[must_use]
429pub const fn decide(prev: Option<u64>, at: u64, cond: Cond, now: u64) -> Applied {
430    if !allowed(prev, at, cond) {
431        return Applied::NotMet;
432    }
433    // The condition is checked first, so HEXPIRE 0 XX on a field with no
434    // deadline is 0 and not 2, and the field survives it.
435    if at <= now {
436        return Applied::Deleted;
437    }
438    Applied::Ok
439}
440
441/// Whether the condition lets this deadline replace that one.
442///
443/// Straight off `hashTypeSetExpiryListpack` in Redis 8.10.1. The asymmetry in the
444/// first arm is theirs and is easy to get backwards: against a field with no
445/// deadline, `XX` and `GT` fail, while `LT` passes, on the reading that no
446/// deadline is infinitely far away and so anything is less than it.
447const fn allowed(prev: Option<u64>, at: u64, cond: Cond) -> bool {
448    match (prev, cond) {
449        (_, Cond::Always) => true,
450        (None, Cond::AlreadySet | Cond::Greater | Cond::LessAndSet) => false,
451        (None, Cond::NotSet | Cond::Less) => true,
452        (Some(_), Cond::NotSet) => false,
453        (Some(_), Cond::AlreadySet) => true,
454        (Some(p), Cond::Greater) => p < at,
455        (Some(p), Cond::Less | Cond::LessAndSet) => p > at,
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use crate::Elements;
463
464    /// A `Deadlines` for a collection that already has `n` rows.
465    fn with_rows(n: usize) -> Deadlines {
466        let mut d = Deadlines::new();
467        for _ in 0..n {
468            d.inserted();
469        }
470        d
471    }
472
473    #[test]
474    fn a_collection_with_no_field_ttl_costs_nothing() {
475        let mut d = with_rows(1000);
476        assert!(d.is_empty());
477        assert_eq!(d.memory_bytes(), 0);
478        assert_eq!(d.soonest(), None);
479        assert!(!d.is_expired(0, u64::MAX));
480        assert_eq!(d.ask(7), Ask::NoDeadline);
481
482        // And the first deadline is what pays for the array.
483        assert_eq!(d.set(7, 5000, Cond::Always, 0), Applied::Ok);
484        assert_eq!(d.memory_bytes(), 8000);
485        assert_eq!(d.len(), 1);
486    }
487
488    #[test]
489    fn a_deadline_goes_on_and_comes_off() {
490        let mut d = with_rows(4);
491        assert_eq!(d.set(2, 900, Cond::Always, 100), Applied::Ok);
492        assert_eq!(d.get(2), Some(900));
493        assert_eq!(d.ask(2), Ask::At(900));
494        assert_eq!(d.get(1), None);
495
496        assert_eq!(d.clear(2), Ask::At(900));
497        assert_eq!(d.get(2), None);
498        assert_eq!(
499            d.clear(2),
500            Ask::NoDeadline,
501            "twice is not an error, it is -1"
502        );
503        assert!(d.is_empty());
504        assert_eq!(d.memory_bytes(), 0, "the last one off gives the array back");
505    }
506
507    /// A field is gone when something looks at it and not before.
508    #[test]
509    fn a_field_is_expired_only_once_its_moment_has_passed() {
510        let mut d = with_rows(2);
511        d.set(0, 1000, Cond::Always, 0);
512        assert!(!d.is_expired(0, 999));
513        assert!(d.is_expired(0, 1000), "the deadline itself has passed");
514        assert!(d.is_expired(0, 1001));
515        assert!(!d.is_expired(1, u64::MAX), "no deadline is not expired");
516    }
517
518    /// Redis replies 2 and deletes the field rather than storing a deadline that
519    /// has already gone, which is what makes `HEXPIRE key 0` an `HDEL`.
520    #[test]
521    fn a_deadline_in_the_past_deletes_instead_of_being_stored() {
522        let mut d = with_rows(2);
523        assert_eq!(
524            d.set(0, 500, Cond::Always, 500),
525            Applied::Deleted,
526            "now counts"
527        );
528        assert_eq!(d.set(0, 499, Cond::Always, 500), Applied::Deleted);
529        assert_eq!(d.get(0), None, "nothing was stored");
530        assert_eq!(d.memory_bytes(), 0, "and nothing was allocated for it");
531    }
532
533    /// The conditions, against the C in `hashTypeSetExpiryListpack`. The first
534    /// group is the one that is easy to get backwards.
535    #[test]
536    fn the_conditions_are_the_ones_redis_applies() {
537        // No deadline yet: XX and GT fail, NX and LT pass.
538        assert!(!allowed(None, 100, Cond::AlreadySet));
539        assert!(!allowed(None, 100, Cond::Greater));
540        assert!(allowed(None, 100, Cond::NotSet));
541        assert!(allowed(None, 100, Cond::Less));
542        assert!(allowed(None, 100, Cond::Always));
543
544        // Already has one: NX fails, XX passes.
545        assert!(!allowed(Some(50), 100, Cond::NotSet));
546        assert!(allowed(Some(50), 100, Cond::AlreadySet));
547
548        // GT wants strictly later, LT strictly earlier, and equal fails both.
549        assert!(allowed(Some(50), 100, Cond::Greater));
550        assert!(!allowed(Some(50), 20, Cond::Greater));
551        assert!(!allowed(Some(50), 50, Cond::Greater));
552        assert!(allowed(Some(50), 20, Cond::Less));
553        assert!(!allowed(Some(50), 100, Cond::Less));
554        assert!(!allowed(Some(50), 50, Cond::Less));
555    }
556
557    #[test]
558    fn a_condition_that_fails_changes_nothing() {
559        let mut d = with_rows(2);
560        d.set(0, 1000, Cond::Always, 0);
561        assert_eq!(d.set(0, 500, Cond::Greater, 0), Applied::NotMet);
562        assert_eq!(d.get(0), Some(1000));
563        assert_eq!(d.set(1, 500, Cond::AlreadySet, 0), Applied::NotMet);
564        assert_eq!(d.get(1), None);
565        assert_eq!(d.len(), 1);
566    }
567
568    /// The condition is checked before the past deadline is, so `HEXPIRE ... 0 XX`
569    /// on a field with no deadline is 0 and not 2, and the field survives.
570    #[test]
571    fn a_failed_condition_beats_a_past_deadline() {
572        let mut d = with_rows(1);
573        assert_eq!(d.set(0, 0, Cond::AlreadySet, 100), Applied::NotMet);
574    }
575
576    /// The one that would silently corrupt every deadline after a hole.
577    #[test]
578    fn deadlines_follow_the_table_through_a_swap_remove() {
579        let mut table: Elements<u32> = Elements::new();
580        let mut d = Deadlines::new();
581        for i in 0..5u32 {
582            table.insert(format!("f{i}").as_bytes(), i).expect("room");
583            d.inserted();
584        }
585        // Every field gets its index as its deadline, so a deadline that has
586        // drifted onto the wrong field is visible rather than plausible.
587        for i in 0..5 {
588            d.set(i, 1000 + i as u64, Cond::Always, 0);
589        }
590
591        // Take out the middle one, which moves the last row into its place.
592        let at = table.iter().position(|(n, _)| n == b"f1").expect("there");
593        table.remove_at(at).expect("there");
594        d.removed(at);
595
596        assert_eq!(table.len(), 4);
597        for (row, (name, _)) in table.iter().enumerate() {
598            let i: u64 = String::from_utf8_lossy(&name[1..]).parse().expect("f<n>");
599            assert_eq!(
600                d.get(row),
601                Some(1000 + i),
602                "field {} kept someone else's deadline",
603                String::from_utf8_lossy(name)
604            );
605        }
606    }
607
608    #[test]
609    fn removing_the_last_field_with_a_deadline_gives_the_array_back() {
610        let mut d = with_rows(3);
611        d.set(1, 1000, Cond::Always, 0);
612        assert_eq!(d.memory_bytes(), 24);
613        d.removed(1);
614        assert!(d.is_empty());
615        assert_eq!(d.memory_bytes(), 0);
616        assert_eq!(d.soonest(), None);
617    }
618
619    /// The bound is allowed to be early and is not allowed to be late, because
620    /// early wastes a walk and late misses an expiry.
621    #[test]
622    fn the_soonest_deadline_is_a_bound_and_leans_early() {
623        let mut d = with_rows(3);
624        d.set(0, 5000, Cond::Always, 0);
625        d.set(1, 3000, Cond::Always, 0);
626        d.set(2, 9000, Cond::Always, 0);
627        assert_eq!(d.soonest(), Some(3000));
628
629        // Persisting the earliest leaves the bound behind, which is allowed.
630        d.clear(1);
631        assert_eq!(
632            d.soonest(),
633            Some(3000),
634            "still early, which is the safe way"
635        );
636        d.refresh_soonest();
637        assert_eq!(
638            d.soonest(),
639            Some(5000),
640            "and exact once someone pays for it"
641        );
642    }
643
644    #[test]
645    fn what_is_left_is_what_redis_replies() {
646        assert_eq!(Ask::Missing.remaining_ms(0), -2);
647        assert_eq!(Ask::NoDeadline.remaining_ms(0), -1);
648        assert_eq!(Ask::At(5000).remaining_ms(1000), 4000);
649        assert_eq!(Ask::At(5000).remaining_ms(5000), -2, "due now is gone");
650        assert_eq!(Ask::At(5000).remaining_ms(9000), -2);
651    }
652
653    #[test]
654    fn the_ceiling_is_the_one_redis_enforces() {
655        assert_eq!(MAX_AT, 0x0000_FFFF_FFFF_FFFF >> 2);
656        assert!(valid_at(MAX_AT));
657        assert!(!valid_at(MAX_AT + 1));
658        assert!(valid_at(0));
659    }
660
661    #[test]
662    fn clearing_the_collection_forgets_everything() {
663        let mut d = with_rows(3);
664        d.set(0, 1000, Cond::Always, 0);
665        d.cleared();
666        assert!(d.is_empty());
667        assert_eq!(d.memory_bytes(), 0);
668        assert_eq!(d.soonest(), None);
669        d.inserted();
670        assert_eq!(d.set(0, 1000, Cond::Always, 0), Applied::Ok);
671    }
672}