1use std::collections::{BTreeMap, BTreeSet};
6use std::io;
7use std::path::Path;
8
9use super::Store;
10use super::packed::Unread;
11use crate::backend::{ObjectError, Packed};
12use crate::ids::VersionId;
13
14pub const TRIES: usize = 5;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct Copied {
21 pub versions: u64,
23 pub tries: usize,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum BackupError {
30 KeptChanging,
33 Io,
36}
37
38impl Store {
39 pub fn backup_to(&self, dir: &Path) -> Result<Copied, BackupError> {
51 let fail = |_| BackupError::Io;
52 let objects = dir.join("objects");
53 self.fs.create_dir_all(&objects).map_err(fail)?;
54 let catalog = dir.join("catalog.sqlite");
55 for tries in 1..=TRIES {
56 for file in ["catalog.sqlite", "catalog.sqlite-wal", "catalog.sqlite-shm"] {
57 remove_if_there(&dir.join(file)).map_err(fail)?;
58 }
59 let places: BTreeMap<String, Option<Packed>> = self
60 .catalog
61 .backup_into(&catalog)
62 .map_err(|_| BackupError::Io)?
63 .into_iter()
64 .map(|(v, place)| (v.to_string(), place))
65 .collect();
66 let versions: BTreeSet<String> = places.keys().cloned().collect();
67 #[cfg(test)]
68 if let Some(hook) = self.hooks.copied.lock().expect("hook").as_mut() {
69 hook(self);
70 }
71 let present: BTreeSet<String> = self
72 .fs
73 .list(&objects)
74 .map_err(fail)?
75 .into_iter()
76 .filter_map(|n| n.into_string().ok())
77 .collect();
78 let mut missing = false;
79 for name in &versions {
80 if present.contains(name) {
81 continue;
82 }
83 let Some(version) = VersionId::parse(name) else {
84 return Err(BackupError::Io);
85 };
86 let place = places.get(name).and_then(Option::as_ref);
89 match self.sealed_blob(version, place) {
90 Ok(bytes) => {
91 let tmp = objects.join(format!("{name}.tmp"));
92 if present.contains(&format!("{name}.tmp")) {
93 self.fs.remove(&tmp).map_err(fail)?;
94 }
95 self.fs.write_new(&tmp, &bytes).map_err(fail)?;
96 self.fs.sync_file(&tmp).map_err(fail)?;
97 self.fs.rename(&tmp, &objects.join(name)).map_err(fail)?;
98 }
99 Err(Unread::Object(ObjectError::Missing)) => missing = true,
102 Err(Unread::Object(ObjectError::Io(e))) => return Err(fail(e)),
103 Err(Unread::Damaged) => return Err(BackupError::Io),
104 }
105 }
106 if missing {
107 continue;
108 }
109 self.catalog
112 .unpack_copy(&catalog)
113 .map_err(|_| BackupError::Io)?;
114 for name in self.fs.list(&objects).map_err(fail)? {
115 let name = name.into_string().unwrap_or_default();
116 if !versions.contains(&name) {
117 self.fs.remove(&objects.join(&name)).map_err(fail)?;
118 }
119 }
120 self.fs.sync_dir(&objects).map_err(fail)?;
121 self.fs.sync_dir(dir).map_err(fail)?;
122 if let Some(parent) = dir.parent() {
125 self.fs.sync_dir(parent).map_err(fail)?;
126 }
127 return Ok(Copied {
128 versions: versions.len() as u64,
129 tries,
130 });
131 }
132 Err(BackupError::KeptChanging)
133 }
134}
135
136fn remove_if_there(path: &Path) -> io::Result<()> {
137 match std::fs::remove_file(path) {
138 Err(e) if e.kind() != io::ErrorKind::NotFound => Err(e),
139 _ => Ok(()),
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use std::sync::atomic::{AtomicUsize, Ordering};
146 use std::sync::{Arc, Mutex};
147
148 use super::*;
149 use crate::clock::Clock;
150 use crate::fs::Fs;
151 use crate::retention::Retention;
152 use crate::store::testing::vcon;
153 use crate::store::tests::{Fixture, fixture};
154
155 const A: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
156 const B: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc47";
157
158 fn stored(f: &Fixture) -> VersionId {
160 let (found, body) = vcon(A, "first");
161 let first = match f.store.create(&f.caller, &found, &body).expect("create") {
162 crate::store::Created::New(v) => v,
163 other => panic!("{other:?}"),
164 };
165 let (found, body) = vcon(A, "second");
166 f.store
167 .replace(&f.caller, first, &found, &body)
168 .expect("replace");
169 let (found, body) = vcon(B, "other");
170 f.store.create(&f.caller, &found, &body).expect("create");
171 first
172 }
173
174 fn examined(f: &Fixture, dir: &Path) -> (Store, crate::store::Examination) {
176 let store = Store::open_beside(
177 dir,
178 f.tenant(),
179 f.tenant_key(),
180 f.fs.clone(),
181 f.clock.clone(),
182 )
183 .expect("the backup opens under the tenant key");
184 let now = f.clock.now();
185 let found = store
186 .examine(Retention::Years(7), now, now)
187 .expect("examines");
188 (store, found)
189 }
190
191 fn body_of(store: &Store, f: &Fixture, uuid: &str) -> Vec<u8> {
192 store
193 .get(&f.caller, uuid.parse().expect("uuid"))
194 .expect("reads")
195 .body
196 }
197
198 #[test]
203 fn a_backup_of_packed_versions_holds_each_in_an_object_file() {
204 let mut f = fixture();
205 let hidden = stored(&f);
206 let sealed: std::collections::BTreeMap<String, Vec<u8>> = f
207 .store
208 .objects
209 .list()
210 .expect("listed")
211 .into_iter()
212 .map(|name| {
213 let v = VersionId::parse(&name).expect("a version");
214 (name, f.store.objects.get(v).expect("its object"))
215 })
216 .collect();
217 crate::store::packer::tests::with_packs(&mut f);
218 f.store
219 .pack(&crate::store::packer::PackPlan::with_limit(1 << 20))
220 .expect("packs");
221 assert!(
222 f.store.objects.list().expect("listed").is_empty(),
223 "all packed"
224 );
225 let dir = f.dir.path().join("backup");
226 let copied = f.store.backup_to(&dir).expect("backs up");
227 assert_eq!(copied.versions, 3, "every version");
228 for (name, bytes) in &sealed {
229 assert_eq!(
230 &f.fs
231 .read(&dir.join("objects").join(name))
232 .expect("an object file"),
233 bytes,
234 "the sealed bytes, byte for byte"
235 );
236 }
237 let (store, found) = examined(&f, &dir);
238 assert!(
239 found.damaged.is_empty(),
240 "nothing damaged: {:?}",
241 found.damaged
242 );
243 assert_eq!(body_of(&store, &f, A), vcon(A, "second").1, "A reads back");
244 assert_eq!(body_of(&store, &f, B), vcon(B, "other").1, "B reads back");
245 assert_eq!(
246 store
247 .legal_get(&f.caller, A.parse().expect("uuid"), Some(hidden))
248 .expect("a legal read")
249 .stored
250 .body,
251 vcon(A, "first").1,
252 "the hidden version reads back"
253 );
254 let conn = store.conn_for_test();
255 let count = |sql: &str| -> i64 { conn.query_row(sql, [], |r| r.get(0)).expect("counted") };
256 assert_eq!(
257 count("SELECT count(*) FROM version WHERE place_file IS NOT NULL"),
258 0,
259 "no place"
260 );
261 assert_eq!(
262 count("SELECT count(*) FROM parquet_file"),
263 0,
264 "no pack file"
265 );
266 }
267
268 #[test]
269 fn a_backup_opens_whole_with_every_version() {
270 let f = fixture();
271 stored(&f);
272 let dir = f.dir.path().join("backup");
273 let copied = f.store.backup_to(&dir).expect("backs up");
274 assert_eq!(
275 copied,
276 Copied {
277 versions: 3,
278 tries: 1
279 }
280 );
281 let (store, found) = examined(&f, &dir);
282 assert_eq!(found.versions, 3);
283 assert!(found.damaged.is_empty(), "{:?}", found.damaged);
284 assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
285 assert_eq!(body_of(&store, &f, A), body_of(&f.store, &f, A));
286 assert_eq!(body_of(&store, &f, B), body_of(&f.store, &f, B));
287 }
288
289 #[test]
290 fn a_version_purged_after_the_catalog_is_copied_makes_another_try() {
291 let f = fixture();
292 let first = stored(&f);
293 let calls = Arc::new(AtomicUsize::new(0));
294 let seen = calls.clone();
295 *f.store.hooks.copied.lock().expect("hook") = Some(Box::new(move |store: &Store| {
296 if seen.fetch_add(1, Ordering::SeqCst) == 0 {
299 let purged = store.purge_due(Retention::Days(0)).expect("purges");
300 assert_eq!(purged.purged, 1);
301 }
302 }));
303 let dir = f.dir.path().join("backup");
304 let copied = f.store.backup_to(&dir).expect("backs up");
305 assert_eq!(
306 copied,
307 Copied {
308 versions: 2,
309 tries: 2
310 }
311 );
312 let (_, found) = examined(&f, &dir);
313 assert_eq!(found.versions, 2);
314 assert!(found.damaged.is_empty(), "{:?}", found.damaged);
315 assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
316 assert!(
317 !dir.join("objects").join(first.to_string()).exists()
318 && f.fs
319 .read(&dir.join("objects").join(first.to_string()))
320 .is_err(),
321 "the purged version's file is not in the backup"
322 );
323 }
324
325 #[test]
326 fn a_store_whose_versions_keep_being_purged_fails_after_its_tries() {
327 let f = fixture();
328 stored(&f);
329 let calls = Arc::new(AtomicUsize::new(0));
330 let seen = calls.clone();
331 let current = Arc::new(Mutex::new(None::<VersionId>));
332 *f.store.hooks.copied.lock().expect("hook") = Some(Box::new(move |store: &Store| {
333 let n = seen.fetch_add(1, Ordering::SeqCst);
336 let mut current = current.lock().expect("current");
337 let caller = crate::store::Caller {
338 key: crate::envelope::Key::generate().expect("key").id(),
339 };
340 let now = current.unwrap_or_else(|| {
341 store
342 .get(&caller, B.parse().expect("uuid"))
343 .expect("B")
344 .version
345 });
346 let (found, body) = vcon(B, &format!("change {n}"));
347 *current = Some(store.replace(&caller, now, &found, &body).expect("replace"));
348 store.purge_due(Retention::Days(0)).expect("purges");
349 }));
350 let dir = f.dir.path().join("backup");
351 assert_eq!(f.store.backup_to(&dir), Err(BackupError::KeptChanging));
352 assert_eq!(calls.load(Ordering::SeqCst), TRIES);
353 }
354
355 #[test]
356 fn files_an_earlier_try_left_are_replaced_or_removed() {
357 let f = fixture();
358 stored(&f);
359 let dir = f.dir.path().join("backup");
360 let objects = dir.join("objects");
361 f.fs.create_dir_all(&objects).expect("dir");
362 let stray = VersionId::generate().to_string();
363 f.fs.write_new(&objects.join(&stray), b"from an earlier try")
364 .expect("write");
365 f.fs.write_new(&objects.join(format!("{stray}.tmp")), b"half")
366 .expect("write");
367 std::fs::write(dir.join("catalog.sqlite"), b"an earlier copy").expect("write");
368 std::fs::write(dir.join("catalog.sqlite-wal"), b"its wal").expect("write");
369 f.store.backup_to(&dir).expect("backs up");
370 let (_, found) = examined(&f, &dir);
371 assert_eq!(found.versions, 3);
372 assert!(found.damaged.is_empty(), "{:?}", found.damaged);
373 assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
374 let names = f.fs.list(&objects).expect("list");
375 assert_eq!(names.len(), 3, "{names:?}");
376 }
377
378 #[test]
379 fn a_backup_survives_a_crash_once_it_returns() {
380 let f = fixture();
381 stored(&f);
382 let dir = f.dir.path().join("backup");
383 f.store.backup_to(&dir).expect("backs up");
384 f.fs.crash();
385 let (_, found) = examined(&f, &dir);
386 assert_eq!(found.versions, 3);
387 assert!(found.damaged.is_empty(), "{:?}", found.damaged);
388 }
389
390 #[test]
391 fn a_backup_taken_while_writes_go_on_opens_whole() {
392 let f = Arc::new(fixture());
393 stored(&f);
394 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
395 let (first, written_once) = std::sync::mpsc::channel();
398 let writer = {
399 let (f, stop) = (f.clone(), stop.clone());
400 std::thread::spawn(move || {
401 let mut n = 0u32;
402 while n == 0 || !stop.load(Ordering::SeqCst) {
403 let uuid = format!("01a07da8-c2bb-83e5-b9a2-{n:012x}");
404 let (found, body) = vcon(&uuid, "load");
405 f.store.create(&f.caller, &found, &body).expect("create");
406 if n == 0 {
407 let _ = first.send(());
408 }
409 n += 1;
410 }
411 n
412 })
413 };
414 written_once
415 .recv_timeout(std::time::Duration::from_secs(20))
416 .expect("the writer stores its first vCon");
417 let dir = f.dir.path().join("backup");
418 let copied = f.store.backup_to(&dir).expect("backs up");
419 stop.store(true, Ordering::SeqCst);
420 let written = writer.join().expect("writer");
421 assert!(written > 0, "writes went on");
422 let (_, found) = examined(&f, &dir);
423 assert_eq!(
424 found.versions, copied.versions,
425 "the copy holds the versions the backup says it copied"
426 );
427 assert!(
428 found.damaged.is_empty(),
429 "every copied version opens: {:?}",
430 found.damaged
431 );
432 assert!(
433 found.leftovers.is_empty(),
434 "the copy holds no file its catalog does not name: {:?}",
435 found.leftovers
436 );
437 }
438}