This repository has no description
1use std::sync::atomic::{AtomicBool, Ordering};
2use std::time::{Duration, SystemTime};
3
4mod common;
5
6use common::{oid_of, repo};
7use knot_lfs::{ClaimedSize, DiskStore, LfsOid, LfsSize, LfsStore, LfsStorePath, Reclaimed};
8
9const ROUNDS: usize = 400;
10const GRACE: Duration = Duration::from_secs(14 * 86_400);
11const BACKDATE: Duration = Duration::from_secs(60 * 86_400);
12
13#[derive(Clone, Copy)]
14enum Bias {
15 TouchFirst,
16 SweepFirst,
17 Simultaneous,
18}
19
20impl Bias {
21 fn of_round(round: usize) -> Self {
22 match round % 3 {
23 0 => Self::TouchFirst,
24 1 => Self::SweepFirst,
25 _ => Self::Simultaneous,
26 }
27 }
28}
29
30fn seed_expired(store: &DiskStore, round: usize) -> LfsOid {
31 let body = format!("past-grace media, round {round}").into_bytes();
32 let oid = oid_of(&body);
33 store
34 .put(
35 &repo(),
36 &oid,
37 ClaimedSize::new(body.len() as u64),
38 &mut &body[..],
39 )
40 .unwrap();
41 let path = store.object_file(&repo(), &oid).unwrap().unwrap().1;
42 std::fs::OpenOptions::new()
43 .write(true)
44 .open(path)
45 .unwrap()
46 .set_modified(SystemTime::now() - BACKDATE)
47 .unwrap();
48 oid
49}
50
51#[test]
52fn a_mention_concurrent_with_the_sweep_never_yields_a_dangling_pointer() {
53 let dir = tempfile::tempdir().unwrap();
54 let store = DiskStore::open(LfsStorePath::new(dir.path())).unwrap();
55
56 let outcomes: Vec<(Option<LfsSize>, Reclaimed)> = (0..ROUNDS)
57 .map(|round| {
58 let oid = seed_expired(&store, round);
59 let bias = Bias::of_round(round);
60 let go = AtomicBool::new(false);
61 let touch_started = AtomicBool::new(false);
62 let sweep_started = AtomicBool::new(false);
63 let (vouched, reclaimed) = std::thread::scope(|scope| {
64 let toucher = scope.spawn(|| {
65 while !go.load(Ordering::Acquire) {
66 std::hint::spin_loop();
67 }
68 if matches!(bias, Bias::SweepFirst) {
69 while !sweep_started.load(Ordering::Acquire) {
70 std::hint::spin_loop();
71 }
72 }
73 touch_started.store(true, Ordering::Release);
74 store.touch(&repo(), &oid).unwrap()
75 });
76 let sweeper = scope.spawn(|| {
77 while !go.load(Ordering::Acquire) {
78 std::hint::spin_loop();
79 }
80 if matches!(bias, Bias::TouchFirst) {
81 while !touch_started.load(Ordering::Acquire) {
82 std::hint::spin_loop();
83 }
84 }
85 sweep_started.store(true, Ordering::Release);
86 store
87 .collect_expired(&repo(), &oid, GRACE, SystemTime::now())
88 .unwrap()
89 });
90 go.store(true, Ordering::Release);
91 (toucher.join().unwrap(), sweeper.join().unwrap())
92 });
93
94 if let Some(size) = vouched {
95 assert!(
96 matches!(reclaimed, Reclaimed::Spared),
97 "round {round}: the sweep deleted an object the server just reported stored"
98 );
99 assert_eq!(
100 store.probe(&repo(), &oid).unwrap(),
101 Some(size),
102 "round {round}: an object reported stored must remain readable"
103 );
104 } else {
105 assert!(
106 matches!(reclaimed, Reclaimed::Swept(_)),
107 "round {round}: a touch that reports missing means the sweeper unlinked first"
108 );
109 assert_eq!(
110 store.probe(&repo(), &oid).unwrap(),
111 None,
112 "round {round}: a swept object reports missing"
113 );
114 }
115 (vouched, reclaimed)
116 })
117 .collect();
118
119 assert!(
120 outcomes.iter().any(|(vouched, _)| vouched.is_some()),
121 "some round must complete the touch first, or the interleaving never varied"
122 );
123 assert!(
124 outcomes.iter().any(|(vouched, _)| vouched.is_none()),
125 "some round must complete the sweep first, or the interleaving never varied"
126 );
127}