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