Skip to main content

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}