1use rusqlite::{OptionalExtension, params};
5use time::OffsetDateTime;
6use uuid::Uuid;
7
8use super::{Store, StoreError, commit, immediate, io};
9use crate::audit::{Action, Actor, Entry};
10use crate::catalog;
11use crate::clock::{from_nanos, nanos};
12use crate::ids::VersionId;
13use crate::retention::Retention;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Kept {
18 pub stored_at: OffsetDateTime,
20 pub hidden: bool,
23 pub held: bool,
25}
26
27pub fn due(version: &Kept, retention: Retention, now: OffsetDateTime) -> bool {
33 version.hidden
34 && !version.held
35 && retention
36 .after(version.stored_at)
37 .is_some_and(|end| now >= end)
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42pub struct PurgeReport {
43 pub purged: u64,
45}
46
47const CHECKPOINT_RETRY: std::time::Duration = std::time::Duration::from_millis(10);
50
51impl Store {
52 pub fn purge_due(&self, retention: Retention) -> Result<PurgeReport, StoreError> {
62 self.purge_hidden(Actor::Sweep, retention, None)
63 }
64
65 pub(super) fn purge_hidden(
68 &self,
69 who: Actor,
70 retention: Retention,
71 uuid: Option<Uuid>,
72 ) -> Result<PurgeReport, StoreError> {
73 let hidden: Vec<[u8; 16]> = {
74 let conn = self.conn();
75 let mut rows = conn
76 .prepare(
77 "SELECT version FROM version
78 WHERE hidden_at IS NOT NULL AND (?1 IS NULL OR uuid = ?1)",
79 )
80 .map_err(io)?;
81 rows.query_map([uuid.map(|u| *u.as_bytes())], |r| r.get(0))
82 .map_err(io)?
83 .collect::<Result<_, _>>()
84 .map_err(io)?
85 };
86 self.pause();
89 let mut report = PurgeReport::default();
90 for version in hidden {
91 if self.purge(who, VersionId(version), retention)? {
92 report.purged += 1;
93 }
94 }
95 if report.purged > 0 {
96 self.checkpoint()?;
97 }
98 Ok(report)
99 }
100
101 fn purge(
103 &self,
104 who: Actor,
105 version: VersionId,
106 retention: Retention,
107 ) -> Result<bool, StoreError> {
108 let mut conn = self.conn();
109 let tx = immediate(&mut conn)?;
110 let row: Option<([u8; 16], i64, bool, bool)> = tx
111 .query_row(
112 "SELECT v.uuid, v.stored_at, c.hold != 0, c.deleted_at IS NOT NULL
113 FROM version v JOIN vcon c ON c.uuid = v.uuid
114 WHERE v.version = ?1 AND v.hidden_at IS NOT NULL",
115 [version.0],
116 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
117 )
118 .optional()
119 .map_err(io)?;
120 let Some((uuid, stored_at, held, deleted)) = row else {
121 return Ok(false);
122 };
123 let now = self.clock.now();
124 let kept = Kept {
125 stored_at: from_nanos(stored_at).ok_or(StoreError::Damaged)?,
126 hidden: true,
127 held,
128 };
129 if !due(&kept, retention, now) {
130 return Ok(false);
131 }
132 tx.execute("DELETE FROM version WHERE version = ?1", [version.0])
133 .map_err(io)?;
134 let left: i64 = tx
135 .query_row(
136 "SELECT count(*) FROM version WHERE uuid = ?1",
137 [uuid],
138 |r| r.get(0),
139 )
140 .map_err(io)?;
141 if deleted && left == 0 {
142 tx.execute("DELETE FROM vcon WHERE uuid = ?1", [uuid])
143 .map_err(io)?;
144 tx.execute(
145 "INSERT INTO tombstone (uuid, purged_at) VALUES (?1, ?2)",
146 params![uuid, nanos(now)],
147 )
148 .map_err(io)?;
149 }
150 let entry = Entry {
151 actor: who,
152 action: Action::Purge,
153 target: Some(Uuid::from_bytes(uuid).to_string()),
154 version: Some(version),
155 status: 200,
156 };
157 self.record(&tx, &entry)?;
158 commit(tx)?;
159 drop(conn);
160 self.fs
161 .remove(&self.objects.join(version.to_string()))
162 .map_err(io)?;
163 Ok(true)
164 }
165
166 fn checkpoint(&self) -> Result<(), StoreError> {
171 loop {
172 let busy: i64 = {
173 let conn = self.conn();
174 conn.busy_timeout(std::time::Duration::ZERO).map_err(io)?;
175 let busy = conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |r| r.get(0));
176 conn.busy_timeout(catalog::BUSY_TIMEOUT).map_err(io)?;
177 busy.map_err(io)?
178 };
179 if busy == 0 {
180 return Ok(());
181 }
182 std::thread::sleep(CHECKPOINT_RETRY);
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use time::Duration;
190 use time::macros::datetime;
191
192 use super::*;
193
194 const SECOND: Duration = Duration::seconds(1);
195
196 fn hidden(stored_at: OffsetDateTime) -> Kept {
197 Kept {
198 stored_at,
199 hidden: true,
200 held: false,
201 }
202 }
203
204 #[test]
205 fn a_hidden_version_is_due_from_the_second_its_period_ends() {
206 let seven = Retention::Years(7);
207 let cases = [
208 (
210 datetime!(2026-03-15 10:00 UTC),
211 datetime!(2033-03-15 10:00 UTC),
212 ),
213 (
216 datetime!(2028-02-29 12:00 UTC),
217 datetime!(2035-02-28 12:00 UTC),
218 ),
219 ];
220 for (stored_at, end) in cases {
221 let version = hidden(stored_at);
222 assert!(
223 !due(&version, seven, end - SECOND),
224 "{stored_at}: a second short"
225 );
226 assert!(due(&version, seven, end), "{stored_at}: at the end");
227 assert!(
228 due(&version, seven, end + SECOND),
229 "{stored_at}: a second past"
230 );
231 }
232 }
233
234 #[test]
235 fn a_hold_keeps_a_version_past_its_end() {
236 let stored_at = datetime!(2028-02-29 12:00 UTC);
237 let end = datetime!(2035-02-28 12:00 UTC);
238 let version = Kept {
239 held: true,
240 ..hidden(stored_at)
241 };
242 for now in [end - SECOND, end, end + SECOND, end + Duration::days(3650)] {
243 assert!(!due(&version, Retention::Years(7), now), "{now}");
244 }
245 }
246
247 #[test]
248 fn a_current_version_is_never_due() {
249 let version = Kept {
250 hidden: false,
251 ..hidden(datetime!(2026-03-15 10:00 UTC))
252 };
253 let end = datetime!(2033-03-15 10:00 UTC);
254 for now in [end - SECOND, end, end + SECOND] {
255 assert!(!due(&version, Retention::Years(7), now), "{now}");
256 }
257 }
258
259 #[test]
260 fn a_zero_period_makes_a_hidden_version_due_at_once() {
261 let stored_at = datetime!(2026-03-15 10:00 UTC);
262 let version = hidden(stored_at);
263 assert!(due(&version, Retention::Days(0), stored_at));
264 assert!(!due(&version, Retention::Days(0), stored_at - SECOND));
265 }
266
267 #[test]
268 fn a_period_ending_past_the_last_time_held_is_never_due() {
269 let version = hidden(datetime!(9998-06-01 00:00 UTC));
270 let last = datetime!(9999-12-31 23:59:59 UTC);
271 assert!(!due(&version, Retention::Years(7), last));
272 }
273}
274
275#[cfg(test)]
276mod purge_tests {
277 use std::path::Path;
278 use std::sync::atomic::{AtomicBool, Ordering};
279 use std::sync::{Arc, mpsc};
280 use std::time::Duration as Wait;
281
282 use aws_lc_rs::cipher::{AES_256, DecryptingKey, DecryptionContext, UnboundCipherKey};
283 use aws_lc_rs::iv::FixedLength;
284 use serde_json::Value;
285 use time::Duration;
286 use uuid::Uuid;
287
288 use super::super::testing::vcon;
289 use super::super::tests::{Fixture, fixture};
290 use super::*;
291 use crate::catalog;
292 use crate::clock::Clock;
293 use crate::envelope::{self, Key};
294 use crate::fs::Fs;
295 use crate::ids::VersionId;
296 use crate::store::{Created, StoreError};
297
298 const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
299 const DAY: Retention = Retention::Days(1);
300
301 fn stored(f: &Fixture, subject: &str) -> (Uuid, VersionId) {
303 let (found, body) = vcon(UUID, subject);
304 let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
305 panic!("a new vCon")
306 };
307 (found.uuid, version)
308 }
309
310 fn later(f: &Fixture, by: Duration) {
311 f.clock.set(f.clock.now() + by);
312 }
313
314 fn count(f: &Fixture, sql: &str, uuid: Uuid) -> i64 {
315 f.store
316 .conn()
317 .query_row(sql, [uuid.as_bytes()], |r| r.get(0))
318 .expect(sql)
319 }
320
321 fn wrapped_key(f: &Fixture, version: VersionId) -> Vec<u8> {
322 f.store
323 .conn()
324 .query_row(
325 "SELECT wrapped_key FROM version WHERE version = ?1",
326 [version.0],
327 |r| r.get(0),
328 )
329 .expect("row")
330 }
331
332 fn object_exists(f: &Fixture, version: VersionId) -> bool {
333 f.fs.read(&f.store.objects.join(version.to_string()))
334 .is_ok()
335 }
336
337 fn purges(f: &Fixture) -> Vec<Value> {
338 f.store
339 .records()
340 .into_iter()
341 .filter(|r| r["action"] == "purge")
342 .collect()
343 }
344
345 #[test]
346 fn a_purge_erases_a_deleted_vcon_and_leaves_a_tombstone() {
347 let f = fixture();
348 let (uuid, version) = stored(&f, "first");
349 f.store
350 .delete(&f.caller, uuid, Retention::Years(7))
351 .expect("deletes");
352 later(&f, Duration::days(2));
353
354 assert_eq!(f.store.purge_due(DAY), Ok(PurgeReport { purged: 1 }));
355
356 assert_eq!(
357 count(&f, "SELECT count(*) FROM version WHERE uuid = ?1", uuid),
358 0
359 );
360 assert_eq!(
361 count(&f, "SELECT count(*) FROM vcon WHERE uuid = ?1", uuid),
362 0
363 );
364 assert_eq!(
365 count(&f, "SELECT count(*) FROM tombstone WHERE uuid = ?1", uuid),
366 1
367 );
368 let purged_at: i64 = f
369 .store
370 .conn()
371 .query_row(
372 "SELECT purged_at FROM tombstone WHERE uuid = ?1",
373 [uuid.as_bytes()],
374 |r| r.get(0),
375 )
376 .expect("tombstone");
377 assert_eq!(
378 purged_at,
379 nanos(f.clock.now()),
380 "the tombstone holds the purge time"
381 );
382 assert!(!object_exists(&f, version), "the object file is removed");
383 let records = purges(&f);
384 assert_eq!(records.len(), 1);
385 assert_eq!(records[0]["actor"], "sweep");
386 assert_eq!(records[0]["target"], uuid.to_string());
387 assert_eq!(records[0]["version"], version.to_string());
388 assert_eq!(f.store.get(&f.caller, uuid), Err(StoreError::Gone));
389 let (found, body) = vcon(UUID, "again");
390 assert_eq!(
391 f.store.create(&f.caller, &found, &body),
392 Err(StoreError::Gone),
393 "a purged uuid cannot be stored again"
394 );
395 }
396
397 #[test]
398 fn a_purge_of_a_replaced_version_keeps_the_live_vcon() {
399 let f = fixture();
400 let (uuid, old) = stored(&f, "first");
401 let (found, body) = vcon(UUID, "second");
402 let new = f
403 .store
404 .replace(&f.caller, old, &found, &body)
405 .expect("replaces");
406 later(&f, Duration::days(2));
407
408 assert_eq!(f.store.purge_due(DAY), Ok(PurgeReport { purged: 1 }));
409
410 assert_eq!(
411 count(&f, "SELECT count(*) FROM version WHERE uuid = ?1", uuid),
412 1
413 );
414 assert_eq!(
415 count(&f, "SELECT count(*) FROM tombstone WHERE uuid = ?1", uuid),
416 0
417 );
418 assert!(!object_exists(&f, old));
419 assert!(object_exists(&f, new));
420 assert_eq!(f.store.get(&f.caller, uuid).expect("reads").body, body);
421 }
422
423 #[test]
424 fn versions_not_due_are_kept() {
425 let f = fixture();
426 let (current, _) = stored(&f, "current");
428 let (found, body) = vcon("0199e5c6-0d7a-7c11-8000-000000000001", "held");
430 f.store.create(&f.caller, &found, &body).expect("stores");
431 f.store
432 .delete(&f.caller, found.uuid, Retention::Years(7))
433 .expect("deletes");
434 f.store
435 .conn()
436 .execute(
437 "UPDATE vcon SET hold = 1 WHERE uuid = ?1",
438 [found.uuid.as_bytes()],
439 )
440 .expect("holds");
441 later(&f, Duration::days(2));
442 let (young, body) = vcon("0199e5c6-0d7a-7c11-8000-000000000002", "young");
444 f.store.create(&f.caller, &young, &body).expect("stores");
445 f.store
446 .delete(&f.caller, young.uuid, Retention::Years(7))
447 .expect("deletes");
448
449 assert_eq!(f.store.purge_due(DAY), Ok(PurgeReport { purged: 0 }));
450
451 for uuid in [current, found.uuid, young.uuid] {
452 assert_eq!(
453 count(&f, "SELECT count(*) FROM version WHERE uuid = ?1", uuid),
454 1
455 );
456 }
457 assert!(purges(&f).is_empty());
458 }
459
460 #[test]
461 fn deleting_an_old_vcon_erases_it_at_once() {
462 let f = fixture();
463 let (uuid, version) = stored(&f, "first");
464 later(&f, Duration::days(2));
465
466 assert_eq!(f.store.delete(&f.caller, uuid, DAY), Ok(()));
467
468 assert_eq!(
469 count(&f, "SELECT count(*) FROM version WHERE uuid = ?1", uuid),
470 0
471 );
472 assert_eq!(
473 count(&f, "SELECT count(*) FROM tombstone WHERE uuid = ?1", uuid),
474 1
475 );
476 assert!(!object_exists(&f, version));
477 let records = purges(&f);
478 assert_eq!(records.len(), 1);
479 assert_eq!(
480 records[0]["actor"],
481 f.caller.key.to_string(),
482 "the delete that caused the purge is its actor"
483 );
484 }
485
486 #[test]
487 fn deleting_a_young_vcon_keeps_it_hidden() {
488 let f = fixture();
489 let (uuid, version) = stored(&f, "first");
490 later(&f, Duration::hours(23));
491
492 assert_eq!(f.store.delete(&f.caller, uuid, DAY), Ok(()));
493
494 assert_eq!(
495 count(
496 &f,
497 "SELECT count(*) FROM version WHERE uuid = ?1 AND hidden_at IS NOT NULL",
498 uuid
499 ),
500 1
501 );
502 assert!(object_exists(&f, version));
503 assert!(purges(&f).is_empty());
504 }
505
506 #[test]
507 fn a_zero_period_erases_inside_the_delete() {
508 let f = fixture();
509 let (uuid, version) = stored(&f, "first");
510
511 assert_eq!(f.store.delete(&f.caller, uuid, Retention::Days(0)), Ok(()));
512
513 assert_eq!(
514 count(&f, "SELECT count(*) FROM tombstone WHERE uuid = ?1", uuid),
515 1
516 );
517 assert!(!object_exists(&f, version));
518 }
519
520 #[test]
521 fn a_delete_purges_only_its_own_uuid() {
522 let f = fixture();
523 let (uuid, _) = stored(&f, "first");
524 let (other, body) = vcon("0199e5c6-0d7a-7c11-8000-000000000003", "other");
525 f.store.create(&f.caller, &other, &body).expect("stores");
526 f.store
527 .delete(&f.caller, other.uuid, Retention::Years(7))
528 .expect("deletes");
529 later(&f, Duration::days(2));
530
531 f.store.delete(&f.caller, uuid, DAY).expect("deletes");
532
533 assert_eq!(
534 count(
535 &f,
536 "SELECT count(*) FROM version WHERE uuid = ?1",
537 other.uuid
538 ),
539 1,
540 "the other uuid waits for the sweep"
541 );
542 }
543
544 #[test]
545 fn a_delete_whose_purge_fails_stands_and_answers_the_failure() {
546 let f = fixture();
547 let (uuid, version) = stored(&f, "first");
548 later(&f, Duration::days(2));
549 f.store
550 .conn()
551 .execute_batch(
552 "CREATE TEMP TRIGGER no_purge BEFORE DELETE ON version
553 BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;",
554 )
555 .expect("trigger");
556
557 assert!(f.store.delete(&f.caller, uuid, DAY).is_err());
558
559 assert_eq!(f.store.get(&f.caller, uuid), Err(StoreError::Gone));
560 assert_eq!(
561 count(
562 &f,
563 "SELECT count(*) FROM version WHERE uuid = ?1 AND hidden_at IS NOT NULL",
564 uuid
565 ),
566 1,
567 "kept hidden for the sweep to purge"
568 );
569 assert!(object_exists(&f, version));
570 }
571
572 #[test]
573 fn deleting_again_erases_what_has_since_come_due() {
574 let f = fixture();
575 let (uuid, version) = stored(&f, "first");
576 f.store
577 .delete(&f.caller, uuid, Retention::Years(7))
578 .expect("deletes");
579 later(&f, Duration::days(2));
580
581 assert_eq!(f.store.delete(&f.caller, uuid, DAY), Ok(()));
582
583 assert!(!object_exists(&f, version));
584 }
585
586 fn due_for_purge(f: &Fixture) -> (Uuid, VersionId, Vec<u8>) {
589 let (found, body) = vcon(UUID, "first");
590 let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
591 panic!("a new vCon")
592 };
593 f.store
594 .delete(&f.caller, found.uuid, Retention::Years(7))
595 .expect("deleted");
596 later(f, Duration::days(2));
597 (found.uuid, version, body)
598 }
599
600 #[test]
601 fn a_purge_cut_off_before_its_commit_leaves_the_version_whole() {
602 let mut f = fixture();
603 let (uuid, version, body) = due_for_purge(&f);
604 let other = super::super::tests::block_commits(&f);
605 other
606 .execute_batch(
607 "CREATE TRIGGER dead BEFORE INSERT ON audit
608 BEGIN SELECT RAISE(ABORT, 'the process has died'); END;",
609 )
610 .expect("trigger");
611
612 assert!(f.store.purge_due(DAY).is_err());
613 drop(other);
614 super::super::tests::restart(&mut f);
615
616 let read = f.store.legal_get(&f.caller, uuid, None).expect("whole");
617 assert_eq!((read.stored.version, read.stored.body), (version, body));
618 assert!(object_exists(&f, version));
619 assert_eq!(f.store.purge_due(DAY), Ok(PurgeReport { purged: 1 }));
620 assert!(!object_exists(&f, version));
621 }
622
623 #[test]
624 fn a_purge_cut_off_at_its_file_step_is_finished_by_recovery() {
625 for step in 0..=1 {
628 let mut f = fixture();
629 let (uuid, version, _) = due_for_purge(&f);
630 f.fs.fail_from(f.fs.log().len() + step);
631 let _ = f.store.purge_due(DAY);
632 super::super::tests::restart(&mut f);
633
634 assert_eq!(
635 count(&f, "SELECT count(*) FROM version WHERE uuid = ?1", uuid),
636 0,
637 "step {step}: the commit stands"
638 );
639 assert_eq!(
640 count(&f, "SELECT count(*) FROM tombstone WHERE uuid = ?1", uuid),
641 1,
642 "step {step}"
643 );
644 assert!(
645 !object_exists(&f, version),
646 "step {step}: recovery removes the file"
647 );
648 assert_eq!(
649 f.store.legal_get(&f.caller, uuid, None),
650 Err(StoreError::Gone),
651 "step {step}"
652 );
653 }
654 }
655
656 fn between_listing_and_purge(f: &Arc<Fixture>, legal: impl FnOnce(&Fixture) + Send + 'static) {
659 let racer = f.clone();
660 *f.store.hooks.between.lock().expect("hook") = Some(Box::new(move || legal(&racer)));
661 }
662
663 #[test]
664 fn a_restore_that_commits_before_the_purge_keeps_the_vcon_live() {
665 let f = Arc::new(fixture());
666 let (uuid, version, body) = due_for_purge(&f);
667 between_listing_and_purge(&f, move |racer| {
668 racer.store.restore(&racer.caller, uuid).expect("restored");
669 });
670
671 assert_eq!(f.store.purge_due(DAY), Ok(PurgeReport { purged: 0 }));
672
673 let read = f.store.get(&f.caller, uuid).expect("live");
674 assert_eq!((read.version, read.body), (version, body));
675 }
676
677 #[test]
678 fn a_hold_that_commits_before_the_purge_keeps_the_version() {
679 let f = Arc::new(fixture());
680 let (uuid, version, body) = due_for_purge(&f);
681 between_listing_and_purge(&f, move |racer| {
682 racer
683 .store
684 .set_hold(&racer.caller, uuid, true)
685 .expect("held");
686 });
687
688 assert_eq!(f.store.purge_due(DAY), Ok(PurgeReport { purged: 0 }));
689
690 let read = f.store.legal_get(&f.caller, uuid, None).expect("kept");
691 assert_eq!((read.stored.version, read.stored.body), (version, body));
692 }
693
694 #[test]
695 fn a_legal_read_racing_a_purge_gets_the_whole_body_or_gone() {
696 let f = Arc::new(fixture());
697 let (uuid, _, body) = due_for_purge(&f);
698 let (seen_tx, seen) = mpsc::channel();
699 between_listing_and_purge(&f, move |racer| {
700 seen_tx
701 .send(racer.store.legal_get(&racer.caller, uuid, None))
702 .expect("send");
703 });
704
705 assert_eq!(f.store.purge_due(DAY), Ok(PurgeReport { purged: 1 }));
706
707 let before = seen
708 .recv_timeout(Wait::from_secs(5))
709 .expect("the racing read ran");
710 assert_eq!(before.expect("before the purge").stored.body, body);
711 assert_eq!(
712 f.store.legal_get(&f.caller, uuid, None),
713 Err(StoreError::Gone),
714 "after it"
715 );
716 }
717
718 fn plain_pages(path: &Path, key: &Key, page_size: usize) -> Vec<Vec<u8>> {
724 const RESERVED: usize = 80;
725 const IV: usize = 16;
726 const SALT: usize = 16;
727 let raw = key.sqlcipher_raw();
728 let hex = &raw[2..66];
729 let key: Vec<u8> = (0..32)
730 .map(|i| u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).expect("hex"))
731 .collect();
732 let decrypt = |number: u32, page: &[u8]| {
733 let start = if number == 1 { SALT } else { 0 };
734 let end = page_size - RESERVED;
735 let iv: [u8; IV] = page[end..end + IV].try_into().expect("an IV");
736 let mut text = page[start..end].to_vec();
737 let cipher = DecryptingKey::cbc(UnboundCipherKey::new(&AES_256, &key).expect("key"))
738 .expect("CBC");
739 cipher
740 .decrypt(&mut text, DecryptionContext::Iv128(FixedLength::from(iv)))
741 .expect("decrypts");
742 text
743 };
744 let mut pages = Vec::new();
745 let db = std::fs::read(path).expect("catalog");
746 for (i, page) in db.chunks_exact(page_size).enumerate() {
747 pages.push(decrypt(u32::try_from(i + 1).expect("page number"), page));
748 }
749 let mut wal_path = path.as_os_str().to_owned();
750 wal_path.push("-wal");
751 let wal = std::fs::read(&wal_path).unwrap_or_default();
752 if let Some(frames) = wal.get(32..) {
753 for frame in frames.chunks_exact(24 + page_size) {
754 let number = u32::from_be_bytes(frame[..4].try_into().expect("4 bytes"));
755 pages.push(decrypt(number, &frame[24..]));
756 }
757 }
758 pages
759 }
760
761 fn holds(pages: &[Vec<u8>], needle: &[u8]) -> bool {
762 pages
763 .iter()
764 .any(|page| page.windows(needle.len()).any(|w| w == needle))
765 }
766
767 #[test]
768 fn no_copy_of_a_purged_wrapped_key_is_left_in_the_catalog() {
769 let f = fixture();
770 let (uuid, version) = stored(&f, "first");
771 let wrapped = wrapped_key(&f, version);
772 f.store
773 .delete(&f.caller, uuid, Retention::Years(7))
774 .expect("deletes");
775 let path = f.dir.path().join("catalog.sqlite");
776 let key = envelope::catalog_key(&f.store.tenant_key).expect("catalog key");
777 let page_size = crate::catalog::tests::cipher_setting(&f.store.conn(), "cipher_page_size");
778 let page_size: usize = page_size.parse().expect("a page size");
779 assert!(holds(&plain_pages(&path, &key, page_size), &wrapped));
782 later(&f, Duration::days(2));
783
784 assert_eq!(f.store.purge_due(DAY), Ok(PurgeReport { purged: 1 }));
785
786 assert!(!holds(&plain_pages(&path, &key, page_size), &wrapped));
787 let mut wal = path.as_os_str().to_owned();
788 wal.push("-wal");
789 assert_eq!(
790 std::fs::metadata(&wal).expect("WAL").len(),
791 0,
792 "WAL truncated"
793 );
794 }
795
796 #[test]
797 fn the_checkpoint_waits_for_a_reader_without_holding_the_catalog() {
798 let f = fixture();
799 let (uuid, _) = stored(&f, "first");
800 f.store
801 .delete(&f.caller, uuid, Retention::Years(7))
802 .expect("deletes");
803 later(&f, Duration::days(2));
804 let path = f.dir.path().join("catalog.sqlite");
805 let key = envelope::catalog_key(&f.store.tenant_key).expect("catalog key");
806 let mut reader = catalog::open(&path, &key, "").expect("a second connection");
807 let released = Arc::new(AtomicBool::new(false));
808 let (reading, started) = mpsc::channel();
809 let held = {
810 let released = released.clone();
811 std::thread::spawn(move || {
812 let tx = reader.transaction().expect("begin");
813 let _: i64 = tx
814 .query_row("SELECT count(*) FROM version", [], |r| r.get(0))
815 .expect("reads");
816 reading.send(()).expect("signal");
817 std::thread::sleep(Wait::from_millis(500));
818 released.store(true, Ordering::SeqCst);
819 drop(tx);
820 })
821 };
822 started.recv().expect("the reader started");
823
824 std::thread::scope(|s| {
825 let purging = s.spawn(|| f.store.purge_due(DAY));
826 std::thread::sleep(Wait::from_millis(100));
828 f.store.counts().expect("the catalog answers");
829 assert!(
830 !released.load(Ordering::SeqCst),
831 "another call had to wait for the reader"
832 );
833 assert_eq!(
834 purging.join().expect("the purge finished"),
835 Ok(PurgeReport { purged: 1 })
836 );
837 });
838
839 held.join().expect("the reader finished");
840 let timeout: i64 = f
841 .store
842 .conn()
843 .pragma_query_value(None, "busy_timeout", |r| r.get(0))
844 .expect("busy timeout");
845 assert_eq!(timeout, 5000, "the catalog's busy timeout is restored");
846 let mut wal = path.as_os_str().to_owned();
847 wal.push("-wal");
848 assert_eq!(
849 std::fs::metadata(&wal).expect("WAL").len(),
850 0,
851 "WAL truncated"
852 );
853 }
854}