Skip to main content

vcon_server/store/
backup.rs

1//! One tenant's Store copied into a backup while a server may be writing
2//! to it: the catalog with SQLite's online backup API, then the object file
3//! of every version row that copy holds.
4
5use std::collections::BTreeSet;
6use std::io;
7use std::path::Path;
8
9use super::Store;
10use crate::catalog;
11use crate::envelope;
12use crate::ids::VersionId;
13
14/// How many times the catalog is copied while versions in each copy are
15/// purged before their object files are copied.
16pub const TRIES: usize = 5;
17
18/// What `backup_to` copied.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct Copied {
21    /// Versions kept, hidden ones included.
22    pub versions: u64,
23    /// How many times the catalog was copied.
24    pub tries: usize,
25}
26
27/// Why a backup of a Store failed.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum BackupError {
30    /// A version in each of `TRIES` copies of the catalog was purged before
31    /// its object file could be copied.
32    KeptChanging,
33    /// A file could not be read, written or synced, or the catalog could
34    /// not be copied.
35    Io,
36}
37
38impl Store {
39    /// Copies this tenant's catalog and object files into `dir`, made if
40    /// new. Anything already in `dir` from an earlier try is replaced or
41    /// removed.
42    ///
43    /// Each try copies the catalog first, in one step inside one read
44    /// transaction, then the object file of each version row in that copy.
45    /// An object file is written once and never changed, so a file copied
46    /// at any try is still right; a row whose file is gone was purged after
47    /// the copy, and the next try copies the catalog again. Files the final
48    /// copy has no row for are removed. Every file and directory written is
49    /// synced, so the backup is whole once this returns.
50    pub fn backup_to(&self, dir: &Path) -> Result<Copied, BackupError> {
51        let fail = |_| BackupError::Io;
52        let key = envelope::catalog_key(&self.tenant_key).map_err(|_| BackupError::Io)?;
53        let objects = dir.join("objects");
54        self.fs.create_dir_all(&objects).map_err(fail)?;
55        let catalog = dir.join("catalog.sqlite");
56        for tries in 1..=TRIES {
57            for file in ["catalog.sqlite", "catalog.sqlite-wal", "catalog.sqlite-shm"] {
58                remove_if_there(&dir.join(file)).map_err(fail)?;
59            }
60            catalog::backup(&self.conn(), &catalog, &key).map_err(|_| BackupError::Io)?;
61            #[cfg(test)]
62            if let Some(hook) = self.hooks.copied.lock().expect("hook").as_mut() {
63                hook(self);
64            }
65            let versions = versions_in(&catalog, &key)?;
66            let present: BTreeSet<String> = self
67                .fs
68                .list(&objects)
69                .map_err(fail)?
70                .into_iter()
71                .filter_map(|n| n.into_string().ok())
72                .collect();
73            let mut missing = false;
74            for name in &versions {
75                if present.contains(name) {
76                    continue;
77                }
78                match self.fs.read(&self.objects.join(name)) {
79                    Ok(bytes) => {
80                        let tmp = objects.join(format!("{name}.tmp"));
81                        if present.contains(&format!("{name}.tmp")) {
82                            self.fs.remove(&tmp).map_err(fail)?;
83                        }
84                        self.fs.write_new(&tmp, &bytes).map_err(fail)?;
85                        self.fs.sync_file(&tmp).map_err(fail)?;
86                        self.fs.rename(&tmp, &objects.join(name)).map_err(fail)?;
87                    }
88                    Err(e) if e.kind() == io::ErrorKind::NotFound => missing = true,
89                    Err(e) => return Err(fail(e)),
90                }
91            }
92            if missing {
93                continue;
94            }
95            for name in self.fs.list(&objects).map_err(fail)? {
96                let name = name.into_string().unwrap_or_default();
97                if !versions.contains(&name) {
98                    self.fs.remove(&objects.join(&name)).map_err(fail)?;
99                }
100            }
101            self.fs.sync_dir(&objects).map_err(fail)?;
102            self.fs.sync_dir(dir).map_err(fail)?;
103            // A new directory survives a crash once the one holding it is
104            // synced too.
105            if let Some(parent) = dir.parent() {
106                self.fs.sync_dir(parent).map_err(fail)?;
107            }
108            return Ok(Copied {
109                versions: versions.len() as u64,
110                tries,
111            });
112        }
113        Err(BackupError::KeptChanging)
114    }
115}
116
117/// The object file names, one per version row, of the catalog copy at
118/// `path`.
119fn versions_in(path: &Path, key: &crate::envelope::Key) -> Result<BTreeSet<String>, BackupError> {
120    let fail = |_| BackupError::Io;
121    let conn = catalog::open(path, key, "").map_err(|_| BackupError::Io)?;
122    let mut rows = conn.prepare("SELECT version FROM version").map_err(fail)?;
123    rows.query_map([], |r| r.get::<_, [u8; 16]>(0))
124        .map_err(fail)?
125        .map(|row| row.map(|id| VersionId(id).to_string()).map_err(fail))
126        .collect()
127}
128
129fn remove_if_there(path: &Path) -> io::Result<()> {
130    match std::fs::remove_file(path) {
131        Err(e) if e.kind() != io::ErrorKind::NotFound => Err(e),
132        _ => Ok(()),
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use std::sync::atomic::{AtomicUsize, Ordering};
139    use std::sync::{Arc, Mutex};
140
141    use super::*;
142    use crate::clock::Clock;
143    use crate::fs::Fs;
144    use crate::retention::Retention;
145    use crate::store::testing::vcon;
146    use crate::store::tests::{Fixture, fixture};
147
148    const A: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
149    const B: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc47";
150
151    /// Stores A (then replaces it, so one version is hidden) and B.
152    fn stored(f: &Fixture) -> VersionId {
153        let (found, body) = vcon(A, "first");
154        let first = match f.store.create(&f.caller, &found, &body).expect("create") {
155            crate::store::Created::New(v) => v,
156            other => panic!("{other:?}"),
157        };
158        let (found, body) = vcon(A, "second");
159        f.store
160            .replace(&f.caller, first, &found, &body)
161            .expect("replace");
162        let (found, body) = vcon(B, "other");
163        f.store.create(&f.caller, &found, &body).expect("create");
164        first
165    }
166
167    /// The backup in `dir` opened as a Store, and examined.
168    fn examined(f: &Fixture, dir: &Path) -> (Store, crate::store::Examination) {
169        let store = Store::open_beside(
170            dir,
171            f.tenant(),
172            f.tenant_key(),
173            f.fs.clone(),
174            f.clock.clone(),
175        )
176        .expect("the backup opens under the tenant key");
177        let now = f.clock.now();
178        let found = store
179            .examine(Retention::Years(7), now, now)
180            .expect("examines");
181        (store, found)
182    }
183
184    fn body_of(store: &Store, f: &Fixture, uuid: &str) -> Vec<u8> {
185        store
186            .get(&f.caller, uuid.parse().expect("uuid"))
187            .expect("reads")
188            .body
189    }
190
191    #[test]
192    fn a_backup_opens_whole_with_every_version() {
193        let f = fixture();
194        stored(&f);
195        let dir = f.dir.path().join("backup");
196        let copied = f.store.backup_to(&dir).expect("backs up");
197        assert_eq!(
198            copied,
199            Copied {
200                versions: 3,
201                tries: 1
202            }
203        );
204        let (store, found) = examined(&f, &dir);
205        assert_eq!(found.versions, 3);
206        assert!(found.damaged.is_empty(), "{:?}", found.damaged);
207        assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
208        assert_eq!(body_of(&store, &f, A), body_of(&f.store, &f, A));
209        assert_eq!(body_of(&store, &f, B), body_of(&f.store, &f, B));
210    }
211
212    #[test]
213    fn a_version_purged_after_the_catalog_is_copied_makes_another_try() {
214        let f = fixture();
215        let first = stored(&f);
216        let calls = Arc::new(AtomicUsize::new(0));
217        let seen = calls.clone();
218        *f.store.hooks.copied.lock().expect("hook") = Some(Box::new(move |store: &Store| {
219            // Once: the hidden version is purged, row and file, after the
220            // catalog copy holds its row.
221            if seen.fetch_add(1, Ordering::SeqCst) == 0 {
222                let purged = store.purge_due(Retention::Days(0)).expect("purges");
223                assert_eq!(purged.purged, 1);
224            }
225        }));
226        let dir = f.dir.path().join("backup");
227        let copied = f.store.backup_to(&dir).expect("backs up");
228        assert_eq!(
229            copied,
230            Copied {
231                versions: 2,
232                tries: 2
233            }
234        );
235        let (_, found) = examined(&f, &dir);
236        assert_eq!(found.versions, 2);
237        assert!(found.damaged.is_empty(), "{:?}", found.damaged);
238        assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
239        assert!(
240            !dir.join("objects").join(first.to_string()).exists()
241                && f.fs
242                    .read(&dir.join("objects").join(first.to_string()))
243                    .is_err(),
244            "the purged version's file is not in the backup"
245        );
246    }
247
248    #[test]
249    fn a_store_whose_versions_keep_being_purged_fails_after_its_tries() {
250        let f = fixture();
251        stored(&f);
252        let calls = Arc::new(AtomicUsize::new(0));
253        let seen = calls.clone();
254        let current = Arc::new(Mutex::new(None::<VersionId>));
255        *f.store.hooks.copied.lock().expect("hook") = Some(Box::new(move |store: &Store| {
256            // Each try: B's current version is replaced and purged at
257            // once, so the copy's row for it never has its file.
258            let n = seen.fetch_add(1, Ordering::SeqCst);
259            let mut current = current.lock().expect("current");
260            let caller = crate::store::Caller {
261                key: crate::envelope::Key::generate().expect("key").id(),
262            };
263            let now = current.unwrap_or_else(|| {
264                store
265                    .get(&caller, B.parse().expect("uuid"))
266                    .expect("B")
267                    .version
268            });
269            let (found, body) = vcon(B, &format!("change {n}"));
270            *current = Some(store.replace(&caller, now, &found, &body).expect("replace"));
271            store.purge_due(Retention::Days(0)).expect("purges");
272        }));
273        let dir = f.dir.path().join("backup");
274        assert_eq!(f.store.backup_to(&dir), Err(BackupError::KeptChanging));
275        assert_eq!(calls.load(Ordering::SeqCst), TRIES);
276    }
277
278    #[test]
279    fn files_an_earlier_try_left_are_replaced_or_removed() {
280        let f = fixture();
281        stored(&f);
282        let dir = f.dir.path().join("backup");
283        let objects = dir.join("objects");
284        f.fs.create_dir_all(&objects).expect("dir");
285        let stray = VersionId::generate().to_string();
286        f.fs.write_new(&objects.join(&stray), b"from an earlier try")
287            .expect("write");
288        f.fs.write_new(&objects.join(format!("{stray}.tmp")), b"half")
289            .expect("write");
290        std::fs::write(dir.join("catalog.sqlite"), b"an earlier copy").expect("write");
291        std::fs::write(dir.join("catalog.sqlite-wal"), b"its wal").expect("write");
292        f.store.backup_to(&dir).expect("backs up");
293        let (_, found) = examined(&f, &dir);
294        assert_eq!(found.versions, 3);
295        assert!(found.damaged.is_empty(), "{:?}", found.damaged);
296        assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
297        let names = f.fs.list(&objects).expect("list");
298        assert_eq!(names.len(), 3, "{names:?}");
299    }
300
301    #[test]
302    fn a_backup_survives_a_crash_once_it_returns() {
303        let f = fixture();
304        stored(&f);
305        let dir = f.dir.path().join("backup");
306        f.store.backup_to(&dir).expect("backs up");
307        f.fs.crash();
308        let (_, found) = examined(&f, &dir);
309        assert_eq!(found.versions, 3);
310        assert!(found.damaged.is_empty(), "{:?}", found.damaged);
311    }
312
313    #[test]
314    fn a_backup_taken_while_writes_go_on_opens_whole() {
315        let f = Arc::new(fixture());
316        stored(&f);
317        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
318        let writer = {
319            let (f, stop) = (f.clone(), stop.clone());
320            std::thread::spawn(move || {
321                let mut n = 0u32;
322                while !stop.load(Ordering::SeqCst) {
323                    let uuid = format!("01a07da8-c2bb-83e5-b9a2-{n:012x}");
324                    let (found, body) = vcon(&uuid, "load");
325                    f.store.create(&f.caller, &found, &body).expect("create");
326                    n += 1;
327                }
328                n
329            })
330        };
331        let dir = f.dir.path().join("backup");
332        let copied = f.store.backup_to(&dir).expect("backs up");
333        stop.store(true, Ordering::SeqCst);
334        let written = writer.join().expect("writer");
335        assert!(written > 0, "writes went on");
336        let (_, found) = examined(&f, &dir);
337        assert_eq!(found.versions, copied.versions);
338        assert!(found.damaged.is_empty(), "{:?}", found.damaged);
339        assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
340    }
341}