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}