1use 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
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 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 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
117fn 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 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 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 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 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}