Skip to main content

yo_kv/
tier.rs

1//! Moving a value out to the file and getting it back, which is WiscKey's idea
2//! with the tag from `06` section 6 doing the bookkeeping.
3//!
4//! Three pieces already exist and this is what joins them. [`cold`]
5//! knows how to lay a value out on the file. [`value`] knows how
6//! to write a record that points at one and how to tell in one bit whether a
7//! record does. [`demote`](crate::demote) knows how to choose. What was missing
8//! is the thing that reads a record, writes its bytes out, replaces it with a
9//! twelve byte pointer, and does the reverse on the way back.
10//!
11//! # What separation buys, exactly
12//!
13//! A resident string record is one meta byte, three access bytes, eight more if
14//! it has a deadline, and then the value. A demoted one is the same head with
15//! twelve bytes of address and length instead of the value. So demotion pays
16//! from thirteen payload bytes upward and costs memory below that, which is why
17//! [`worth_demoting`] is arithmetic on the two lengths and not a tunable. There
18//! is no threshold to get wrong.
19//!
20//! What is kept in memory is chosen the same way: the deadline, the access
21//! field, the kind and the encoding all stay, so `TTL`, `TYPE`, `OBJECT
22//! ENCODING`, `STRLEN`, `EXISTS` and every eviction policy still answer at
23//! memory speed on a key whose bytes are on the device. G9's budget of 1.05
24//! device reads per point read is spent on reads that actually want bytes.
25//!
26//! # The doorkeeper, and why a fault is not a promotion
27//!
28//! Reading a demoted value does not bring it back. The first read of a key sets
29//! its bits in the doorkeeper and serves from the file; a second read while
30//! those bits are still there brings it into memory. So a scan over cold data
31//! displaces nothing, and a key that is genuinely warming up pays one extra
32//! device read to prove it. That is the TinyLFU admission argument and it is the
33//! difference between a tier and a cache that thrashes.
34//!
35//! # Where the collections are
36//!
37//! This file moves strings, and only ones that are not int encoded. A collection
38//! keeps its body in a slab and its record holds a slab index, so moving one
39//! means freeing a slab slot and growing a record, and neither of those is
40//! reachable from here. The two halves it does own are [`Tier::stash`] and
41//! [`Tier::fetch`], which are the store side with the record side left out, and
42//! the rest is in `Keyspace::demote_body` and `Keyspace::promote_body` beside
43//! it.
44//!
45//! A demoted body arriving at [`Tier::fault`] is refused rather than served,
46//! because putting a value back here means writing a string record and that
47//! would turn a set into a string. The caller routes them, and the refusal is
48//! there so that a caller which forgets gets an error instead of a corrupted
49//! key.
50//!
51//! Victims are chosen by sampling, through the same [`evict::Pool`] eviction
52//! uses, rather than by the S3-FIFO and SIEVE queues in [`demote`](crate::demote).
53//! Those queues want a slot number per entry that is stable across an arena
54//! compaction, and this crate does not have one to give them: an address moves
55//! when a segment is evacuated and a key is the thing being looked up. Deciding
56//! where that number lives is a record layout question and it is the next one
57//! this milestone has to answer. Sampling is what eviction and the expire cycle
58//! already do, it needs nothing new, and it is a floor rather than a ceiling.
59//!
60//! # Space on the file
61//!
62//! Promoting a value leaves its chunks where they are. There is no delete on
63//! [`Blocks`] and there does not need to be one, because a chunk nobody points
64//! at is exactly what the log's compaction already collects, and the same is
65//! true of the chunks a crash leaves behind between the last chunk write and the
66//! directory write.
67
68use yo_common::{Code, Error, Result, Rng};
69use yo_index::RawMap;
70
71use crate::access::{Lfu, Policy};
72use crate::cold::{self, Blocks};
73use crate::demote::Doorkeeper;
74use crate::evict;
75use crate::value::{self, Encoding, Kind};
76
77/// How many keys the doorkeeper remembers before it clears itself.
78///
79/// Large enough that a read and the read that follows it a few thousand keys
80/// later still count as the same window, small enough that the filter does not
81/// saturate and start admitting everything. Both failure modes are the same
82/// failure, which is a doorkeeper that has stopped saying no.
83pub const WINDOW: usize = 8192;
84
85/// How many entries one round of sampling walks past before it gives up on
86/// finding its sixteen victims in this part of the keyspace.
87///
88/// Eviction does not need a number like this, because every entry it looks at
89/// is a candidate and sixteen entries is sixteen candidates. Demotion is not
90/// like that. A record that is already cold is skipped, and in a keyspace that
91/// is mostly cold, which is exactly the state a sweep spends most of its time
92/// in, nearly every entry a round walks is one it has to skip. Counting those
93/// against the round's budget makes the sweep stall with the last few percent
94/// of the keyspace still in memory, sitting a few buckets further along than
95/// the round was allowed to look.
96///
97/// So the budget counts victims found and this counts entries walked, purely so
98/// that a round over a segment holding nothing demotable still ends. It is
99/// larger than a segment on purpose: a barren round then means the segment it
100/// drew is genuinely clean, which is the thing [`BARREN`] wants to know.
101pub const WALK: usize = 1024;
102
103/// How many rounds of sampling have to come back with nothing before
104/// [`Tier::relieve`] accepts that there is nothing left to move.
105///
106/// A round covers the whole of one index segment, so a barren round is a
107/// segment with nothing left in it worth moving. Sixteen of those in a row,
108/// against segments drawn at random, is a keyspace that is done.
109///
110/// It has to be a run and not a single round because sampling picks its segment
111/// and its starting bucket out of one random draw, so two rounds that draw the
112/// same pair walk the same entries and the second one finds every one of them
113/// already moved. Stopping on the first barren round quit with ninety four
114/// percent of the keyspace still in memory.
115pub const BARREN: usize = 16;
116
117/// What happened to a read of a key that may not have been in memory.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum Faulted {
120    /// No such key. Nothing was read and nothing was written.
121    Missing,
122    /// The value was in memory all along, so the output buffer was not touched
123    /// and the caller should read the record the way it always does.
124    Warm,
125    /// Read from the file and deliberately left there, because one read is not
126    /// enough to earn a slot in memory back.
127    Served,
128    /// Read from the file and brought back into memory, so the next read of
129    /// this key does not touch the device.
130    Promoted,
131}
132
133/// The running totals, for `INFO` and for the gates.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
135pub struct Stats {
136    /// Values moved out to the file.
137    pub demoted: u64,
138    /// Values brought back into memory.
139    pub promoted: u64,
140    /// Reads that went to the device, whether or not they promoted. This over
141    /// the number of point reads is the ratio G9 is a gate on.
142    pub faults: u64,
143    /// Reads that went to the device and left the value there.
144    pub served: u64,
145    /// Payload bytes written to the file.
146    pub bytes_out: u64,
147    /// Payload bytes read back from it.
148    pub bytes_in: u64,
149}
150
151/// What a sweep did, which is two numbers because it does two things.
152///
153/// Kept apart rather than added up because they answer different questions.
154/// `moved` is how much colder the keyspace got and it is what a test about
155/// demotion is written against. `freed` is how much memory came back, and that
156/// is what a server holding itself to a limit has to read.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
158pub struct Relief {
159    /// Values that went out to the file.
160    pub moved: usize,
161    /// Bytes of memory the map gave back while that was happening, which is
162    /// segments the arena handed over and not records that got shorter.
163    pub freed: usize,
164}
165
166impl Relief {
167    /// Whether the sweep is worth calling again, which is the question the
168    /// caller with the limit is really asking.
169    ///
170    /// Either number being non zero is progress. Both being zero is a keyspace
171    /// with nothing left to move and no dead space to reclaim, and a write that
172    /// cannot be fitted in that is a write that has to be refused.
173    #[must_use]
174    pub const fn made_room(self) -> bool {
175        self.moved > 0 || self.freed > 0
176    }
177}
178
179/// Whether moving this record's value to the file would save memory.
180///
181/// Straight comparison of the two record lengths. A record whose value is short
182/// enough that the pointer costs more than the bytes is left alone, and that is
183/// the whole of the size policy.
184#[must_use]
185pub fn worth_demoting(rec: &[u8]) -> bool {
186    let m = value::Meta::from_byte(rec[0]);
187    if m.is_cold() || m.kind() != Kind::String || m.encoding() == Encoding::Int {
188        return false;
189    }
190    rec.len() > value::cold_record_len(m.has_expiry())
191}
192
193/// The tier, which owns the file side of the keyspace.
194pub struct Tier<B: Blocks> {
195    blocks: B,
196    door: Doorkeeper,
197    scratch: cold::Scratch,
198    pool: evict::Pool,
199    /// The key of the victim being worked on, so that taking it out of the pool
200    /// does not hold a borrow across the demotion.
201    keybuf: Vec<u8>,
202    rng: Rng,
203    stats: Stats,
204}
205
206impl<B: Blocks> Tier<B> {
207    /// A tier over `blocks`, with a doorkeeper of the default window.
208    pub fn new(blocks: B) -> Tier<B> {
209        Tier::with_window(blocks, WINDOW)
210    }
211
212    /// A tier whose doorkeeper remembers `window` keys.
213    pub fn with_window(blocks: B, window: usize) -> Tier<B> {
214        Tier {
215            blocks,
216            door: Doorkeeper::new(window),
217            scratch: cold::Scratch::new(),
218            pool: evict::Pool::new(),
219            keybuf: Vec::new(),
220            rng: Rng::new(0x5eed_1234_9abc_def0),
221            stats: Stats::default(),
222        }
223    }
224
225    /// What has happened so far.
226    #[must_use]
227    pub const fn stats(&self) -> Stats {
228        self.stats
229    }
230
231    /// How many bytes the store holds, which is what `maxstore` is compared
232    /// against.
233    ///
234    /// Asked of the store rather than added up here. [`Stats::bytes_out`] counts
235    /// payload that was written and never goes down, and a limit on the file has
236    /// to be a limit on the file.
237    #[must_use]
238    pub fn store_bytes(&self) -> u64 {
239        self.blocks.bytes()
240    }
241
242    /// The store, for a caller that has to flush or close it.
243    pub const fn blocks(&self) -> &B {
244        &self.blocks
245    }
246
247    /// The store, mutably, for the same reason.
248    pub const fn blocks_mut(&mut self) -> &mut B {
249        &mut self.blocks
250    }
251
252    /// What the tier's own buffers cost, which the memory report has to include
253    /// because they are not free and are not counted anywhere else.
254    #[must_use]
255    pub fn memory_bytes(&self) -> usize {
256        self.door.memory_bytes()
257            + self.scratch.memory_bytes()
258            + self.pool.memory_bytes()
259            + self.keybuf.capacity()
260    }
261
262    /// Move `key`'s value out to the file.
263    ///
264    /// `Ok(false)` when there is no such key, when it is already on the file,
265    /// or when moving it would cost more memory than it saves. None of those is
266    /// an error: a caller under memory pressure asks about a lot of keys and
267    /// most of the answers are no.
268    ///
269    /// # Errors
270    ///
271    /// Whatever the store says when it cannot take the bytes.
272    pub fn demote(&mut self, map: &mut RawMap, key: &[u8]) -> Result<bool> {
273        let Some(addr) = map.find(key) else {
274            return Ok(false);
275        };
276        let rec = map.value_at(addr);
277        if !worth_demoting(rec) {
278            return Ok(false);
279        }
280        let m = value::Meta::from_byte(rec[0]);
281        let (kind, enc) = (m.kind(), m.encoding());
282        let expire_at = value::expire_at(rec);
283        // Carried across rather than restamped. A key that was moved to the file
284        // was not used, and a demotion that looked like a use would make the
285        // next demotion pick the wrong victim.
286        let was = value::access(rec).unwrap_or_default();
287
288        let value::Str::Bytes(bytes) = value::read(rec) else {
289            // Int encoding is refused above, so this cannot happen, and if the
290            // encoding rules ever change it should be a no and not a panic.
291            return Ok(false);
292        };
293        let len = bytes.len() as u32;
294        let chain = cold::write(&mut self.blocks, bytes, &mut self.scratch)?;
295
296        let wrote = map.set_with(
297            key,
298            value::cold_record_len(expire_at.is_some()),
299            |_| {},
300            |out| {
301                value::write_cold_record(out, kind, enc, chain.at, len, expire_at);
302                value::set_access(out, was);
303                value::has_expiry(out)
304            },
305        );
306        debug_assert!(wrote.is_some(), "the key was found a moment ago");
307
308        self.stats.demoted += 1;
309        self.stats.bytes_out += u64::from(len);
310        Ok(true)
311    }
312
313    /// Write `bytes` to the file and answer where they went.
314    ///
315    /// The store half of demotion with the record half left out, which is what a
316    /// collection needs. A string's value is its record, so [`Tier::demote`] can
317    /// do both ends and does. A collection's body is in a slab and its record
318    /// holds a number, so the caller is the only one that can free the slot and
319    /// rewrite the record, and all it wants from here is the chain.
320    ///
321    /// # Errors
322    ///
323    /// Whatever the store says when it cannot take the bytes.
324    pub fn stash(&mut self, bytes: &[u8]) -> Result<cold::Chain> {
325        let chain = cold::write(&mut self.blocks, bytes, &mut self.scratch)?;
326        self.stats.demoted += 1;
327        self.stats.bytes_out += chain.len;
328        Ok(chain)
329    }
330
331    /// Read a chain back into `out`, which is cleared first.
332    ///
333    /// The other half of [`Tier::stash`], and the doorkeeper does not get a vote
334    /// here for the same reason it does not in [`Tier::thaw`]: a collection
335    /// command needs its body in a slab to answer at all, so there is no serving
336    /// it from the file and leaving it there. The read that costs one device read
337    /// is the read that promotes.
338    ///
339    /// # Errors
340    ///
341    /// Whatever the store says when the chain will not read back.
342    pub fn fetch(&mut self, chain: cold::Chain, out: &mut Vec<u8>) -> Result<()> {
343        out.clear();
344        out.reserve(chain.len as usize);
345        // Same order as in `read`, and for the same reason: the release goes
346        // before the borrows and not after, because after is inside the scope
347        // that owns them.
348        self.blocks.release();
349        {
350            let reader = cold::Reader::open(&self.blocks, chain)?;
351            for piece in reader.range(0, reader.len()) {
352                out.extend_from_slice(piece?);
353            }
354        }
355        self.stats.faults += 1;
356        self.stats.bytes_in += chain.len;
357        self.stats.promoted += 1;
358        Ok(())
359    }
360
361    /// Read `key`'s value, from the file if that is where it is.
362    ///
363    /// `out` is cleared and filled only when the answer is [`Faulted::Served`]
364    /// or [`Faulted::Promoted`]. It belongs to the caller so that a server can
365    /// keep one buffer per shard and a fault costs no allocation once it has
366    /// grown, which is Y7.
367    ///
368    /// # Errors
369    ///
370    /// Whatever the store says when the chain will not read back.
371    pub fn fault(&mut self, map: &mut RawMap, key: &[u8], out: &mut Vec<u8>) -> Result<Faulted> {
372        self.read(map, key, out, true)
373    }
374
375    /// Read `key`'s value and put it back in memory whatever the doorkeeper
376    /// thinks.
377    ///
378    /// This is for a command that is about to write the key. `APPEND` on a
379    /// demoted value reads it, adds to it and stores the result, and the result
380    /// is a resident record no matter which way the doorkeeper would have gone,
381    /// so asking it would be asking a question whose answer cannot be used. The
382    /// same goes for `INCR`, `SETRANGE`, `SETBIT`, `GETSET` and the rest of the
383    /// read modify write family.
384    ///
385    /// A promotion here still costs one device read and no more, and the value
386    /// it read is the one the caller was going to ask for anyway.
387    ///
388    /// # Errors
389    ///
390    /// Whatever the store says when the chain will not read back.
391    pub fn thaw(&mut self, map: &mut RawMap, key: &[u8], out: &mut Vec<u8>) -> Result<Faulted> {
392        self.read(map, key, out, false)
393    }
394
395    /// The body of both, with `ask` saying whether the doorkeeper gets a vote.
396    fn read(
397        &mut self,
398        map: &mut RawMap,
399        key: &[u8],
400        out: &mut Vec<u8>,
401        ask: bool,
402    ) -> Result<Faulted> {
403        let Some(addr) = map.find(key) else {
404            return Ok(Faulted::Missing);
405        };
406        let rec = map.value_at(addr);
407        let Some(c) = value::cold(rec) else {
408            return Ok(Faulted::Warm);
409        };
410        let m = value::Meta::from_byte(rec[0]);
411        if m.kind().is_body() {
412            // This puts a value back by writing a string record, so a demoted
413            // collection arriving here would come back as a string holding the
414            // bytes its body froze to. The caller routes those to
415            // `Keyspace::promote_body`, which has a slab to put a body in, and
416            // this says so rather than trusting that it always will.
417            return Err(Error::new(
418                Code::Invalid,
419                "a demoted body cannot be read back as a string",
420            )
421            .with_detail(m.kind().name().to_string()));
422        }
423        let enc = m.encoding();
424        let expire_at = value::expire_at(rec);
425        let was = value::access(rec).unwrap_or_default();
426
427        out.clear();
428        out.reserve(c.len as usize);
429        let chain = cold::Chain {
430            at: c.at,
431            len: u64::from(c.len),
432        };
433        // Before the borrows start, not after they end, because after they end
434        // is inside a scope that owns them. One value's chunks and its
435        // directory are alive together here on purpose, so this is the point
436        // where a store that has to stage bytes to lend them out is allowed to
437        // drop the last value's.
438        self.blocks.release();
439        {
440            let reader = cold::Reader::open(&self.blocks, chain)?;
441            for piece in reader.range(0, reader.len()) {
442                out.extend_from_slice(piece?);
443            }
444        }
445        self.stats.faults += 1;
446        self.stats.bytes_in += u64::from(c.len);
447
448        // One read is not enough. The bits go down now and the key comes back
449        // on the next read, if there is one.
450        if ask && !self.door.admit(RawMap::hash_of(key)) {
451            self.stats.served += 1;
452            return Ok(Faulted::Served);
453        }
454
455        let wrote = map.set_with(
456            key,
457            value::record_len(enc, out.len(), expire_at.is_some()),
458            |_| {},
459            |dst| {
460                value::write_record(dst, enc, out, expire_at);
461                value::set_access(dst, was);
462                value::has_expiry(dst)
463            },
464        );
465        debug_assert!(wrote.is_some(), "the key was found a moment ago");
466        self.stats.promoted += 1;
467        Ok(Faulted::Promoted)
468    }
469
470    /// Move values out until the map fits in `budget` bytes.
471    ///
472    /// Answers with a [`Relief`], which is what moved and what that was worth.
473    /// Stops early when [`BARREN`] rounds in a row find nothing worth demoting,
474    /// which is the case where every value left is shorter than the pointer
475    /// that would replace it, and the honest answer there is that memory cannot
476    /// be given back rather than that the loop should keep spinning.
477    ///
478    /// Two things had to be right before that stop rule meant what it says, and
479    /// both of them are about a sweep that runs long enough to make most of the
480    /// keyspace cold. One barren round is a collision rather than a conclusion,
481    /// which is what [`BARREN`] is for, and a round has to spend its budget on
482    /// victims found rather than entries walked, which is what [`WALK`] is for.
483    /// Each constant has the failure it prevents written on it.
484    ///
485    /// # Compaction is the part that gives the memory back
486    ///
487    /// Demoting a key does not free anything on its own, and finding that out
488    /// is worth a paragraph. Replacing a long record with a short one leaves
489    /// the long one behind as dead bytes in a segment the arena still owns, so
490    /// the number a memory limit is compared against does not move until a
491    /// segment is evacuated and handed back. So each round of demotions is
492    /// followed by [`RawMap::compact_hard`], which is the entry point written
493    /// for a store that has run out of room and will evacuate a segment holding
494    /// a single dead record rather than wait for a worthwhile one.
495    ///
496    /// A round drains its whole pool before checking the budget again, so this
497    /// can overshoot by up to the pool size. That is bounded by
498    /// [`evict::CANDIDATES`] keys and it is the right way round: demoting one
499    /// key too many costs one device read later, and stopping one key short
500    /// costs a memory limit that was not respected.
501    ///
502    /// # Why the count of values moved is not the answer on its own
503    ///
504    /// Because the two halves of this loop run at different rates. Demotion
505    /// happens key by key and compaction happens two megabytes at a time, so a
506    /// sweep that has been running for a while is full of rounds that move
507    /// values and free nothing, and rounds that move nothing and free a whole
508    /// segment that earlier rounds had emptied out. The second kind is not
509    /// rare: sampling draws one index segment, and in a keyspace that is mostly
510    /// cold it draws a segment with nothing resident in it often.
511    ///
512    /// A caller asking for room and reading only the count refuses its client's
513    /// write on one of those rounds, on a server whose memory just went down by
514    /// two megabytes. That is what [`Relief::made_room`] is for and it is why
515    /// this counts both.
516    ///
517    /// # Errors
518    ///
519    /// Whatever the store says when it cannot take the bytes.
520    pub fn relieve(
521        &mut self,
522        map: &mut RawMap,
523        budget: usize,
524        policy: Policy,
525        now_ms: u64,
526        lfu: Lfu,
527    ) -> Result<Relief> {
528        let start = map.memory_bytes();
529        let mut moved = 0;
530        let mut barren = 0;
531        while map.memory_bytes() > budget {
532            let round = self.round(map, policy, now_ms, lfu)?;
533            // After every round and not only the productive ones, because the
534            // state a long load spends most of its time in is a keyspace that is
535            // already cold, holding segments earlier rounds emptied out and
536            // nothing has handed back yet. Those rounds move nothing and free
537            // two megabytes, and stopping on the count would refuse a client's
538            // write on a server whose memory just went down.
539            //
540            // What stops this being the expensive loop it used to be is the
541            // floor on `compact_hard`. This runs on databases whose memory is
542            // somewhere else entirely: the budget is the arena's share of the
543            // limit, a keyspace full of collections keeps its bodies in slabs
544            // the arena has never heard of, and a round looking for strings to
545            // demote finds none of them. Without a floor the loop answered that
546            // by walking the whole arena on every write and handing back
547            // segments that were almost entirely live.
548            while map.memory_bytes() > budget && map.compact_hard().is_some() {}
549            if round == 0 {
550                barren += 1;
551                if barren == BARREN {
552                    break;
553                }
554                continue;
555            }
556            barren = 0;
557            moved += round;
558        }
559        Ok(Relief {
560            moved,
561            freed: start.saturating_sub(map.memory_bytes()),
562        })
563    }
564
565    /// One sample and demote pass, which is the body of [`Tier::relieve`] and is
566    /// separate so that a test can watch a single round.
567    fn round(&mut self, map: &mut RawMap, policy: Policy, now_ms: u64, lfu: Lfu) -> Result<usize> {
568        self.pool.clear();
569        let r = self.rng.next_u64();
570        let pool = &mut self.pool;
571        let mut seen = 0usize;
572        let mut found = 0usize;
573        map.sample(r, |k, v, _| {
574            seen += 1;
575            if worth_demoting(v) {
576                pool.offer(k, evict::score(v, policy, now_ms, lfu));
577                found += 1;
578            }
579            found < evict::CANDIDATES && seen < WALK
580        });
581
582        let mut moved = 0;
583        // Out of the pool and into a buffer of our own, because the pool hands
584        // back a slice of itself and demoting needs the whole tier.
585        let mut kb = core::mem::take(&mut self.keybuf);
586        while let Some(k) = self.pool.take() {
587            kb.clear();
588            kb.extend_from_slice(k);
589            if self.demote(map, &kb)? {
590                moved += 1;
591            }
592        }
593        self.keybuf = kb;
594        Ok(moved)
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601    use crate::access::Access;
602    use crate::many;
603    use yo_common::{Addr, Code, Error, Space};
604
605    /// The same in memory store the `cold` unit tests use, counting its reads.
606    struct Mem {
607        blobs: Vec<Vec<u8>>,
608        reads: std::cell::Cell<usize>,
609    }
610
611    impl Mem {
612        fn new() -> Mem {
613            Mem {
614                blobs: Vec::new(),
615                reads: std::cell::Cell::new(0),
616            }
617        }
618    }
619
620    impl Blocks for Mem {
621        fn put(&mut self, bytes: &[u8]) -> Result<Addr> {
622            self.blobs.push(bytes.to_vec());
623            Ok(Addr::new(Space::Log, (self.blobs.len() - 1) as u64))
624        }
625
626        fn get(&self, at: Addr) -> Result<&[u8]> {
627            self.reads.set(self.reads.get() + 1);
628            self.blobs
629                .get(at.offset() as usize)
630                .map(Vec::as_slice)
631                .ok_or_else(|| Error::new(Code::NotFound, "no such block"))
632        }
633
634        fn bytes(&self) -> u64 {
635            self.blobs.iter().map(|b| b.len() as u64).sum()
636        }
637    }
638
639    fn tier() -> Tier<Mem> {
640        Tier::new(Mem::new())
641    }
642
643    /// A map with one string in it, written the way the keyspace writes one.
644    fn map_with(key: &[u8], val: &[u8], expire_at: Option<u64>) -> RawMap {
645        let mut m = RawMap::new();
646        put(&mut m, key, val, expire_at);
647        m
648    }
649
650    fn put(m: &mut RawMap, key: &[u8], val: &[u8], expire_at: Option<u64>) {
651        let enc = Encoding::of(val);
652        let len = value::record_len(enc, val.len(), expire_at.is_some());
653        m.set_with(
654            key,
655            len,
656            |_| {},
657            |out| {
658                value::write_record(out, enc, val, expire_at);
659                value::has_expiry(out)
660            },
661        );
662    }
663
664    /// Read a key twice, which is what the doorkeeper asks for before it lets
665    /// anything back into memory.
666    fn fault_twice(t: &mut Tier<Mem>, m: &mut RawMap, key: &[u8]) -> (Faulted, Faulted, Vec<u8>) {
667        let mut out = Vec::new();
668        let first = t.fault(m, key, &mut out).expect("a first read");
669        let second = t.fault(m, key, &mut out).expect("a second read");
670        (first, second, out)
671    }
672
673    #[test]
674    fn a_value_goes_out_to_the_file_and_the_record_shrinks_to_a_pointer() {
675        let val = vec![b'x'; 4000];
676        let mut m = map_with(b"k", &val, None);
677        let before = m.value_at(m.find(b"k").expect("there")).len();
678        let mut t = tier();
679
680        assert!(t.demote(&mut m, b"k").expect("demoted"));
681
682        let rec = m.value_at(m.find(b"k").expect("still there"));
683        assert!(rec.len() < before / 100, "the record did not shrink");
684        assert_eq!(value::cold(rec).expect("cold").len, 4000);
685        assert_eq!(t.stats().demoted, 1);
686        assert_eq!(t.stats().bytes_out, 4000);
687    }
688
689    #[test]
690    fn the_questions_that_do_not_want_the_bytes_are_still_answered_in_memory() {
691        let val = vec![b'y'; 900];
692        let deadline = Some(1_900_000_000_000);
693        let mut m = map_with(b"k", &val, deadline);
694        let mut t = tier();
695        t.demote(&mut m, b"k").expect("demoted");
696
697        let rec = m.value_at(m.find(b"k").expect("there"));
698        // STRLEN, TYPE, OBJECT ENCODING and TTL, in that order, on a key whose
699        // bytes are on the device. None of these is allowed to fault.
700        assert_eq!(value::str_len(rec), Some(900));
701        assert_eq!(value::kind(rec), Kind::String);
702        assert_eq!(value::Meta::from_byte(rec[0]).encoding(), Encoding::Raw);
703        assert_eq!(value::expire_at(rec), deadline);
704        assert_eq!(t.blocks().reads.get(), 0, "answering those read the device");
705    }
706
707    #[test]
708    fn a_value_too_short_to_be_worth_moving_is_left_where_it_is() {
709        // Twelve payload bytes against a twelve byte pointer plus the head that
710        // both records share, so this one loses by moving.
711        let mut m = map_with(b"k", b"hello-world!", None);
712        let mut t = tier();
713        assert!(!t.demote(&mut m, b"k").expect("asked"));
714        assert!(value::cold(m.value_at(m.find(b"k").expect("there"))).is_none());
715    }
716
717    #[test]
718    fn an_int_encoded_value_is_never_moved() {
719        let mut m = map_with(b"k", b"1234567890123", None);
720        let mut t = tier();
721        assert!(!t.demote(&mut m, b"k").expect("asked"));
722    }
723
724    #[test]
725    fn a_key_that_is_not_there_is_a_no_and_not_an_error() {
726        let mut m = RawMap::new();
727        let mut t = tier();
728        assert!(!t.demote(&mut m, b"nothing").expect("asked"));
729        let mut out = Vec::new();
730        assert_eq!(
731            t.fault(&mut m, b"nothing", &mut out).expect("asked"),
732            Faulted::Missing
733        );
734    }
735
736    #[test]
737    fn demoting_twice_is_a_no_the_second_time() {
738        let val = vec![b'z'; 500];
739        let mut m = map_with(b"k", &val, None);
740        let mut t = tier();
741        assert!(t.demote(&mut m, b"k").expect("demoted"));
742        assert!(!t.demote(&mut m, b"k").expect("asked again"));
743        assert_eq!(t.stats().demoted, 1);
744    }
745
746    #[test]
747    fn a_resident_key_is_warm_and_the_buffer_is_left_alone() {
748        let mut m = map_with(b"k", b"a value long enough to matter", None);
749        let mut t = tier();
750        let mut out = vec![1, 2, 3];
751        assert_eq!(
752            t.fault(&mut m, b"k", &mut out).expect("read"),
753            Faulted::Warm
754        );
755        assert_eq!(out, vec![1, 2, 3], "a warm read touched the buffer");
756        assert_eq!(t.stats().faults, 0);
757    }
758
759    #[test]
760    fn the_first_read_serves_from_the_file_and_the_second_brings_it_back() {
761        let val = vec![b'q'; 3000];
762        let mut m = map_with(b"k", &val, None);
763        let mut t = tier();
764        t.demote(&mut m, b"k").expect("demoted");
765
766        let (first, second, out) = fault_twice(&mut t, &mut m, b"k");
767        assert_eq!(first, Faulted::Served, "one read earned a slot in memory");
768        assert_eq!(second, Faulted::Promoted);
769        assert_eq!(out, val);
770        assert_eq!(t.stats().faults, 2);
771        assert_eq!(t.stats().served, 1);
772        assert_eq!(t.stats().promoted, 1);
773
774        // And now it is back, so the third read is not a fault at all.
775        let mut again = Vec::new();
776        assert_eq!(
777            t.fault(&mut m, b"k", &mut again).expect("read"),
778            Faulted::Warm
779        );
780        assert_eq!(
781            value::read(m.value_at(m.find(b"k").expect("there"))).len(),
782            3000
783        );
784    }
785
786    #[test]
787    fn a_scan_over_cold_data_promotes_nothing() {
788        let mut m = RawMap::new();
789        let val = vec![b'c'; 700];
790        for i in 0..64u32 {
791            put(&mut m, &i.to_le_bytes(), &val, None);
792        }
793        let mut t = tier();
794        for i in 0..64u32 {
795            t.demote(&mut m, &i.to_le_bytes()).expect("demoted");
796        }
797
798        let mut out = Vec::new();
799        for i in 0..64u32 {
800            t.fault(&mut m, &i.to_le_bytes(), &mut out).expect("read");
801        }
802        assert_eq!(
803            t.stats().promoted,
804            0,
805            "a single pass over cold keys pulled some back in"
806        );
807        assert_eq!(t.stats().served, 64);
808    }
809
810    #[test]
811    fn the_deadline_and_the_access_field_survive_a_round_trip() {
812        let val = vec![b'r'; 1200];
813        let deadline = Some(1_888_777_666_555);
814        let mut m = map_with(b"k", &val, deadline);
815        // Stamp something recognisable, so that a demotion that restamped it
816        // would show up rather than looking like a fresh record.
817        let a = Access::lru(1_000_000);
818        {
819            let addr = m.find(b"k").expect("there");
820            value::set_access(m.value_at_mut(addr), a);
821        }
822        let mut t = tier();
823        t.demote(&mut m, b"k").expect("demoted");
824        assert_eq!(
825            value::access(m.value_at(m.find(b"k").expect("there"))),
826            Some(a),
827            "demotion looked like a use"
828        );
829
830        let (_, _, out) = fault_twice(&mut t, &mut m, b"k");
831        assert_eq!(out, val);
832        let rec = m.value_at(m.find(b"k").expect("there"));
833        assert_eq!(value::expire_at(rec), deadline);
834        assert_eq!(value::access(rec), Some(a));
835    }
836
837    #[test]
838    fn a_value_bigger_than_one_chunk_makes_the_trip_as_well() {
839        let val: Vec<u8> = (0..cold::CHUNK * 2 + 77).map(|i| (i % 251) as u8).collect();
840        let mut m = map_with(b"big", &val, None);
841        let mut t = tier();
842        assert!(t.demote(&mut m, b"big").expect("demoted"));
843        let (_, _, out) = fault_twice(&mut t, &mut m, b"big");
844        assert_eq!(out, val);
845    }
846
847    #[test]
848    fn relieve_moves_values_out_until_the_map_fits() {
849        // Enough data to span several arena segments. A budget below one
850        // segment is a budget nothing can meet, because a segment is the unit
851        // the arena hands back, and a test that asked for one would be testing
852        // the arena's minimum rather than the demotion.
853        let mut m = RawMap::new();
854        let val = vec![b'p'; 2000];
855        for i in 0..4_000u32 {
856            put(&mut m, &i.to_le_bytes(), &val, None);
857        }
858        let full = m.memory_bytes();
859        let budget = full / 2;
860
861        let mut t = tier();
862        let moved = t
863            .relieve(
864                &mut m,
865                budget,
866                Policy::AllKeysLru,
867                2_000_000,
868                Lfu::default(),
869            )
870            .expect("relieved");
871        assert!(moved.moved > 0, "nothing was moved");
872        assert!(
873            m.memory_bytes() <= budget,
874            "still {} bytes against a budget of {budget}",
875            m.memory_bytes()
876        );
877        // Every key is still there, which is the whole difference between this
878        // and eviction.
879        assert_eq!(m.len(), 4_000);
880    }
881
882    #[test]
883    fn one_unlucky_round_does_not_end_the_sweep() {
884        // Two bugs written down, both of which left a sweep that had been asked
885        // for the whole keyspace sitting on a large part of it. The first
886        // version of `relieve` stopped on the first round that found nothing,
887        // and quit at six percent moved, because sampling walks forward from a
888        // segment and a bucket drawn at random and two rounds that draw the
889        // same pair see the same entries. The second counted entries walked
890        // against a round's budget of sixteen rather than victims found, and
891        // stalled at eighty five percent, because by then almost every entry a
892        // round walked was one it had already moved.
893        let mut m = RawMap::new();
894        let val = vec![b'u'; 2000];
895        for i in 0..4_000u32 {
896            put(&mut m, &i.to_le_bytes(), &val, None);
897        }
898        let mut t = tier();
899        t.relieve(&mut m, 1, Policy::AllKeysLru, 2_000_000, Lfu::default())
900            .expect("relieved");
901
902        let cold = (0..4_000u32)
903            .filter(|i| {
904                let addr = m.find(&i.to_le_bytes()).expect("still there");
905                value::cold(m.value_at(addr)).is_some()
906            })
907            .count();
908        assert!(
909            cold > 3_900,
910            "only {cold} of 4000 were moved, so the sweep gave up early"
911        );
912    }
913
914    #[test]
915    fn the_memory_the_map_holds_actually_goes_down() {
916        // Demotion on its own frees nothing: the record it replaces becomes dead
917        // bytes in a segment the arena still owns. This is the check that the
918        // compaction in `relieve` is doing the part that gives it back.
919        let n = many(4_000u32);
920        let mut m = RawMap::new();
921        let val = vec![b'v'; 2000];
922        for i in 0..n {
923            put(&mut m, &i.to_le_bytes(), &val, None);
924        }
925        let before = m.memory_bytes();
926        let mut t = tier();
927        t.relieve(&mut m, 1, Policy::AllKeysLru, 2_000_000, Lfu::default())
928            .expect("relieved");
929
930        // What the same keys would have cost if their values had never been in
931        // memory at all. The arena cannot hand back its last segment, so this
932        // is the floor, and asking the sweep to reach it says more than a
933        // fraction of `before` picked because it passes.
934        let mut bare = RawMap::new();
935        let stub = vec![b'v'; 4];
936        for i in 0..n {
937            put(&mut bare, &i.to_le_bytes(), &stub, None);
938        }
939        let floor = bare.memory_bytes();
940        assert!(
941            m.memory_bytes() <= floor,
942            "{before} bytes went to {}, and the floor is {floor}",
943            m.memory_bytes()
944        );
945    }
946
947    #[test]
948    fn relieve_gives_up_rather_than_spinning_when_nothing_is_worth_moving() {
949        let mut m = RawMap::new();
950        for i in 0..200u32 {
951            put(&mut m, &i.to_le_bytes(), b"tiny", None);
952        }
953        let mut t = tier();
954        let moved = t
955            .relieve(&mut m, 1, Policy::AllKeysLru, 2_000_000, Lfu::default())
956            .expect("asked");
957        assert_eq!(moved, Relief::default());
958    }
959
960    #[test]
961    fn a_sweep_that_moves_nothing_and_frees_a_segment_still_says_it_made_room() {
962        // The state a server spends most of a long load in: a keyspace that is
963        // already cold, holding segments that earlier rounds emptied out and
964        // that nothing has handed back yet. Every round here is barren because
965        // there is genuinely nothing left worth moving, and the memory still
966        // comes back. A caller reading only the count sees a zero and refuses
967        // its client's write, which is the bug this is here about.
968        let mut m = RawMap::new();
969        let val = vec![b'v'; 4096];
970        for i in 0..2_000u32 {
971            put(&mut m, &i.to_le_bytes(), &val, None);
972        }
973        let mut t = tier();
974        for i in 0..2_000u32 {
975            assert!(
976                t.demote(&mut m, &i.to_le_bytes()).expect("demoted"),
977                "key {i} did not go out"
978            );
979        }
980
981        let before = m.memory_bytes();
982        let r = t
983            .relieve(
984                &mut m,
985                before - 1,
986                Policy::AllKeysLru,
987                2_000_000,
988                Lfu::default(),
989            )
990            .expect("swept");
991
992        assert_eq!(r.moved, 0, "there was nothing left in memory to move");
993        assert!(
994            r.freed > 0,
995            "compaction gave nothing back, so this checked nothing"
996        );
997        assert!(
998            r.made_room(),
999            "a sweep that freed {} said it did not",
1000            r.freed
1001        );
1002        assert_eq!(m.len(), 2_000, "a sweep that lost keys");
1003    }
1004
1005    #[test]
1006    fn what_relieve_moved_still_reads_back_byte_for_byte() {
1007        let mut m = RawMap::new();
1008        let mut want = Vec::new();
1009        for i in 0..4_000u32 {
1010            let val: Vec<u8> = (0..900).map(|j| (i as usize + j) as u8).collect();
1011            put(&mut m, &i.to_le_bytes(), &val, None);
1012            want.push(val);
1013        }
1014        let budget = m.memory_bytes() / 2;
1015        let mut t = tier();
1016        let moved = t
1017            .relieve(
1018                &mut m,
1019                budget,
1020                Policy::AllKeysLru,
1021                2_000_000,
1022                Lfu::default(),
1023            )
1024            .expect("relieved");
1025        assert!(
1026            moved.moved > 0,
1027            "nothing was moved, so this checked nothing"
1028        );
1029
1030        let mut out = Vec::new();
1031        for (i, val) in want.iter().enumerate() {
1032            let key = (i as u32).to_le_bytes();
1033            match t.fault(&mut m, &key, &mut out).expect("read") {
1034                Faulted::Warm => {
1035                    let rec = m.value_at(m.find(&key).expect("there"));
1036                    assert_eq!(value::read(rec), value::Str::Bytes(val));
1037                }
1038                Faulted::Served | Faulted::Promoted => assert_eq!(&out, val),
1039                Faulted::Missing => panic!("key {i} went missing"),
1040            }
1041        }
1042    }
1043}