yo_resp/dispatch/load.rs
1//! Reading a whole RDB file back into the keyspace.
2//!
3//! The other direction from [`super::persist`], and the one that arrived second.
4//! Writing a file is what makes the dataset readable by everything else in the
5//! Redis world, and reading one is what makes everything else in the Redis world
6//! readable here, which is the half a person migrating actually cares about.
7//!
8//! # Two callers, one walk
9//!
10//! `DEBUG RELOAD` writes the file and reads it straight back, so that a suite can
11//! prove a value survives the round trip. `yodb serve --restore` reads a file
12//! somebody else wrote before the port opens, so that a server starts up holding
13//! a dataset that came off a real Redis. They want the same thing done to the
14//! keyspace and they differ only in where the bytes came from and what gets
15//! printed when it goes wrong, so the walk is here and the two of them bring
16//! their own file and their own log line.
17//!
18//! # Why a file that goes wrong halfway leaves a mess
19//!
20//! Because that is what a real server does, and matching it is worth more than
21//! improving on it. Everything cheap that can be wrong is answered before the
22//! keyspace is touched at all: the magic, the version and the checksum are all
23//! checked by [`Load::open`], which reads every byte of the file, so a damaged
24//! file is refused with the dataset still standing. What is left after that is a
25//! value that will not build, and the only way to be atomic about one of those
26//! would be to build the whole dataset beside the live one and swap, which costs
27//! twice the memory on the one path where memory is already the problem.
28
29use yo_kv::restore::{Fault, Item, Load};
30
31use super::{DATABASES, Server};
32
33/// What a file put where.
34///
35/// Counts and not keys, because both callers report rather than inspect. What a
36/// person wants to see after loading a file somebody handed them is how much of
37/// it landed and whether any of it was dropped on the way, and the answer to the
38/// second is what tells them the file is older than they thought.
39#[derive(Debug, Default, Clone, Copy)]
40pub struct Loaded {
41 /// The version out of the header.
42 pub version: u16,
43 /// Keys that landed, per database.
44 ///
45 /// A fixed array because there are sixteen databases and there is no
46 /// arrangement of a file that makes seventeen. Reported rather than summed
47 /// because a file that put everything in database nine is a file somebody
48 /// needs to know about before they wonder why `DBSIZE` says nought.
49 pub keys: [usize; DATABASES],
50 /// Keys the file carried that were already dead, so were read and dropped.
51 pub expired: usize,
52 /// Function libraries the file carried.
53 pub libraries: usize,
54}
55
56impl Loaded {
57 /// How many keys landed in all.
58 #[must_use]
59 pub fn total(&self) -> usize {
60 self.keys.iter().sum()
61 }
62}
63
64/// Why a load stopped.
65///
66/// Two arms, because there are two kinds of bad file. One is a file the reader
67/// can say something specific about, which is [`Fault`], and the other is a file
68/// that parsed perfectly and asked for a database this server does not have,
69/// which is not the reader's business to refuse.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum Refused {
72 /// The reader would not go on, and said why.
73 Fault(Fault),
74 /// The file has a key in a database past the last one.
75 Database(usize),
76}
77
78impl core::fmt::Display for Refused {
79 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
80 match self {
81 Refused::Fault(fault) => write!(f, "{fault}"),
82 Refused::Database(db) => write!(
83 f,
84 "the file has a key in database {db} and there are {DATABASES}"
85 ),
86 }
87 }
88}
89
90impl Server {
91 /// Build the dataset in `image` into this server.
92 ///
93 /// `flush` says whether what is already here goes first. Dropping it is what
94 /// a reload wants, since the file it just wrote holds all of it, and keeping
95 /// it is what `NOFLUSH` and a merge want.
96 ///
97 /// The flush takes the databases and leaves the search indexes alone, which
98 /// is the one thing this does differently from `FLUSHALL`. An index is a
99 /// schema and its own copy of what it has read, and on a reload every key it
100 /// follows comes back under the same name with the same value, so dropping
101 /// it would mean rebuilding it against a keyspace that already agrees with
102 /// it. On a restore into an empty server there is no index to drop.
103 ///
104 /// Nothing here allocates on a command path by accident, so the caller wraps
105 /// it in [`yo_alloc::allow`]: building a dataset is all allocation and that
106 /// is what it is for.
107 ///
108 /// # Errors
109 ///
110 /// [`Refused`] for a file that will not parse or that wants a database this
111 /// server has not got. The keyspace is untouched when the header, the
112 /// version or the checksum is the problem, because those are answered before
113 /// the flush, and half loaded when a value in the middle of the file is.
114 pub fn load_image(&self, image: &[u8], flush: bool) -> Result<Loaded, Refused> {
115 // Any stripe of any database carries the same four thresholds, and they
116 // are copied out rather than borrowed because the keyspace they came off
117 // is about to be written into.
118 let bands = self.dbs[0].hold_stripe(0).bands();
119 let mut load =
120 Load::open(image, bands.limits(), self.clock.now_ms()).map_err(Refused::Fault)?;
121 let mut done = Loaded {
122 version: load.version(),
123 ..Loaded::default()
124 };
125 if flush {
126 for db in &self.dbs {
127 db.clear();
128 }
129 }
130 // By reference, because the count of keys the file dropped for having
131 // died is on the reader and a walk that takes it cannot be asked after.
132 for item in load.by_ref() {
133 match item {
134 Ok(Item::Key { db, key, record }) => {
135 let Some(into) = self.dbs.get(db) else {
136 return Err(Refused::Database(db));
137 };
138 into.hold(&key).import(&key, record);
139 done.keys[db] += 1;
140 }
141 Ok(Item::Library(_)) => done.libraries += 1,
142 // The writer talking about itself. Redis writes `redis-ver`,
143 // `redis-bits` and `ctime` and reads none of them back except to
144 // log, and there is nothing here that would read them either.
145 Ok(Item::Aux { .. }) => {}
146 Err(fault) => return Err(Refused::Fault(fault)),
147 }
148 }
149 done.expired = load.expired();
150 self.persist.note_load(&done);
151 Ok(done)
152 }
153}