yo_index/map.rs
1//! The raw map: an index and an arena wired together.
2//!
3//! This is the smallest thing that is actually a key value store, and it is the
4//! thing M0's exit gate measures against aki's `f1raw` numbers. There is no
5//! record header yet beyond two lengths, no TTL, no type byte, no version. All
6//! of that arrives in M1 and replaces [`Record`] without the index noticing,
7//! which is the point of keeping the two crates apart.
8//!
9//! Layout of one record in the arena:
10//!
11//! ```text
12//! +--------+--------+-----------+-------------+
13//! | klen | vlen | key bytes | value bytes |
14//! | u32 LE | u32 LE | klen | vlen |
15//! +--------+--------+-----------+-------------+
16//! ```
17//!
18//! Key and value live in one allocation so that a hit is one cache miss for the
19//! bucket and one for the record, not three.
20
21use crate::index::{Index, Keys};
22use crate::scan::Cursor;
23use crate::tagged::Tagged;
24use yo_arena::Arena;
25use yo_common::{Addr, Space, bytes_eq, wyhash};
26
27/// Bytes of length prefix in front of a record.
28const HDR: usize = 8;
29
30/// The least a single [`RawMap::compact_step`] walks.
31///
32/// A segment is two megabytes and evacuating one in a single call was a stop
33/// the world pause in the middle of a batch. At 64 byte values that is around
34/// twenty six thousand records, each one an index probe, a copy and an index
35/// write, and the replies behind it wait for all of them. It is why the write
36/// rows had a p99 of 3.9 milliseconds against Redis at 0.8 while the p50 was
37/// in line: the median command paid nothing and one command in a few thousand
38/// paid for the whole segment.
39///
40/// Sixty four kilobytes is a thirty second of a segment, which puts the worst
41/// call at a few hundred records. Smaller would be smoother and would spend
42/// more of the total on the fixed cost of picking up where the last call left
43/// off; this is the smallest size at which that overhead is still noise.
44///
45/// The budget is spent on how far the cursor moves and not on how many records
46/// move, because a segment can be entirely dead. Charging only for records
47/// that move would let one call walk two megabytes of headers for free, which
48/// is the pause this exists to prevent, just without the copying.
49const EVAC_FLOOR: usize = 64 * 1024;
50
51/// The most, which is a whole segment.
52///
53/// The cap is here so that the scaling below has an end, not because a segment
54/// is a good amount of work to do at once. Reaching it means the collector is
55/// sixteen times past the line it starts at, at which point the pause is the
56/// smaller problem.
57const EVAC_CEILING: usize = yo_arena::SEGMENT_SIZE;
58
59/// How much a caller is willing to pay for the memory a sweep gives back.
60///
61/// Not how hard to work but which trades to accept, which is the part that
62/// turned out to matter: the difference between the three is entirely in which
63/// segment gets picked, and picking badly costs a hundred times more than the
64/// work itself.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66enum Sweep {
67 /// Only when the store as a whole is dirty enough to be worth a sweep.
68 Ordinary,
69 /// However clean the store is overall, as long as this segment is worth
70 /// emptying on its own.
71 Hard,
72}
73
74/// A segment that is partway through being evacuated, and how far it got.
75#[derive(Clone, Copy)]
76struct Evac {
77 seg: usize,
78 off: usize,
79}
80
81/// What compaction has done to a map over its life.
82///
83/// The write amplification of value separation, in the two parts it is actually
84/// made of. Every record the walk steps over costs a liveness probe whether it
85/// is live or not, and every live one it finds costs a copy on top of that, so a
86/// segment full of dead records and a segment full of live ones are different
87/// amounts of work for the same number of bytes. One counter cannot tell those
88/// apart, which is why there are three.
89///
90/// Counted here rather than in the caller because this is the only place that
91/// knows a record moved, and the numbers are wanted per store rather than per
92/// command. They never reset, including across [`RawMap::clear`].
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
94pub struct Compaction {
95 /// Records the walk has stepped over, live and dead together.
96 pub walked: u64,
97 /// The ones that were still live and had to be copied somewhere else.
98 pub moved: u64,
99 /// What those copies came to, headers and keys included.
100 pub bytes: u64,
101}
102
103struct Record;
104
105impl Record {
106 #[inline]
107 fn lens(bytes: &[u8]) -> (usize, usize) {
108 let k = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
109 let v = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
110 (k, v)
111 }
112}
113
114/// Arena backed record access, which is what the index probes through.
115struct Records<'a> {
116 arena: &'a Arena,
117}
118
119impl Keys for Records<'_> {
120 #[inline]
121 fn hash_at(&self, addr: Addr) -> u64 {
122 let (klen, _) = Record::lens(self.arena.get(addr, HDR));
123 let bytes = self.arena.get(addr, HDR + klen);
124 wyhash(&bytes[HDR..], 0)
125 }
126
127 #[inline]
128 fn eq_at(&self, addr: Addr, key: &[u8]) -> bool {
129 let bytes = self.arena.get(addr, HDR);
130 let (klen, _) = Record::lens(bytes);
131 if klen != key.len() {
132 return false;
133 }
134 let bytes = self.arena.get(addr, HDR + klen);
135 bytes_eq(&bytes[HDR..], key)
136 }
137}
138
139/// A single shard's key value map: bytes in, bytes out, nothing else.
140///
141/// Not `Sync`, and deliberately so. One of these is inside one stripe, and the
142/// lock around the stripe is what decides which thread has it, which is `05`
143/// section 1's whole argument: one owner at a time means no atomics on the hot
144/// path.
145///
146/// ```
147/// let mut m = yo_index::RawMap::new();
148/// assert_eq!(m.set(b"k", b"v"), None);
149/// assert_eq!(m.get(b"k"), Some(&b"v"[..]));
150/// assert_eq!(m.set(b"k", b"w").is_some(), true);
151/// assert_eq!(m.get(b"k"), Some(&b"w"[..]));
152/// assert_eq!(m.del(b"k"), true);
153/// assert_eq!(m.get(b"k"), None);
154/// ```
155pub struct RawMap {
156 index: Index,
157 arena: Arena,
158 /// Where the last `compact_step` stopped, if it stopped partway.
159 evac: Option<Evac>,
160 /// How many times anything in here has been written to.
161 ///
162 /// A caller that resolved a key once and wants to skip resolving it again
163 /// needs to know whether anything could have moved in between, and the
164 /// honest answer is any write at all. Every method that takes `&mut self`
165 /// bumps this, including the in place ones, so the question a caller asks is
166 /// "has this map been written since" and not "has this map been written in a
167 /// way I thought would matter".
168 ///
169 /// It lives here rather than in the caller because there are eleven places
170 /// in `yo-kv` that write to a map and one place here that could be missed,
171 /// and a missed invalidation is a stale answer rather than a slow one.
172 ///
173 /// [`RawMap::value_at_mut`] is the one exception and it is argued for where
174 /// it is written. Everything else, including the in place ones, bumps this.
175 writes: u64,
176 /// The records the caller marked when it wrote them.
177 ///
178 /// A second index of a subset of the keys, which exists so that a caller
179 /// looking for one of them does not have to walk past the ones it is not
180 /// looking for. The only thing that uses it is expiry: a key with a deadline
181 /// is rare in most databases, and both the active expire cycle and the
182 /// `volatile-*` eviction policies were sampling the whole map to find one.
183 ///
184 /// It is here and not in `yo-kv` because this is the only thing that knows
185 /// where a record is. An overwrite can move one, a delete takes one away,
186 /// and compaction moves them between segments, and all three are in this
187 /// file. A set of addresses kept anywhere else would go stale on the third.
188 ///
189 /// What "marked" means is entirely the caller's business. This holds
190 /// addresses and has never heard of a deadline.
191 tagged: Tagged,
192 /// What compaction has cost so far.
193 compaction: Compaction,
194}
195
196impl RawMap {
197 /// An empty map.
198 pub fn new() -> RawMap {
199 RawMap {
200 index: Index::new(),
201 arena: Arena::new(),
202 evac: None,
203 writes: 0,
204 tagged: Tagged::new(),
205 compaction: Compaction::default(),
206 }
207 }
208
209 /// What compaction has done to this map since it was made.
210 ///
211 /// A running total and not a rate, so two reads either side of a load say
212 /// what that load cost. See [`Compaction`] for what the three numbers are
213 /// and why they are not one.
214 #[inline]
215 #[must_use]
216 pub const fn compaction(&self) -> Compaction {
217 self.compaction
218 }
219
220 /// How many times this map has been written to.
221 ///
222 /// Two reads of this with the same value either side of some work mean
223 /// nothing in the map moved, so an address or a slot resolved before the
224 /// first read is still the right one after the second. It never goes
225 /// backwards, including across [`RawMap::clear`].
226 #[inline]
227 #[must_use]
228 pub const fn writes(&self) -> u64 {
229 self.writes
230 }
231
232 /// How many keys are stored.
233 #[inline]
234 pub fn len(&self) -> usize {
235 self.index.len()
236 }
237
238 /// Whether the map is empty.
239 #[inline]
240 pub fn is_empty(&self) -> bool {
241 self.index.is_empty()
242 }
243
244 /// Throw everything away and give the memory back.
245 ///
246 /// A fresh index and a fresh arena rather than a walk that deletes each key
247 /// in turn. Deleting one at a time would leave an arena the size of the
248 /// data that used to be in it and an index still grown to fit it, and the
249 /// one thing a client that has just said `FLUSHALL` is entitled to expect is
250 /// the memory back.
251 pub fn clear(&mut self) {
252 // Carried across the reset and bumped, because a counter that went back
253 // to zero here could land on a value a memo was already holding and
254 // read as "nothing moved" on the one call where everything did.
255 let writes = self.writes;
256 let compaction = self.compaction;
257 *self = RawMap::new();
258 self.writes = writes + 1;
259 // Carried for a plainer reason: it is what this store has spent, and a
260 // `FLUSHALL` does not give any of it back.
261 self.compaction = compaction;
262 }
263
264 /// The hash this map files `key` under.
265 ///
266 /// Public because the batch walk in `04` section 3 hashes on the first walk
267 /// and looks up on the second, and the alternative is hashing every key
268 /// twice to keep the seed a private detail.
269 #[inline]
270 #[must_use]
271 pub fn hash_of(key: &[u8]) -> u64 {
272 wyhash(key, 0)
273 }
274
275 /// Ask the cache for the bucket `hash` will be looked up in.
276 #[inline]
277 pub fn prefetch(&self, hash: u64) {
278 self.index.prefetch(hash);
279 }
280
281 /// The value stored under `key`.
282 #[inline]
283 pub fn get(&self, key: &[u8]) -> Option<&[u8]> {
284 self.get_hashed(Self::hash_of(key), key)
285 }
286
287 /// The value stored under `key`, whose hash the caller already has.
288 ///
289 /// The second walk's entry point. `hash` has to be [`RawMap::hash_of`] of
290 /// this key: a hash from somewhere else is not unsafe, it just misses.
291 #[inline]
292 pub fn get_hashed(&self, hash: u64, key: &[u8]) -> Option<&[u8]> {
293 let addr = self.index.get(hash, key, &Records { arena: &self.arena })?;
294 Some(self.value_at(addr))
295 }
296
297 /// Where `key`'s record is, for a caller that has to look at it twice.
298 ///
299 /// A `GET` has to know whether the key is past its deadline before it can
300 /// answer, and then has to read the value it just decided about. Asking
301 /// [`RawMap::get`] twice is two hashes and two probes for one record, and a
302 /// probe is the expensive half of a command. This hands back the address
303 /// instead, and [`RawMap::value_at`] reads it with no probe at all.
304 ///
305 /// The address is good until the next write to this map. Anything that
306 /// inserts, deletes or compacts can move a record, and an address held
307 /// across one of those reads whatever is at that spot now. Hold it for the
308 /// length of one command and no longer.
309 #[inline]
310 pub fn find(&self, key: &[u8]) -> Option<Addr> {
311 self.find_hashed(Self::hash_of(key), key)
312 }
313
314 /// [`RawMap::find`] for a caller that already hashed the key.
315 #[inline]
316 pub fn find_hashed(&self, hash: u64, key: &[u8]) -> Option<Addr> {
317 self.index.get(hash, key, &Records { arena: &self.arena })
318 }
319
320 /// The value at an address this map handed out, with no probe.
321 ///
322 /// See [`RawMap::find`] for how long an address is worth holding.
323 #[inline]
324 #[must_use]
325 pub fn value_at(&self, addr: Addr) -> &[u8] {
326 let (klen, vlen) = Record::lens(self.arena.get(addr, HDR));
327 &self.arena.get(addr, HDR + klen + vlen)[HDR + klen..]
328 }
329
330 /// The value at an address, to be overwritten in place, without counting as
331 /// a write.
332 ///
333 /// This is the one method taking a mutable borrow that leaves
334 /// [`RawMap::writes`] where it was, and that is a deliberate exception to
335 /// the rule stated on the counter rather than an oversight in it.
336 ///
337 /// It is sound because nothing moves. The record already exists, the caller
338 /// already holds its address, there is no allocation and no index write, so
339 /// every address and every number read out of a record before the call is
340 /// still right afterwards. That is a stronger guarantee than the counter is
341 /// asking about, and it is one this method can actually make.
342 ///
343 /// It exists because the conservative answer costs more here than it
344 /// protects. The eviction clock is written back on nearly every read, under
345 /// eight of the ten policies including the default, so counting it as a write
346 /// would invalidate the caller's memo on every single command rather than on
347 /// every write. That is a measured nineteen nanoseconds a command on single
348 /// key `SADD`, given up to avoid thinking once about three bytes written
349 /// inside a record that is not going anywhere.
350 ///
351 /// The length cannot change, for the same reason it cannot in
352 /// [`RawMap::value_mut`], and an address is only good until the next real
353 /// write, for the same reason it is in [`RawMap::find`].
354 #[inline]
355 pub fn value_at_mut(&mut self, addr: Addr) -> &mut [u8] {
356 let (klen, vlen) = Record::lens(self.arena.get(addr, HDR));
357 &mut self.arena.get_mut(addr, HDR + klen + vlen)[HDR + klen..]
358 }
359
360 /// The value stored under `key`, to be overwritten where it lies.
361 ///
362 /// The length cannot change, which is the whole reason this is safe to
363 /// offer. `INCR` on an integer encoded string is a probe, an add and a
364 /// store, and the store is eight bytes back into the record it came from
365 /// (`08` section 2). Going through [`RawMap::set`] instead would write a
366 /// fresh record and free the old one on every increment, which is an arena
367 /// append and a dead byte per operation for a value whose size never moves.
368 ///
369 /// There is no reader to tear. A map belongs to one shard thread and is not
370 /// `Sync`, so the only code that can observe a half written value is the
371 /// code doing the writing. When a replica stream or a snapshot reader starts
372 /// walking the arena from another thread, this becomes an epoch question and
373 /// the write becomes an install rather than an overwrite.
374 #[inline]
375 pub fn value_mut(&mut self, key: &[u8]) -> Option<&mut [u8]> {
376 self.value_mut_hashed(Self::hash_of(key), key)
377 }
378
379 /// [`RawMap::value_mut`] for a caller that already hashed the key.
380 #[inline]
381 pub fn value_mut_hashed(&mut self, hash: u64, key: &[u8]) -> Option<&mut [u8]> {
382 self.writes += 1;
383 let addr = self.index.get(hash, key, &Records { arena: &self.arena })?;
384 let (klen, vlen) = Record::lens(self.arena.get(addr, HDR));
385 Some(&mut self.arena.get_mut(addr, HDR + klen + vlen)[HDR + klen..])
386 }
387
388 /// Store `val` under `key`, returning the length of the value it replaced.
389 pub fn set(&mut self, key: &[u8], val: &[u8]) -> Option<usize> {
390 self.set_with(
391 key,
392 val.len(),
393 |_| {},
394 |buf| {
395 buf.copy_from_slice(val);
396 false
397 },
398 )
399 }
400
401 /// The largest record this map can store, key and value and header together.
402 ///
403 /// A value past this belongs in the log region rather than the arena, which
404 /// is `06` section 2's business and not this crate's.
405 #[inline]
406 #[must_use]
407 pub const fn max_record() -> usize {
408 yo_arena::MAX_ALLOC
409 }
410
411 /// Bytes of record header in front of the key.
412 #[inline]
413 #[must_use]
414 pub const fn header_len() -> usize {
415 HDR
416 }
417
418 /// Store a `vlen` byte value under `key`, written by `fill`.
419 ///
420 /// The same thing [`RawMap::set`] does, except that the caller writes
421 /// straight into the record instead of building the value somewhere else
422 /// first and having it copied in. A string with a one byte encoding tag in
423 /// front of it would otherwise be assembled in a scratch buffer and then
424 /// memcpy'd again, and two copies for one `SET` is one too many on a path
425 /// that is trying to be ten times faster than Redis.
426 ///
427 /// `fill` is handed exactly `vlen` bytes of uninitialised-looking storage.
428 /// It is arena memory that has been handed out before and freed, so its
429 /// contents are arbitrary and every byte of it must be written. What it
430 /// answers is whether this record should be marked, which is what
431 /// [`RawMap::sample_tagged`] later draws from. A caller with no use for that
432 /// answers `false` and pays a branch.
433 ///
434 /// `peek` is handed the value that was already under `key`, if there was
435 /// one, before anything is written over it. It exists because the caller
436 /// keeps counts that depend on what the old value was, and this is the only
437 /// place those bytes can be read for free: both paths through here have
438 /// already loaded the old record's header to find out how long it is, so the
439 /// value is in cache and would otherwise cost a second lookup to see. A
440 /// caller with nothing to ask passes an empty closure and pays nothing.
441 ///
442 /// # Panics
443 ///
444 /// If the whole record would exceed [`RawMap::max_record`].
445 pub fn set_with<P, F>(&mut self, key: &[u8], vlen: usize, peek: P, fill: F) -> Option<usize>
446 where
447 P: FnOnce(&[u8]),
448 F: FnOnce(&mut [u8]) -> bool,
449 {
450 self.writes += 1;
451 assert!(key.len() <= u32::MAX as usize, "key too long");
452 assert!(vlen <= u32::MAX as usize, "value too long");
453 let total = HDR + key.len() + vlen;
454 let h = wyhash(key, 0);
455
456 // A key that is already here, in a record exactly the size the new value
457 // needs, is written over where it lies. No allocation, no dead bytes, no
458 // index write, and nothing for compaction to collect later.
459 //
460 // This used to say the in place path had to wait for epochs, because a
461 // reader that had already resolved the address would see a torn value.
462 // That was never a rule this map kept: `value_mut` is the same write and
463 // `INCR` has been doing it since the day it was written, for the same
464 // reason given there. A map belongs to one shard thread and is not
465 // `Sync`, so the only code that can see a half written value is the code
466 // writing it. When a replica stream or a snapshot reader starts walking
467 // the arena from another thread, both of these become an install rather
468 // than an overwrite, together.
469 //
470 // Exactly the size and not merely small enough. A shorter value in a
471 // longer record would leave the header disagreeing with the space the
472 // record occupies, and compaction walks a segment by stepping over each
473 // record by the length in its header, so the walk would land in the
474 // middle of the next one.
475 //
476 // Overwriting a key with a value the same size as the last one is what
477 // half of the world's caches do, and it is what every SET benchmark
478 // does. On gamingpc it was 25 percent of SET throughput at pipeline 16
479 // and 37 percent of MSET, all of it spent making garbage and then
480 // collecting it.
481 if let Some(addr) = self.index.get(h, key, &Records { arena: &self.arena }) {
482 let (klen, old_vlen) = Record::lens(self.arena.get(addr, HDR));
483 debug_assert_eq!(klen, key.len(), "the index matched a different key");
484 // Before `fill`, because the in place path writes over exactly the
485 // bytes `peek` is being handed. Once, and here rather than next to
486 // the free below, because this is the branch that knows the key was
487 // there and both paths out of it go past this line.
488 peek(&self.arena.get(addr, HDR + klen + old_vlen)[HDR + klen..]);
489 if old_vlen == vlen {
490 let rec = self.arena.get_mut(addr, total);
491 let tag = fill(&mut rec[HDR + klen..]);
492 // The record did not move, so this is the only thing that can
493 // have changed about where it stands: `PERSIST` on a key whose
494 // value is the same length is exactly this branch.
495 self.retag(addr, tag);
496 return Some(vlen);
497 }
498 }
499
500 let (addr, buf) = self
501 .arena
502 .alloc(total)
503 .expect("record is larger than a segment");
504 buf[0..4].copy_from_slice(&(key.len() as u32).to_le_bytes());
505 buf[4..8].copy_from_slice(&(vlen as u32).to_le_bytes());
506 // The arena hands back a run padded up to its alignment, so index to
507 // `total` rather than to the end of the slice.
508 buf[HDR..HDR + key.len()].copy_from_slice(key);
509 let tag = fill(&mut buf[HDR + key.len()..total]);
510
511 let old = {
512 let recs = Records { arena: &self.arena };
513 self.index.insert(h, key, addr, &recs)
514 };
515 // After the insert and not before, because the address the old record
516 // was at is only known once the index has handed it back, and tagging
517 // the new one first would put both in the set for the width of the call
518 // if they happened to be the same address, which they cannot be, but the
519 // order that does not depend on that is the one to write.
520 if let Some(prev) = old {
521 self.tagged.remove(prev);
522 }
523 if tag {
524 self.tagged.insert(addr);
525 } else {
526 // Nothing to take out. `addr` is a run the arena has just handed
527 // back, and nothing is ever freed while it is still marked: a delete
528 // unmarks before it frees, an overwrite unmarks the record it
529 // replaces on the line above, and compaction moves the mark before
530 // it frees the copy it moved from. So a fresh address is never in
531 // the set, and this is the common path, which is every `SET` on a
532 // database that has any deadline in it at all.
533 debug_assert!(
534 !self.tagged.contains(addr),
535 "the arena handed out an address that is still marked"
536 );
537 }
538 match old {
539 Some(prev) => {
540 let (pk, pv) = Record::lens(self.arena.get(prev, HDR));
541 self.arena.free(prev, HDR + pk + pv);
542 Some(pv)
543 }
544 None => None,
545 }
546 }
547
548 /// Put `addr` in the marked set, or take it out, to match `tag`.
549 ///
550 /// For the in place path, which is the one where the record was already
551 /// there and could already have been marked. It cannot tell whether the mark
552 /// changed without asking, because a deadline is eight bytes in the record
553 /// and a value eight bytes shorter with a deadline is the same length as a
554 /// value without one, so a write that lands in place is not proof that the
555 /// mark stayed put.
556 ///
557 /// On a database where nothing is marked the ask is one comparison against a
558 /// zero length, which is what the overwhelming majority of servers pay.
559 #[inline]
560 fn retag(&mut self, addr: Addr, tag: bool) {
561 if tag {
562 self.tagged.insert(addr);
563 } else {
564 self.tagged.remove(addr);
565 }
566 }
567
568 /// Remove `key`, returning whether it was there.
569 #[inline]
570 pub fn del(&mut self, key: &[u8]) -> bool {
571 self.del_with(key, |_| {})
572 }
573
574 /// Remove `key`, showing its value to `peek` first, and return whether it
575 /// was there.
576 ///
577 /// The sibling of [`RawMap::set_with`], and it exists for the same reason.
578 /// This already reads the record's header to find out how long it is before
579 /// handing the bytes back to the arena, so the value is in cache and a
580 /// caller who keeps a count that depends on what was removed can read it
581 /// here for the price of a closure call. Asking with a [`RawMap::get`] first
582 /// would be a second lookup for a question this one already knows the answer
583 /// to. `peek` is not called when the key was not there.
584 pub fn del_with<P: FnOnce(&[u8])>(&mut self, key: &[u8], peek: P) -> bool {
585 self.writes += 1;
586 let h = wyhash(key, 0);
587 let addr = {
588 let recs = Records { arena: &self.arena };
589 self.index.remove(h, key, &recs)
590 };
591 match addr {
592 Some(a) => {
593 let (k, v) = Record::lens(self.arena.get(a, HDR));
594 peek(&self.arena.get(a, HDR + k + v)[HDR + k..]);
595 self.tagged.remove(a);
596 self.arena.free(a, HDR + k + v);
597 true
598 }
599 None => false,
600 }
601 }
602
603 /// Whether `key` is present.
604 #[inline]
605 pub fn contains(&self, key: &[u8]) -> bool {
606 let h = wyhash(key, 0);
607 self.index.contains(h, key, &Records { arena: &self.arena })
608 }
609
610 /// The key and the value at an address this map handed out.
611 ///
612 /// The pair rather than either one alone, because they are one contiguous
613 /// read: the header says how long the key is and the value starts where the
614 /// key ends, so asking for both costs what asking for one costs.
615 #[inline]
616 #[must_use]
617 pub fn entry_at(&self, addr: Addr) -> (&[u8], &[u8]) {
618 let (klen, vlen) = Record::lens(self.arena.get(addr, HDR));
619 let bytes = self.arena.get(addr, HDR + klen + vlen);
620 (&bytes[HDR..HDR + klen], &bytes[HDR + klen..])
621 }
622
623 /// Walk a batch of the map, and say where the next batch starts.
624 ///
625 /// This is `SCAN`. `budget` is how many entries the caller would like, and
626 /// it is a floor and not a ceiling: the walk stops at the first bucket
627 /// boundary past it, so a batch of ten can come back with fifteen. Redis's
628 /// `COUNT` behaves the same way and for the same reason, which is that a
629 /// bucket is the smallest unit a cursor can name.
630 ///
631 /// A budget of zero still does one bucket, so a caller that keeps passing
632 /// the cursor back always finishes rather than spinning on the same number.
633 ///
634 /// The guarantee, in full: a key that is present for the whole walk is
635 /// handed to `out` at least once. A key added or removed partway through may
636 /// or may not appear, and a key may appear twice. The reasoning is in
637 /// [`Cursor`], and the part worth knowing here is that none of it depends on
638 /// the map holding still between calls.
639 pub fn scan(&self, from: Cursor, budget: usize, mut out: impl FnMut(&[u8], &[u8])) -> Cursor {
640 // The index and the arena are separate fields, so the walk can hold one
641 // and the closure the other. That is what keeps this allocation free:
642 // there is no list of addresses in between.
643 let arena = &self.arena;
644 let mut at = from;
645 let mut seen = 0usize;
646 loop {
647 at = self.index.scan(at, |addr| {
648 let (klen, vlen) = Record::lens(arena.get(addr, HDR));
649 let bytes = arena.get(addr, HDR + klen + vlen);
650 out(&bytes[HDR..HDR + klen], &bytes[HDR + klen..]);
651 seen += 1;
652 });
653 if at.is_end() || seen >= budget {
654 return at;
655 }
656 }
657 }
658
659 /// Entries picked at random, for eviction sampling, until `out` says stop.
660 ///
661 /// The key, the value and the address of each, because a caller choosing a
662 /// victim needs all three: the value to score it, the key to delete it, and
663 /// the address to delete it by without a second probe. `out` answers whether
664 /// to keep going. [`Index::sample`] is where the argument for all of it lives,
665 /// including why the budget is the caller's and why this can hand back
666 /// nothing at all.
667 pub fn sample(&self, r: u64, mut out: impl FnMut(&[u8], &[u8], Addr) -> bool) {
668 let arena = &self.arena;
669 self.index.sample(r, |addr| {
670 let (klen, vlen) = Record::lens(arena.get(addr, HDR));
671 let bytes = arena.get(addr, HDR + klen + vlen);
672 out(&bytes[HDR..HDR + klen], &bytes[HDR + klen..], addr)
673 });
674 }
675
676 /// The index, for stats and for compaction.
677 pub fn index(&self) -> &Index {
678 &self.index
679 }
680
681 /// The arena, for stats and for compaction.
682 pub fn arena(&self) -> &Arena {
683 &self.arena
684 }
685
686 /// Bytes held by index structure plus arena segments.
687 pub fn memory_bytes(&self) -> usize {
688 self.index.memory_bytes()
689 + self.arena.reserved_bytes() as usize
690 + self.tagged.memory_bytes()
691 }
692
693 /// How many records are marked.
694 ///
695 /// Exact, and kept exact by every write path, so a caller can branch on a
696 /// zero here rather than starting a sweep that was never going to find
697 /// anything.
698 #[inline]
699 #[must_use]
700 pub fn tagged_len(&self) -> usize {
701 self.tagged.len()
702 }
703
704 /// Whether the record at `addr` is marked.
705 ///
706 /// For a test and for a debug assertion. Nothing on a hot path asks this:
707 /// the mark is written from the record's own bytes, so anything holding the
708 /// record already knows.
709 #[must_use]
710 pub fn is_tagged(&self, addr: Addr) -> bool {
711 self.tagged.contains(addr)
712 }
713
714 /// Walk marked records from wherever `r` lands, until `out` says stop.
715 ///
716 /// [`RawMap::sample`] for the marked subset, and the reason the subset
717 /// exists. A database of ten million keys where a thousand carry a deadline
718 /// gives the expire cycle a thousand candidates to draw from instead of ten
719 /// million, and the cycle stops costing anything at all in the case that
720 /// matters most, which is the one where the answer is that there is nothing
721 /// to do.
722 pub fn sample_tagged(&self, r: u64, mut out: impl FnMut(&[u8], &[u8], Addr) -> bool) {
723 let arena = &self.arena;
724 self.tagged.sample(r, |addr| {
725 let (klen, vlen) = Record::lens(arena.get(addr, HDR));
726 let bytes = arena.get(addr, HDR + klen + vlen);
727 out(&bytes[HDR..HDR + klen], &bytes[HDR + klen..], addr)
728 });
729 }
730
731 /// Move every live record out of `seg` and into the current segment, then
732 /// put the segment back on the arena's free list.
733 ///
734 /// Copy, rewrite the index entry, done. No forwarding pointers and no read
735 /// barrier, which is the F2 shape from `05` section 3.2 and is what an
736 /// allocation having exactly one referent buys.
737 ///
738 /// The walk is over the segment and not over the index. Both find the same
739 /// records, and the index walk is the one written in the spec, but it reads
740 /// the whole index to compact two megabytes: fine when this only ran in a
741 /// test, wrong once the event loop calls it, because the pause would then
742 /// grow with the size of the database rather than with the size of a
743 /// segment. Walking the segment costs one index probe per record in it and
744 /// does not care how many keys exist elsewhere.
745 ///
746 /// Records sit back to back from the header to the segment's bump, each one
747 /// rounded up to the arena's alignment, and every arena allocation is a
748 /// record, so the next one is always a known distance away. A record is
749 /// live when the index still points at this copy of it, and dead when it
750 /// points somewhere else or at nothing, which is exactly what an overwrite
751 /// and a delete leave behind.
752 ///
753 /// The reclaim at the end is the part that makes the space usable again.
754 /// Moving the records out only makes a segment empty, and an empty segment
755 /// that nothing ever bumps through again is still two megabytes the process
756 /// is holding.
757 pub fn compact_segment(&mut self, seg: usize) -> usize {
758 self.writes += 1;
759 if seg == self.arena.current_segment() {
760 // Its bump is a cursor, not a checkpoint, and reclaiming it would
761 // take the ground out from under the next allocation.
762 return 0;
763 }
764 let (moved, _) = self.evacuate(seg, yo_arena::HEADER_SIZE, usize::MAX);
765 self.arena.reclaim(seg);
766 moved
767 }
768
769 /// Walk `seg` from `from`, moving live records out, and stop once the walk
770 /// has covered `budget` bytes of it. Says how many records moved and where
771 /// to start again.
772 ///
773 /// The record that straddles the budget is finished rather than cut in
774 /// half, so the walk can go a little past what was asked for. The overrun
775 /// is one record and the budget is thousands of bytes.
776 ///
777 /// Nothing here reclaims. A segment is only empty once the walk reaches the
778 /// bump, and the caller is the one that knows whether it did.
779 fn evacuate(&mut self, seg: usize, from: usize, budget: usize) -> (usize, usize) {
780 let base = (seg as u64) << yo_arena::SEGMENT_SHIFT;
781 let bump = self.arena.recorded_bump(seg) as usize;
782 let stop = from.saturating_add(budget).min(bump);
783
784 let mut moved = 0;
785 let mut off = from;
786 while off < stop {
787 let old = Addr::new(Space::Arena, base + off as u64);
788 let (klen, vlen) = Record::lens(self.arena.get(old, HDR));
789 let total = HDR + klen + vlen;
790 off += total.next_multiple_of(yo_arena::ALIGN);
791 self.compaction.walked += 1;
792
793 let hash = {
794 let bytes = self.arena.get(old, HDR + klen);
795 wyhash(&bytes[HDR..], 0)
796 };
797 let live = {
798 let bytes = self.arena.get(old, HDR + klen);
799 let key = &bytes[HDR..];
800 let recs = Records { arena: &self.arena };
801 self.index.get(hash, key, &recs) == Some(old)
802 };
803 if !live {
804 continue;
805 }
806
807 let new = self.arena.copy_within(old, total);
808 let bytes = self.arena.get(new, HDR + klen);
809 let key = &bytes[HDR..];
810 let recs = Records { arena: &self.arena };
811 let ok = self.index.relocate(hash, key, new, &recs);
812 debug_assert!(ok, "compaction lost an entry the index just handed us");
813 // The one place a record moves without anybody writing to it, and
814 // therefore the one place the tagged set would go stale if this line
815 // were not here.
816 if self.tagged.remove(old) {
817 self.tagged.insert(new);
818 }
819 self.arena.free(old, total);
820 moved += 1;
821 self.compaction.moved += 1;
822 self.compaction.bytes += total as u64;
823 }
824 (moved, off)
825 }
826
827 /// How much to walk on this call, given how far behind the collector is.
828 ///
829 /// A fixed budget has to be either a good pause or a good collection rate
830 /// and it cannot be both. At 64 kilobytes a segment takes thirty two calls,
831 /// and a pipelined flood of writes makes garbage faster than one call per
832 /// batch gets it back: measured with variable sized values at pipeline 16,
833 /// the tail came down from 2.6 milliseconds to 1.6 and the process held 18
834 /// MB more, because segments queued up waiting their turn to be walked.
835 ///
836 /// So the floor is what a command can be asked to wait for, and the depth
837 /// of that queue is what says how much more than the floor is needed to
838 /// keep up. One candidate is a store that is keeping up and pays the floor.
839 /// Nine is a store nine segments behind, and it walks nine slices.
840 ///
841 /// The queue and not the dead byte total. Dead bytes were tried first,
842 /// measured against the point compaction starts at, and that ratio cannot
843 /// see a backlog at all: the threshold is a fraction of what the arena
844 /// holds, so a collector that falls behind grows the arena, which raises
845 /// the threshold, which puts the ratio back where it was. It sat at the
846 /// floor through the whole flood and the 18 MB stayed exactly where it was.
847 /// A count of segments has no such denominator.
848 ///
849 /// Linear in the depth and not squared. This is a controller in a loop with
850 /// its own input, and a term that grows faster than the error is how one of
851 /// those starts to oscillate.
852 fn budget(&self) -> usize {
853 let behind = self.arena.candidate_count().max(1);
854 EVAC_FLOOR.saturating_mul(behind).min(EVAC_CEILING)
855 }
856
857 /// Do one bounded slice of compaction, and say how many records moved.
858 ///
859 /// `None` means there was no candidate and there is nothing in flight. It
860 /// is not the same as `Some(0)`, which is a slice that walked only records
861 /// that had already been overwritten: that one made progress and cost
862 /// something, and a caller deciding whether to go round again needs to be
863 /// told so.
864 ///
865 /// This is the whole maintenance contract: a bounded amount of work per
866 /// call, so a caller that runs it once per batch never pays for a full pass
867 /// over the arena and never pays for a whole segment either. Finding out
868 /// there is nothing to do is one comparison against the running dead byte
869 /// total.
870 ///
871 /// A segment takes as many calls as it takes. Each one picks up where the
872 /// last stopped and only the call that reaches the end gives the two
873 /// megabytes back, so the space comes back in one lump at the end while the
874 /// cost of getting it back is spread over the batches in between. That is
875 /// the trade: a segment stays around a little longer than it used to, and
876 /// no single command waits for the whole of it.
877 ///
878 /// The segment in flight is finished before another is chosen, rather than
879 /// asking which segment is worst on every call. Otherwise a segment that is
880 /// three quarters evacuated could be put down in favour of a worse one and
881 /// never picked up, and the arena would fill with segments that are nearly
882 /// empty and never reclaimed.
883 pub fn compact_step(&mut self) -> Option<usize> {
884 self.compact(Sweep::Ordinary)
885 }
886
887 /// One slice of compaction for a store that has run out of room.
888 ///
889 /// The same work, choosing between segments the way
890 /// [`Arena::any_candidate`](yo_arena::Arena::any_candidate) chooses rather
891 /// than the way [`Arena::worst_candidate`](yo_arena::Arena::worst_candidate)
892 /// does, so a store that is clean overall still collects the parts of it
893 /// that are not. The reason is written on `any_candidate`.
894 ///
895 /// A segment already in flight is finished first either way, so switching
896 /// between this and [`RawMap::compact_step`] cannot leave a segment half
897 /// evacuated forever.
898 pub fn compact_hard(&mut self) -> Option<usize> {
899 self.compact(Sweep::Hard)
900 }
901
902 fn compact(&mut self, sweep: Sweep) -> Option<usize> {
903 self.writes += 1;
904 let (seg, from) = match self.evac {
905 Some(e) => (e.seg, e.off),
906 None => {
907 let pick = match sweep {
908 Sweep::Ordinary => self.arena.worst_candidate()?,
909 Sweep::Hard => self.arena.any_candidate()?,
910 };
911 (pick, yo_arena::HEADER_SIZE)
912 }
913 };
914 if seg == self.arena.current_segment() {
915 self.evac = None;
916 return Some(0);
917 }
918
919 // After the choice and not before it. The count is a walk over the
920 // segment headers, and a store with nothing to collect should not pay
921 // for one on every batch to be told there is nothing to collect.
922 let budget = self.budget();
923 let (moved, off) = self.evacuate(seg, from, budget);
924 if off >= self.arena.recorded_bump(seg) as usize {
925 self.arena.reclaim(seg);
926 self.evac = None;
927 } else {
928 self.evac = Some(Evac { seg, off });
929 }
930 Some(moved)
931 }
932}
933
934impl Default for RawMap {
935 fn default() -> RawMap {
936 RawMap::new()
937 }
938}
939
940#[cfg(test)]
941mod tests {
942 use super::*;
943 use std::collections::{HashMap, HashSet};
944
945 /// `key:` and the index zero padded to twelve digits.
946 ///
947 /// Written out by hand rather than with `format!`, which produces the same
948 /// bytes. Formatting is a lot of machinery for twelve digits, and Miri pays
949 /// per operation rather than per instruction, so under the interpreter one
950 /// `format!` costs a couple of milliseconds. `grows_through_many_splits`
951 /// calls this once per set, get, delete and contains, which is ten thousand
952 /// calls on its own, and that is twenty seconds of the ninety five this
953 /// crate's Miri shard used to take.
954 fn key(i: usize) -> Vec<u8> {
955 let mut k = *b"key:000000000000";
956 let mut n = i;
957 let mut p = k.len() - 1;
958 while n > 0 {
959 k[p] = b'0' + (n % 10) as u8;
960 n /= 10;
961 p -= 1;
962 }
963 k.to_vec()
964 }
965
966 /// `v` and the index, unpadded, which is what `format!("v{i}")` gives.
967 fn val(i: usize) -> Vec<u8> {
968 let mut v = vec![b'v'];
969 if i == 0 {
970 v.push(b'0');
971 return v;
972 }
973 let start = v.len();
974 let mut n = i;
975 while n > 0 {
976 v.push(b'0' + (n % 10) as u8);
977 n /= 10;
978 }
979 v[start..].reverse();
980 v
981 }
982
983 // Miri is a few hundred times slower than the machine, so the counts below
984 // shrink under it. They stay large enough to force directory doublings,
985 // segment splits and overflow chains, which is what these tests are for.
986 // Only the scale goes away, not the coverage.
987 // Three thousand and not fewer. `splits() > 4` is the assertion and the
988 // splits go 1, 1, 2, 3, 3, 5 at 800, 1200, 1500, 2000, 2500 and 3000 keys,
989 // so this is already the smallest count that grows the directory the number
990 // of times the test asks about.
991 #[cfg(miri)]
992 const GROW_N: usize = 3_000;
993 #[cfg(not(miri))]
994 const GROW_N: usize = 200_000;
995
996 #[cfg(miri)]
997 const ADVERSARIAL_N: u64 = 1_000;
998 #[cfg(not(miri))]
999 const ADVERSARIAL_N: u64 = 50_000;
1000
1001 // Big values so that a handful of records fills a 2 MiB segment and
1002 // compaction has something to do without a hundred thousand writes.
1003 #[cfg(miri)]
1004 const COMPACT_VAL: usize = 65_536;
1005 #[cfg(miri)]
1006 const COMPACT_N: usize = 200;
1007 #[cfg(not(miri))]
1008 const COMPACT_VAL: usize = 1024;
1009 #[cfg(not(miri))]
1010 const COMPACT_N: usize = 8_000;
1011
1012 #[test]
1013 fn set_get_del() {
1014 let mut m = RawMap::new();
1015 assert!(m.is_empty());
1016 assert_eq!(m.set(b"a", b"1"), None);
1017 assert_eq!(m.get(b"a"), Some(&b"1"[..]));
1018 assert_eq!(m.len(), 1);
1019 assert_eq!(m.set(b"a", b"22"), Some(1));
1020 assert_eq!(m.get(b"a"), Some(&b"22"[..]));
1021 assert_eq!(m.len(), 1);
1022 assert!(m.del(b"a"));
1023 assert!(!m.del(b"a"));
1024 assert_eq!(m.get(b"a"), None);
1025 assert!(m.is_empty());
1026 }
1027
1028 #[test]
1029 fn a_value_can_be_overwritten_where_it_lies() {
1030 let mut m = RawMap::new();
1031 m.set(b"n", &7u64.to_le_bytes());
1032 m.set(b"other", b"untouched");
1033 let before = m.arena().live_bytes();
1034
1035 let v = m.value_mut(b"n").expect("the key is there");
1036 v.copy_from_slice(&8u64.to_le_bytes());
1037
1038 assert_eq!(m.get(b"n"), Some(&8u64.to_le_bytes()[..]));
1039 assert_eq!(m.get(b"other"), Some(&b"untouched"[..]));
1040 // The point of the whole method: no second record and nothing dead.
1041 assert_eq!(m.arena().live_bytes(), before);
1042 assert_eq!(m.len(), 2);
1043
1044 assert!(m.value_mut(b"missing").is_none());
1045 }
1046
1047 /// A key overwritten with a value the same size stays in the record it is
1048 /// already in, and one overwritten with a different size does not.
1049 ///
1050 /// The first is the shape every SET benchmark and half the world's caches
1051 /// have: the same keys, the same value size, over and over. Writing a fresh
1052 /// record for each of those makes a dead one to go with it, and compaction
1053 /// then spends a quarter of the server's write throughput copying live
1054 /// records out from between them.
1055 #[test]
1056 fn an_overwrite_of_the_same_size_makes_no_garbage() {
1057 let mut m = RawMap::new();
1058 m.set(b"k", b"12345678");
1059 m.set(b"other", b"untouched");
1060 let live = m.arena().live_bytes();
1061 let dead = m.arena().dead_bytes_total();
1062
1063 for i in 0..1000u32 {
1064 let v = format!("{i:08}");
1065 assert_eq!(m.set(b"k", v.as_bytes()), Some(8));
1066 }
1067
1068 assert_eq!(m.get(b"k"), Some(&b"00000999"[..]));
1069 assert_eq!(m.get(b"other"), Some(&b"untouched"[..]));
1070 assert_eq!(m.len(), 2);
1071 assert_eq!(m.arena().live_bytes(), live, "a thousand writes, no growth");
1072 assert_eq!(m.arena().dead_bytes_total(), dead, "and nothing dead");
1073
1074 // A different length cannot go in the same hole, because the record has
1075 // to be as long as its header says it is.
1076 assert_eq!(m.set(b"k", b"123456789"), Some(8));
1077 assert_eq!(m.get(b"k"), Some(&b"123456789"[..]));
1078 assert!(
1079 m.arena().dead_bytes_total() > dead,
1080 "the old record is dead"
1081 );
1082 }
1083
1084 /// An expiring value and a plain one are different record lengths, so the
1085 /// one does not get written over the other.
1086 ///
1087 /// This is the case the in place path has to refuse rather than the case it
1088 /// is for, and it is the one that would corrupt a record if it took it: the
1089 /// value here is a keyspace record, whose deadline is inside the value, so
1090 /// two values of the same visible length are two different record lengths.
1091 #[test]
1092 fn a_longer_value_moves_and_the_index_follows_it() {
1093 let mut m = RawMap::new();
1094 m.set(b"k", b"aaaa");
1095 let first = m
1096 .index()
1097 .get(RawMap::hash_of(b"k"), b"k", &Records { arena: m.arena() });
1098
1099 m.set(b"k", b"aaaaaaaa");
1100 let second = m
1101 .index()
1102 .get(RawMap::hash_of(b"k"), b"k", &Records { arena: m.arena() });
1103
1104 assert_ne!(first, second, "a longer value needs a new record");
1105 assert_eq!(m.get(b"k"), Some(&b"aaaaaaaa"[..]));
1106 }
1107
1108 #[test]
1109 fn empty_key_and_empty_value() {
1110 let mut m = RawMap::new();
1111 m.set(b"", b"");
1112 assert_eq!(m.get(b""), Some(&b""[..]));
1113 m.set(b"x", b"");
1114 assert_eq!(m.get(b"x"), Some(&b""[..]));
1115 assert_eq!(m.len(), 2);
1116 }
1117
1118 #[test]
1119 fn grows_through_many_splits() {
1120 let mut m = RawMap::new();
1121 const N: usize = GROW_N;
1122 for i in 0..N {
1123 m.set(&key(i), &val(i));
1124 }
1125 assert_eq!(m.len(), N);
1126 assert!(
1127 m.index().splits() > 4,
1128 "expected real growth, saw {} splits",
1129 m.index().splits()
1130 );
1131 for i in 0..N {
1132 assert_eq!(
1133 m.get(&key(i)),
1134 Some(val(i).as_slice()),
1135 "lost key {i} after {} splits",
1136 m.index().splits()
1137 );
1138 }
1139 for i in (0..N).step_by(3) {
1140 assert!(m.del(&key(i)), "delete missed key {i}");
1141 }
1142 for i in 0..N {
1143 assert_eq!(
1144 m.contains(&key(i)),
1145 i % 3 != 0,
1146 "wrong presence for key {i}"
1147 );
1148 }
1149 }
1150
1151 #[test]
1152 fn compaction_preserves_everything() {
1153 let mut m = RawMap::new();
1154 // Enough to fill several arena segments with 1 KiB values.
1155 let val = vec![b'z'; COMPACT_VAL];
1156 const N: usize = COMPACT_N;
1157 for i in 0..N {
1158 m.set(&key(i), &val);
1159 }
1160 // Kill half, which pushes the early segments over the dead ratio.
1161 for i in (0..N).step_by(2) {
1162 m.del(&key(i));
1163 }
1164 let candidates = m.arena().compaction_candidates();
1165 assert!(
1166 !candidates.is_empty(),
1167 "expected at least one segment past the dead ratio"
1168 );
1169 for seg in candidates {
1170 m.compact_segment(seg);
1171 }
1172 for i in 0..N {
1173 let want = if i % 2 == 0 { None } else { Some(val.clone()) };
1174 assert_eq!(m.get(&key(i)).map(|v| v.to_vec()), want, "key {i}");
1175 }
1176 }
1177
1178 /// A mark follows its record wherever the record goes.
1179 ///
1180 /// The whole reason the marked set lives in this file. Compaction moves a
1181 /// record to a new address without anybody writing to it, so a set of
1182 /// addresses kept by a caller would be pointing at freed space afterwards,
1183 /// and the sample would read whatever the arena handed out next.
1184 #[test]
1185 fn compaction_carries_the_marks_with_it() {
1186 let mut m = RawMap::new();
1187 let val = vec![b'z'; COMPACT_VAL];
1188 const N: usize = COMPACT_N;
1189 for i in 0..N {
1190 m.set_with(
1191 &key(i),
1192 val.len(),
1193 |_| {},
1194 |b| {
1195 b.copy_from_slice(&val);
1196 i % 3 == 0
1197 },
1198 );
1199 }
1200 let want = (0..N).filter(|i| i % 3 == 0).count();
1201 assert_eq!(m.tagged_len(), want);
1202
1203 for i in (0..N).step_by(2) {
1204 m.del(&key(i));
1205 }
1206 let want = (0..N).filter(|i| i % 3 == 0 && i % 2 == 1).count();
1207 assert_eq!(m.tagged_len(), want, "a delete takes the mark with it");
1208
1209 for seg in m.arena().compaction_candidates() {
1210 m.compact_segment(seg);
1211 }
1212 assert_eq!(
1213 m.tagged_len(),
1214 want,
1215 "and compaction moves it rather than losing it"
1216 );
1217
1218 // Every mark points at a record that is still there and is one of the
1219 // ones that was marked, which is what a stale address would fail.
1220 let mut seen = 0;
1221 m.sample_tagged(0, |k, _, addr| {
1222 assert!(m.get(k).is_some(), "a mark on a key that is gone");
1223 let i: usize = std::str::from_utf8(&k[4..]).unwrap().parse().unwrap();
1224 assert!(
1225 i.is_multiple_of(3) && !i.is_multiple_of(2),
1226 "key {i} was never marked"
1227 );
1228 assert!(m.is_tagged(addr));
1229 seen += 1;
1230 true
1231 });
1232 assert_eq!(seen, want);
1233 }
1234
1235 /// A mark goes on and comes off with the record's own bytes, which is how
1236 /// PERSIST works: the value is the same length, so the record does not move
1237 /// and only the mark changes.
1238 #[test]
1239 fn a_mark_goes_on_and_comes_off_in_place() {
1240 let mut m = RawMap::new();
1241 let mark = |m: &mut RawMap, on: bool| {
1242 m.set_with(
1243 b"k",
1244 1,
1245 |_| {},
1246 |b| {
1247 b[0] = b'v';
1248 on
1249 },
1250 )
1251 };
1252 mark(&mut m, true);
1253 assert_eq!(m.tagged_len(), 1);
1254 mark(&mut m, true);
1255 assert_eq!(m.tagged_len(), 1, "marking twice is marking once");
1256 mark(&mut m, false);
1257 assert_eq!(m.tagged_len(), 0);
1258 mark(&mut m, true);
1259 assert_eq!(m.tagged_len(), 1);
1260 assert!(m.del(b"k"));
1261 assert_eq!(m.tagged_len(), 0);
1262 }
1263
1264 /// The bug this exists for: overwriting a key writes a new record and only
1265 /// counts the old one dead, so without compaction a server that rewrites
1266 /// the same keys holds every version of every one of them forever. Measured
1267 /// on a real server before this, 400000 sets over 100000 keys came to 742
1268 /// bytes a key for 64 byte values.
1269 #[test]
1270 fn rewriting_the_same_keys_stops_growing() {
1271 let mut m = RawMap::new();
1272 let val = vec![b'z'; COMPACT_VAL];
1273 const N: usize = COMPACT_N;
1274
1275 for i in 0..N {
1276 m.set(&key(i), &val);
1277 m.compact_step();
1278 }
1279 let after_first_pass = m.arena().reserved_bytes();
1280
1281 // Nine more passes over the same keys, writing the same amount of data
1282 // nine more times and keeping exactly as much of it.
1283 for _ in 0..9 {
1284 for i in 0..N {
1285 m.set(&key(i), &val);
1286 m.compact_step();
1287 }
1288 }
1289 let after_ten = m.arena().reserved_bytes();
1290
1291 assert!(
1292 after_ten <= after_first_pass * 2,
1293 "held {after_ten} after ten passes against {after_first_pass} after one, \
1294 which is the grow forever shape"
1295 );
1296 assert!(
1297 after_ten < m.arena().live_bytes() * 2,
1298 "held {after_ten} for {} live, which is more than the ratio allows",
1299 m.arena().live_bytes()
1300 );
1301 for i in 0..N {
1302 assert_eq!(
1303 m.get(&key(i)).map(<[u8]>::to_vec),
1304 Some(val.clone()),
1305 "key {i}"
1306 );
1307 }
1308 }
1309
1310 /// A segment is evacuated over several calls, and it comes back only on the
1311 /// call whose walk reaches the end of it.
1312 ///
1313 /// This is what the budget is for. One call used to copy every live record
1314 /// in two megabytes, around twenty six thousand of them at 64 byte values,
1315 /// and the whole batch of replies queued behind it waited for all of them.
1316 /// That is where a p99 of 3.9 milliseconds on the write rows came from
1317 /// while the p50 was in line with Redis: the median command paid nothing
1318 /// and one command in a few thousand paid for a segment.
1319 ///
1320 /// The loop is also what catches a walk that restarts instead of resuming.
1321 /// A restart would move records and look like progress, and it would spend
1322 /// every call re-walking the dead space it made on the last one, so the
1323 /// cursor would never reach the bump and the segment would never come back.
1324 #[test]
1325 fn a_segment_comes_back_over_several_calls() {
1326 let mut m = RawMap::new();
1327 let val = vec![b'z'; COMPACT_VAL];
1328 const N: usize = COMPACT_N;
1329 for i in 0..N {
1330 m.set(&key(i), &val);
1331 }
1332 // Every other key, so the early segments are well past the dead ratio
1333 // and there is still a live half to copy out.
1334 for i in (0..N).step_by(2) {
1335 m.del(&key(i));
1336 }
1337
1338 let rec = (HDR + key(0).len() + COMPACT_VAL).next_multiple_of(yo_arena::ALIGN);
1339 let per_call = m.budget() / rec + 1;
1340 let free = m.arena().free_segments();
1341
1342 let moved = m.compact_step().expect("half of it is dead");
1343 assert!(
1344 moved <= per_call,
1345 "one call moved {moved} records and the budget is {per_call}"
1346 );
1347 assert_eq!(
1348 m.arena().free_segments(),
1349 free,
1350 "a segment came back before the walk reached the end of it"
1351 );
1352
1353 let mut calls = 1;
1354 while m.arena().free_segments() == free {
1355 m.compact_step()
1356 .expect("the segment in flight is not finished");
1357 calls += 1;
1358 assert!(calls < 1000, "the walk is not getting any further along");
1359 }
1360 assert!(calls > 2, "the whole segment came back in {calls} calls");
1361
1362 for i in 0..N {
1363 let want = if i % 2 == 0 { None } else { Some(val.clone()) };
1364 assert_eq!(m.get(&key(i)).map(<[u8]>::to_vec), want, "key {i}");
1365 }
1366 }
1367
1368 /// A store barely holding any garbage collects nothing until it is asked to.
1369 ///
1370 /// The global ratio is the reason [`RawMap::compact_hard`] exists. A server
1371 /// under a memory limit needs the pages back whether or not the store as a
1372 /// whole is dirty enough to be worth a sweep, and a server that is not under
1373 /// one should not pay for copying that buys it a few kilobytes.
1374 ///
1375 /// The per segment ratio is a different question and the hard path keeps it.
1376 /// What is being asked for here is a store that is clean overall and has one
1377 /// part of it that is not, which is why the deletes are a run and not a
1378 /// stride: records land in the order they were written, so a run of them
1379 /// empties out the segments it lands in rather than taking a tenth off every
1380 /// segment and leaving none of them worth moving.
1381 #[test]
1382 fn a_store_with_little_dead_in_it_only_collects_when_pushed() {
1383 let mut m = RawMap::new();
1384 let val = vec![b'z'; COMPACT_VAL];
1385 const N: usize = COMPACT_N;
1386 const DEAD: usize = N / 10;
1387 for i in 0..N {
1388 m.set(&key(i), &val);
1389 }
1390 // A tenth of the keys, which is under the eighth of everything held that
1391 // compaction normally waits for.
1392 for i in 0..DEAD {
1393 m.del(&key(i));
1394 }
1395
1396 assert_eq!(m.compact_step(), None, "not worth collecting");
1397 let free = m.arena().free_segments();
1398 let mut calls = 0;
1399 while m.arena().free_segments() == free {
1400 assert!(
1401 m.compact_hard().is_some(),
1402 "there is a segment holding something dead"
1403 );
1404 calls += 1;
1405 assert!(calls < 1000, "the walk is not getting any further along");
1406 }
1407 // Everything still reads back, which is the thing that matters: the
1408 // records that were live in the segment that came back were moved and
1409 // their index entries were moved with them.
1410 for i in 0..N {
1411 let want = if i < DEAD { None } else { Some(val.clone()) };
1412 assert_eq!(m.get(&key(i)).map(<[u8]>::to_vec), want, "key {i}");
1413 }
1414 }
1415
1416 /// A store with a little dead spread thinly through it collects nothing,
1417 /// however hard it is asked.
1418 ///
1419 /// One key in fifty, so no segment is anywhere near worth emptying. There is
1420 /// no pressure high enough to make copying forty nine bytes to get one back
1421 /// the right move, because a caller under pressure has something cheaper it
1422 /// could be doing with the same effort.
1423 #[test]
1424 fn a_barely_dead_store_collects_nothing_however_hard_it_is_asked() {
1425 let mut m = RawMap::new();
1426 let val = vec![b'z'; COMPACT_VAL];
1427 const N: usize = COMPACT_N;
1428 for i in 0..N {
1429 m.set(&key(i), &val);
1430 }
1431 for i in (0..N).step_by(50) {
1432 m.del(&key(i));
1433 }
1434
1435 assert_eq!(m.compact_step(), None, "not worth collecting");
1436 assert_eq!(m.compact_hard(), None, "fifty bytes moved for one back");
1437 }
1438
1439 /// Compaction says what it walked past and what it had to copy.
1440 ///
1441 /// The two are separate because they cost different things and because the
1442 /// gap between them is the useful part: a walk that steps over a thousand
1443 /// records and copies two got its segment back cheaply, and one that copies
1444 /// nine hundred of them paid nearly the price of the writes twice over.
1445 #[test]
1446 fn compaction_counts_what_it_walked_and_what_it_moved() {
1447 let mut m = RawMap::new();
1448 let val = vec![b'z'; COMPACT_VAL];
1449 const N: usize = COMPACT_N;
1450 for i in 0..N {
1451 m.set(&key(i), &val);
1452 }
1453 assert_eq!(
1454 m.compaction(),
1455 Compaction::default(),
1456 "a load with nothing dead in it has nothing to collect"
1457 );
1458
1459 // Half of them dead, so a walk over a segment should find about half of
1460 // what it steps over still live.
1461 for i in (0..N).step_by(2) {
1462 m.del(&key(i));
1463 }
1464 for _ in 0..200 {
1465 m.compact_step();
1466 }
1467 let c = m.compaction();
1468 assert!(c.walked > 0, "the walk did not step over anything");
1469 assert!(c.moved > 0, "everything it stepped over was dead");
1470 assert!(c.moved < c.walked, "nothing it stepped over was dead");
1471 assert!(
1472 c.bytes >= c.moved * COMPACT_VAL as u64,
1473 "{} records moved and only {} bytes with them",
1474 c.moved,
1475 c.bytes
1476 );
1477
1478 // What a store has spent is not something a flush gives back.
1479 m.clear();
1480 assert_eq!(m.compaction(), c, "the bill was thrown away with the data");
1481 }
1482
1483 /// The budget grows with how far behind the collector is.
1484 ///
1485 /// A store with one segment waiting pays the floor, which is the pause a
1486 /// command can be asked to wait for. One with a queue of them walks a slice
1487 /// per segment in the queue, which is what keeps a pipelined write flood
1488 /// from outrunning one call per batch and leaving the process holding the
1489 /// segments that never got their turn.
1490 #[test]
1491 fn the_budget_scales_with_the_backlog() {
1492 let mut m = RawMap::new();
1493 let val = vec![b'z'; COMPACT_VAL];
1494 const N: usize = COMPACT_N;
1495 for i in 0..N {
1496 m.set(&key(i), &val);
1497 }
1498 assert_eq!(m.budget(), EVAC_FLOOR, "nothing is waiting yet");
1499
1500 for i in 0..N {
1501 m.del(&key(i));
1502 }
1503 let flooded = m.budget();
1504 assert!(
1505 flooded >= EVAC_FLOOR * m.arena().candidate_count(),
1506 "{} segments are waiting and the budget is {flooded}",
1507 m.arena().candidate_count()
1508 );
1509 assert!(
1510 flooded > EVAC_FLOOR,
1511 "every segment is dead and the budget is still the floor"
1512 );
1513 assert!(flooded <= EVAC_CEILING, "walked past a whole segment");
1514 }
1515
1516 /// A segment that is partway through being evacuated is finished before a
1517 /// worse one is started.
1518 ///
1519 /// Writes keep coming while a segment is being walked and they make dead
1520 /// space elsewhere, so the answer to "which segment is worst" moves around
1521 /// underneath a walk that takes thirty calls. Asking it again on every call
1522 /// would let a segment be put down at nine tenths done in favour of one
1523 /// that is slightly worse, and the arena would fill up with segments that
1524 /// are nearly empty and never reclaimed.
1525 ///
1526 /// Here the first quarter of the keyspace is deleted so that the segment at
1527 /// the front is the only candidate, one call starts on it, and then the
1528 /// back half goes too so that another segment ties with it mid walk. The
1529 /// tie goes to the later segment, so a walk that asked again would move to
1530 /// it and leave the first one part done.
1531 #[test]
1532 fn the_segment_in_flight_is_finished_first() {
1533 let mut m = RawMap::new();
1534 let val = vec![b'z'; COMPACT_VAL];
1535 const N: usize = COMPACT_N;
1536 for i in 0..N {
1537 m.set(&key(i), &val);
1538 }
1539 for i in 0..N / 4 {
1540 m.del(&key(i));
1541 }
1542
1543 let free = m.arena().free_segments();
1544 let first = m.arena().worst_candidate().expect("the front is all dead");
1545 m.compact_step().expect("there is a candidate");
1546
1547 for i in N / 2..N {
1548 m.del(&key(i));
1549 }
1550 let worse = m.arena().worst_candidate().expect("the back is all dead");
1551 assert_ne!(worse, first, "the test needs the answer to have moved");
1552
1553 while m.arena().free_segments() == free {
1554 m.compact_step()
1555 .expect("the segment in flight is not finished");
1556 }
1557 assert!(
1558 m.arena().is_free(first),
1559 "the segment that was in flight is not the one that came back"
1560 );
1561 assert!(
1562 !m.arena().is_free(worse),
1563 "the walk moved to the segment that tied with it partway through"
1564 );
1565 }
1566
1567 /// A segment that compaction emptied is bumped through again rather than
1568 /// sitting there holding two megabytes.
1569 #[test]
1570 fn an_emptied_segment_is_used_again() {
1571 let mut m = RawMap::new();
1572 let val = vec![b'z'; COMPACT_VAL];
1573 const N: usize = COMPACT_N;
1574 for i in 0..N {
1575 m.set(&key(i), &val);
1576 }
1577 for i in (0..N).step_by(2) {
1578 m.del(&key(i));
1579 }
1580
1581 let before = m.arena().segment_count();
1582 let seg = m.arena().worst_candidate().expect("half of it is dead");
1583 m.compact_segment(seg);
1584 assert_eq!(
1585 m.arena().free_segments(),
1586 1,
1587 "the segment did not come back"
1588 );
1589
1590 // Write until the free segment has to be taken, and the count is where
1591 // it was rather than one higher.
1592 for i in N..N * 2 {
1593 m.set(&key(i), &val);
1594 if m.arena().free_segments() == 0 {
1595 break;
1596 }
1597 }
1598 assert_eq!(
1599 m.arena().segment_count(),
1600 before,
1601 "asked the system for memory while holding an empty segment"
1602 );
1603 }
1604
1605 #[test]
1606 fn adversarial_keys_that_share_low_bits() {
1607 // Keys chosen so that many land in the same bucket index. The point is
1608 // that overflow chaining and splitting both still work when the hash is
1609 // not being kind.
1610 let mut m = RawMap::new();
1611 let mut inserted = Vec::new();
1612 for i in 0..ADVERSARIAL_N {
1613 let k = i.to_le_bytes().to_vec();
1614 m.set(&k, b"v");
1615 inserted.push(k);
1616 }
1617 for k in &inserted {
1618 assert_eq!(m.get(k), Some(&b"v"[..]));
1619 }
1620 assert_eq!(m.len(), inserted.len());
1621 }
1622
1623 /// Whatever memoizes against this counter is only correct if every way of
1624 /// moving something in the map moves it too. A method that mutates and does
1625 /// not is not a slow memo, it is a wrong answer, so this asserts on the whole
1626 /// `&mut self` surface rather than on the ones that look like they matter.
1627 ///
1628 /// The single exception is pinned by the test below this one, so a method
1629 /// added without a decision about which side it falls on fails here.
1630 #[test]
1631 fn every_way_of_writing_moves_the_counter() {
1632 let mut m = RawMap::new();
1633 let mut last = m.writes();
1634 let mut moved = |m: &RawMap, what: &str| {
1635 assert!(m.writes() > last, "{what} did not move the counter");
1636 last = m.writes();
1637 };
1638
1639 m.set(b"k", b"v");
1640 moved(&m, "set");
1641 m.set_with(
1642 b"k",
1643 1,
1644 |_| {},
1645 |b| {
1646 b[0] = b'w';
1647 false
1648 },
1649 );
1650 moved(&m, "set_with");
1651 m.value_mut(b"k");
1652 moved(&m, "value_mut");
1653 m.value_mut_hashed(RawMap::hash_of(b"k"), b"k");
1654 moved(&m, "value_mut_hashed");
1655 m.compact_step();
1656 moved(&m, "compact_step");
1657 m.compact_segment(0);
1658 moved(&m, "compact_segment");
1659 m.del(b"k");
1660 moved(&m, "del");
1661 }
1662
1663 /// The exception, pinned so that it stays a decision rather than becoming a
1664 /// habit. An in place stamp leaves the counter alone, and everything the
1665 /// caller resolved before it is still right after it.
1666 #[test]
1667 fn sampling_hands_back_real_entries_and_stops_when_told() {
1668 let mut m = RawMap::new();
1669 for i in 0..2000u32 {
1670 m.set(format!("k{i}").as_bytes(), format!("v{i}").as_bytes());
1671 }
1672
1673 // Whatever it hands over is really in the map, key and value together,
1674 // and the address it gives is the address that key resolves to.
1675 let mut count = 0usize;
1676 m.sample(0x1234_5678_9abc_def0, |key, val, addr| {
1677 assert_eq!(m.get(key), Some(val));
1678 assert_eq!(m.find(key), Some(addr));
1679 count += 1;
1680 count < 5
1681 });
1682 assert_eq!(count, 5, "it did not stop when it was told to");
1683
1684 // A caller that never says stop still terminates, because the segment is
1685 // the bound and not the caller.
1686 let mut all = 0usize;
1687 m.sample(0, |_, _, _| {
1688 all += 1;
1689 true
1690 });
1691 assert!(all > 0, "it found nothing in a map of two thousand keys");
1692 assert!(
1693 all < m.len(),
1694 "one segment and not the whole map, got {all} of {}",
1695 m.len()
1696 );
1697 }
1698
1699 #[test]
1700 fn sampling_a_sparse_map_still_finds_something() {
1701 // The case a sampler that looked in one bucket would get wrong. Two keys
1702 // in a map sized for two thousand is sixty two empty buckets for every
1703 // two that are worth looking in.
1704 let mut m = RawMap::new();
1705 for i in 0..2000u32 {
1706 m.set(format!("k{i}").as_bytes(), b"v");
1707 }
1708 for i in 0..1998u32 {
1709 m.del(format!("k{i}").as_bytes());
1710 }
1711 assert_eq!(m.len(), 2);
1712
1713 // Not every draw lands in the segment those two are in, so this is about
1714 // whether it ever finds them rather than whether it always does.
1715 let mut found = 0usize;
1716 for r in 0..200u64 {
1717 m.sample(r.wrapping_mul(0x9e37_79b9_7f4a_7c15), |_, _, _| {
1718 found += 1;
1719 true
1720 });
1721 }
1722 assert!(found > 0, "two hundred draws and it never found either key");
1723 }
1724
1725 #[test]
1726 fn stamping_a_value_in_place_is_not_a_write() {
1727 let mut m = RawMap::new();
1728 m.set(b"k", b"hello");
1729 let addr = m.find(b"k").expect("just stored");
1730 let before = m.writes();
1731
1732 m.value_at_mut(addr)[0] = b'j';
1733
1734 assert_eq!(m.writes(), before, "a stamp counted as a write");
1735 assert_eq!(m.get(b"k"), Some(&b"jello"[..]));
1736 // And the address the caller was holding still means what it meant, which
1737 // is the guarantee the counter would otherwise be asked about.
1738 assert_eq!(m.find(b"k"), Some(addr));
1739 assert_eq!(m.value_at(addr), b"jello");
1740 }
1741
1742 /// `clear` replaces the map with a fresh one, and a fresh one starts at
1743 /// zero. A memo taken at write 3 against a map that went back to 0 and
1744 /// climbed to 3 again would read as still valid on the one call where every
1745 /// key in the map had been thrown away.
1746 #[test]
1747 fn clearing_does_not_send_the_counter_backwards() {
1748 let mut m = RawMap::new();
1749 for i in 0..10u32 {
1750 m.set(&i.to_le_bytes(), b"v");
1751 }
1752 let before = m.writes();
1753 m.clear();
1754 assert!(m.writes() > before, "clear went backwards or stood still");
1755 }
1756
1757 /// Enough keys to have split several times, so a walk crosses segments of
1758 /// different local depths rather than staying inside one.
1759 #[cfg(miri)]
1760 const SCAN_N: usize = 400;
1761 #[cfg(not(miri))]
1762 const SCAN_N: usize = 20_000;
1763
1764 /// How many keys go in between one call of a growing walk and the next.
1765 ///
1766 /// This is the number that decides what that test costs, and not `SCAN_N`,
1767 /// which is why it is its own constant. The walk takes eight keys a call
1768 /// and this puts keys back in behind it, so at 64 the map grows eight times
1769 /// faster than the walk eats it and the loop runs until the directory has
1770 /// doubled its way out from under the whole thing. Shrinking the starting
1771 /// population without shrinking this leaves the ratio where it was and the
1772 /// test still runs for twenty minutes interpreted.
1773 ///
1774 /// Eight is one call's worth, so the map still grows during the walk and
1775 /// the directory still doubles, which is the assertion. What goes away is
1776 /// the number of times over.
1777 #[cfg(miri)]
1778 const GREW_PER_CALL: usize = 8;
1779 #[cfg(not(miri))]
1780 const GREW_PER_CALL: usize = 64;
1781
1782 #[test]
1783 fn a_walk_of_an_empty_map_ends_on_the_first_call() {
1784 let m = RawMap::new();
1785 let mut seen = 0;
1786 let at = m.scan(Cursor::START, 1000, |_, _| seen += 1);
1787 assert_eq!(seen, 0);
1788 assert!(
1789 at.is_end(),
1790 "an empty map took more than one call to finish"
1791 );
1792 }
1793
1794 /// The plain case, and the one every other guarantee is stated against: no
1795 /// writes during the walk, so every key comes back once and no key comes
1796 /// back twice.
1797 #[test]
1798 fn a_quiet_walk_returns_every_key_exactly_once() {
1799 let mut m = RawMap::new();
1800 for i in 0..SCAN_N {
1801 m.set(&key(i), &val(i));
1802 }
1803
1804 let mut counts: HashMap<Vec<u8>, usize> = HashMap::new();
1805 let mut at = Cursor::START;
1806 let mut calls = 0;
1807 loop {
1808 at = m.scan(at, 1, |k, v| {
1809 // Both borrows are shared, so the walk can look the key up
1810 // while it is handing it over. The pair arriving together is
1811 // the point: a bucket walk that read the header of one record
1812 // and the body of the next would still pass a key only check.
1813 assert_eq!(m.get(k), Some(v), "the value came back on the wrong key");
1814 *counts.entry(k.to_vec()).or_default() += 1;
1815 });
1816 calls += 1;
1817 assert!(calls < 1_000_000, "the cursor is not advancing");
1818 if at.is_end() {
1819 break;
1820 }
1821 }
1822
1823 assert_eq!(
1824 counts.len(),
1825 SCAN_N,
1826 "the walk missed keys or invented them"
1827 );
1828 for i in 0..SCAN_N {
1829 assert_eq!(counts.get(&key(i)).copied(), Some(1), "key {i}");
1830 }
1831 }
1832
1833 /// A budget is a floor and not a ceiling, and asking for everything at once
1834 /// is one call.
1835 #[test]
1836 fn a_budget_big_enough_finishes_in_one_call() {
1837 let mut m = RawMap::new();
1838 for i in 0..SCAN_N {
1839 m.set(&key(i), &val(i));
1840 }
1841
1842 let mut seen = 0;
1843 let at = m.scan(Cursor::START, usize::MAX, |_, _| seen += 1);
1844 assert_eq!(seen, SCAN_N);
1845 assert!(at.is_end());
1846 }
1847
1848 /// The guarantee that matters: the map grows underneath the walk, the
1849 /// directory doubles and segments split, and a key that was there the whole
1850 /// time still comes back.
1851 ///
1852 /// Written the way a client uses it, which is a cursor held across calls
1853 /// with other work happening in between, because the failure this is looking
1854 /// for is a cursor that means one thing before a split and another after.
1855 #[test]
1856 fn a_walk_survives_the_map_growing_underneath_it() {
1857 let mut m = RawMap::new();
1858 // The keys that are there throughout. Named apart from the ones added
1859 // during the walk so the two are easy to tell apart in the assertion.
1860 for i in 0..SCAN_N {
1861 m.set(&key(i), &val(i));
1862 }
1863 let depth_before = m.index().global_depth();
1864
1865 let mut seen: HashSet<Vec<u8>> = HashSet::new();
1866 let mut at = Cursor::START;
1867 let mut added = SCAN_N;
1868 loop {
1869 at = m.scan(at, 8, |k, _| {
1870 seen.insert(k.to_vec());
1871 });
1872 if at.is_end() {
1873 break;
1874 }
1875 // Between one call and the next, which is where a client would be.
1876 for _ in 0..GREW_PER_CALL {
1877 m.set(&key(added), &val(added));
1878 added += 1;
1879 }
1880 }
1881
1882 assert!(
1883 m.index().global_depth() > depth_before,
1884 "the directory never doubled, so this test proved nothing"
1885 );
1886 for i in 0..SCAN_N {
1887 assert!(
1888 seen.contains(&key(i)),
1889 "key {i} was there throughout and never came back"
1890 );
1891 }
1892 }
1893
1894 /// Deletes during a walk are the other half of the same guarantee. A key
1895 /// that survives to the end still comes back, whatever happened to its
1896 /// neighbours.
1897 #[test]
1898 fn a_walk_survives_keys_being_deleted_underneath_it() {
1899 let mut m = RawMap::new();
1900 for i in 0..SCAN_N {
1901 m.set(&key(i), &val(i));
1902 }
1903
1904 let mut seen: HashSet<Vec<u8>> = HashSet::new();
1905 let mut at = Cursor::START;
1906 let mut next_gone = 1;
1907 loop {
1908 at = m.scan(at, 8, |k, _| {
1909 seen.insert(k.to_vec());
1910 });
1911 if at.is_end() {
1912 break;
1913 }
1914 // Every odd key goes, a few at a time. The even ones are what the
1915 // assertion is about.
1916 for _ in 0..16 {
1917 if next_gone < SCAN_N {
1918 m.del(&key(next_gone));
1919 next_gone += 2;
1920 }
1921 }
1922 }
1923
1924 for i in (0..SCAN_N).step_by(2) {
1925 assert!(
1926 seen.contains(&key(i)),
1927 "key {i} was never deleted and never came back"
1928 );
1929 }
1930 }
1931
1932 /// A cursor names a place in the keyspace and not a place in memory, so a
1933 /// walk started partway through returns everything from there on.
1934 ///
1935 /// The prefix is what says where that is. Starting at prefix `p` resumes in
1936 /// the segment holding `p`, which begins at or before it, so every key whose
1937 /// own prefix is `p` or higher is still ahead of the walk.
1938 #[test]
1939 fn a_walk_that_starts_partway_returns_everything_from_there_on() {
1940 let mut m = RawMap::new();
1941 for i in 0..SCAN_N {
1942 m.set(&key(i), &val(i));
1943 }
1944
1945 let half = 1u64 << (crate::scan::PREFIX_BITS - 1);
1946 let mut seen: HashSet<Vec<u8>> = HashSet::new();
1947 let at = m.scan(Cursor::at(half, 0), usize::MAX, |k, _| {
1948 seen.insert(k.to_vec());
1949 });
1950 assert!(at.is_end());
1951
1952 let mut expected = 0;
1953 for i in 0..SCAN_N {
1954 let k = key(i);
1955 if Cursor::prefix_of(RawMap::hash_of(&k)) >= half {
1956 expected += 1;
1957 assert!(
1958 seen.contains(&k),
1959 "key {i} is past the cursor and did not come back"
1960 );
1961 }
1962 }
1963 // Both halves of the keyspace have keys in them, or the assertion above
1964 // is checking nothing.
1965 assert!(
1966 expected > 0 && expected < SCAN_N,
1967 "the split point was degenerate"
1968 );
1969 }
1970}