1use std::collections::BTreeSet;
6use std::io;
7use std::path::Path;
8
9use super::Store;
10use crate::backend::ObjectError;
11use crate::ids::VersionId;
12
13pub const TRIES: usize = 5;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Copied {
20 pub versions: u64,
22 pub tries: usize,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum BackupError {
29 KeptChanging,
32 Io,
35}
36
37impl Store {
38 pub fn backup_to(&self, dir: &Path) -> Result<Copied, BackupError> {
50 let fail = |_| BackupError::Io;
51 let objects = dir.join("objects");
52 self.fs.create_dir_all(&objects).map_err(fail)?;
53 let catalog = dir.join("catalog.sqlite");
54 for tries in 1..=TRIES {
55 for file in ["catalog.sqlite", "catalog.sqlite-wal", "catalog.sqlite-shm"] {
56 remove_if_there(&dir.join(file)).map_err(fail)?;
57 }
58 let versions: BTreeSet<String> = self
59 .catalog
60 .backup_into(&catalog)
61 .map_err(|_| BackupError::Io)?
62 .into_iter()
63 .map(|v| v.to_string())
64 .collect();
65 #[cfg(test)]
66 if let Some(hook) = self.hooks.copied.lock().expect("hook").as_mut() {
67 hook(self);
68 }
69 let present: BTreeSet<String> = self
70 .fs
71 .list(&objects)
72 .map_err(fail)?
73 .into_iter()
74 .filter_map(|n| n.into_string().ok())
75 .collect();
76 let mut missing = false;
77 for name in &versions {
78 if present.contains(name) {
79 continue;
80 }
81 let Some(version) = VersionId::parse(name) else {
82 return Err(BackupError::Io);
83 };
84 match self.objects.get(version) {
85 Ok(bytes) => {
86 let tmp = objects.join(format!("{name}.tmp"));
87 if present.contains(&format!("{name}.tmp")) {
88 self.fs.remove(&tmp).map_err(fail)?;
89 }
90 self.fs.write_new(&tmp, &bytes).map_err(fail)?;
91 self.fs.sync_file(&tmp).map_err(fail)?;
92 self.fs.rename(&tmp, &objects.join(name)).map_err(fail)?;
93 }
94 Err(ObjectError::Missing) => missing = true,
95 Err(ObjectError::Io(e)) => return Err(fail(e)),
96 }
97 }
98 if missing {
99 continue;
100 }
101 for name in self.fs.list(&objects).map_err(fail)? {
102 let name = name.into_string().unwrap_or_default();
103 if !versions.contains(&name) {
104 self.fs.remove(&objects.join(&name)).map_err(fail)?;
105 }
106 }
107 self.fs.sync_dir(&objects).map_err(fail)?;
108 self.fs.sync_dir(dir).map_err(fail)?;
109 if let Some(parent) = dir.parent() {
112 self.fs.sync_dir(parent).map_err(fail)?;
113 }
114 return Ok(Copied {
115 versions: versions.len() as u64,
116 tries,
117 });
118 }
119 Err(BackupError::KeptChanging)
120 }
121}
122
123fn remove_if_there(path: &Path) -> io::Result<()> {
124 match std::fs::remove_file(path) {
125 Err(e) if e.kind() != io::ErrorKind::NotFound => Err(e),
126 _ => Ok(()),
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use std::sync::atomic::{AtomicUsize, Ordering};
133 use std::sync::{Arc, Mutex};
134
135 use super::*;
136 use crate::clock::Clock;
137 use crate::fs::Fs;
138 use crate::retention::Retention;
139 use crate::store::testing::vcon;
140 use crate::store::tests::{Fixture, fixture};
141
142 const A: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
143 const B: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc47";
144
145 fn stored(f: &Fixture) -> VersionId {
147 let (found, body) = vcon(A, "first");
148 let first = match f.store.create(&f.caller, &found, &body).expect("create") {
149 crate::store::Created::New(v) => v,
150 other => panic!("{other:?}"),
151 };
152 let (found, body) = vcon(A, "second");
153 f.store
154 .replace(&f.caller, first, &found, &body)
155 .expect("replace");
156 let (found, body) = vcon(B, "other");
157 f.store.create(&f.caller, &found, &body).expect("create");
158 first
159 }
160
161 fn examined(f: &Fixture, dir: &Path) -> (Store, crate::store::Examination) {
163 let store = Store::open_beside(
164 dir,
165 f.tenant(),
166 f.tenant_key(),
167 f.fs.clone(),
168 f.clock.clone(),
169 )
170 .expect("the backup opens under the tenant key");
171 let now = f.clock.now();
172 let found = store
173 .examine(Retention::Years(7), now, now)
174 .expect("examines");
175 (store, found)
176 }
177
178 fn body_of(store: &Store, f: &Fixture, uuid: &str) -> Vec<u8> {
179 store
180 .get(&f.caller, uuid.parse().expect("uuid"))
181 .expect("reads")
182 .body
183 }
184
185 #[test]
186 fn a_backup_opens_whole_with_every_version() {
187 let f = fixture();
188 stored(&f);
189 let dir = f.dir.path().join("backup");
190 let copied = f.store.backup_to(&dir).expect("backs up");
191 assert_eq!(
192 copied,
193 Copied {
194 versions: 3,
195 tries: 1
196 }
197 );
198 let (store, found) = examined(&f, &dir);
199 assert_eq!(found.versions, 3);
200 assert!(found.damaged.is_empty(), "{:?}", found.damaged);
201 assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
202 assert_eq!(body_of(&store, &f, A), body_of(&f.store, &f, A));
203 assert_eq!(body_of(&store, &f, B), body_of(&f.store, &f, B));
204 }
205
206 #[test]
207 fn a_version_purged_after_the_catalog_is_copied_makes_another_try() {
208 let f = fixture();
209 let first = stored(&f);
210 let calls = Arc::new(AtomicUsize::new(0));
211 let seen = calls.clone();
212 *f.store.hooks.copied.lock().expect("hook") = Some(Box::new(move |store: &Store| {
213 if seen.fetch_add(1, Ordering::SeqCst) == 0 {
216 let purged = store.purge_due(Retention::Days(0)).expect("purges");
217 assert_eq!(purged.purged, 1);
218 }
219 }));
220 let dir = f.dir.path().join("backup");
221 let copied = f.store.backup_to(&dir).expect("backs up");
222 assert_eq!(
223 copied,
224 Copied {
225 versions: 2,
226 tries: 2
227 }
228 );
229 let (_, found) = examined(&f, &dir);
230 assert_eq!(found.versions, 2);
231 assert!(found.damaged.is_empty(), "{:?}", found.damaged);
232 assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
233 assert!(
234 !dir.join("objects").join(first.to_string()).exists()
235 && f.fs
236 .read(&dir.join("objects").join(first.to_string()))
237 .is_err(),
238 "the purged version's file is not in the backup"
239 );
240 }
241
242 #[test]
243 fn a_store_whose_versions_keep_being_purged_fails_after_its_tries() {
244 let f = fixture();
245 stored(&f);
246 let calls = Arc::new(AtomicUsize::new(0));
247 let seen = calls.clone();
248 let current = Arc::new(Mutex::new(None::<VersionId>));
249 *f.store.hooks.copied.lock().expect("hook") = Some(Box::new(move |store: &Store| {
250 let n = seen.fetch_add(1, Ordering::SeqCst);
253 let mut current = current.lock().expect("current");
254 let caller = crate::store::Caller {
255 key: crate::envelope::Key::generate().expect("key").id(),
256 };
257 let now = current.unwrap_or_else(|| {
258 store
259 .get(&caller, B.parse().expect("uuid"))
260 .expect("B")
261 .version
262 });
263 let (found, body) = vcon(B, &format!("change {n}"));
264 *current = Some(store.replace(&caller, now, &found, &body).expect("replace"));
265 store.purge_due(Retention::Days(0)).expect("purges");
266 }));
267 let dir = f.dir.path().join("backup");
268 assert_eq!(f.store.backup_to(&dir), Err(BackupError::KeptChanging));
269 assert_eq!(calls.load(Ordering::SeqCst), TRIES);
270 }
271
272 #[test]
273 fn files_an_earlier_try_left_are_replaced_or_removed() {
274 let f = fixture();
275 stored(&f);
276 let dir = f.dir.path().join("backup");
277 let objects = dir.join("objects");
278 f.fs.create_dir_all(&objects).expect("dir");
279 let stray = VersionId::generate().to_string();
280 f.fs.write_new(&objects.join(&stray), b"from an earlier try")
281 .expect("write");
282 f.fs.write_new(&objects.join(format!("{stray}.tmp")), b"half")
283 .expect("write");
284 std::fs::write(dir.join("catalog.sqlite"), b"an earlier copy").expect("write");
285 std::fs::write(dir.join("catalog.sqlite-wal"), b"its wal").expect("write");
286 f.store.backup_to(&dir).expect("backs up");
287 let (_, found) = examined(&f, &dir);
288 assert_eq!(found.versions, 3);
289 assert!(found.damaged.is_empty(), "{:?}", found.damaged);
290 assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
291 let names = f.fs.list(&objects).expect("list");
292 assert_eq!(names.len(), 3, "{names:?}");
293 }
294
295 #[test]
296 fn a_backup_survives_a_crash_once_it_returns() {
297 let f = fixture();
298 stored(&f);
299 let dir = f.dir.path().join("backup");
300 f.store.backup_to(&dir).expect("backs up");
301 f.fs.crash();
302 let (_, found) = examined(&f, &dir);
303 assert_eq!(found.versions, 3);
304 assert!(found.damaged.is_empty(), "{:?}", found.damaged);
305 }
306
307 #[test]
308 fn a_backup_taken_while_writes_go_on_opens_whole() {
309 let f = Arc::new(fixture());
310 stored(&f);
311 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
312 let (first, written_once) = std::sync::mpsc::channel();
315 let writer = {
316 let (f, stop) = (f.clone(), stop.clone());
317 std::thread::spawn(move || {
318 let mut n = 0u32;
319 while n == 0 || !stop.load(Ordering::SeqCst) {
320 let uuid = format!("01a07da8-c2bb-83e5-b9a2-{n:012x}");
321 let (found, body) = vcon(&uuid, "load");
322 f.store.create(&f.caller, &found, &body).expect("create");
323 if n == 0 {
324 let _ = first.send(());
325 }
326 n += 1;
327 }
328 n
329 })
330 };
331 written_once
332 .recv_timeout(std::time::Duration::from_secs(20))
333 .expect("the writer stores its first vCon");
334 let dir = f.dir.path().join("backup");
335 let copied = f.store.backup_to(&dir).expect("backs up");
336 stop.store(true, Ordering::SeqCst);
337 let written = writer.join().expect("writer");
338 assert!(written > 0, "writes went on");
339 let (_, found) = examined(&f, &dir);
340 assert_eq!(
341 found.versions, copied.versions,
342 "the copy holds the versions the backup says it copied"
343 );
344 assert!(
345 found.damaged.is_empty(),
346 "every copied version opens: {:?}",
347 found.damaged
348 );
349 assert!(
350 found.leftovers.is_empty(),
351 "the copy holds no file its catalog does not name: {:?}",
352 found.leftovers
353 );
354 }
355}