This repository has no description
7.8 kB
235 lines
1use std::path::Path;
2use std::process::{Command, Stdio};
3use std::time::{Duration, Instant};
4
5use knot_git::{
6 EntryKind, Identity, Layout, NewCommit, RefUpdate, Repo, StagedAction, StagedChange,
7};
8use knot_maintenance::{
9 GeometricFactor, ObjectCount, Options, PruneGrace, ReflogRetention, run_repo,
10};
11use knot_types::{AuthorName, BranchName, Email, Oid, RefName, RepoDid, UnixSeconds};
12
13const DID: &str = "did:plc:squid";
14const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
15const BLOB_BYTES: usize = 8 * 1024 * 1024;
16const HISTORY: u32 = 40;
17const NOW_SECONDS: UnixSeconds = UnixSeconds::new(1_700_000_500);
18
19fn incompressible(len: usize) -> Vec<u8> {
20 let mut state = 0x2545_f491_4f6c_dd1du64;
21 (0..len)
22 .map(|_| {
23 state ^= state << 13;
24 state ^= state >> 7;
25 state ^= state << 17;
26 (state & 0xff) as u8
27 })
28 .collect()
29}
30
31fn identity() -> Identity {
32 Identity {
33 name: AuthorName::new("nel"),
34 email: Email::new("nel@oyster.cafe"),
35 time: UnixSeconds::new(1_700_000_000),
36 offset_seconds: 0,
37 }
38}
39
40fn options() -> Options {
41 Options {
42 repack_max_objects: ObjectCount::new(5_000_000),
43 geometric_factor: GeometricFactor::full_repack(),
44 prune_grace: PruneGrace::from_secs(0),
45 reflog_floor: ReflogRetention::from_secs(i64::MAX as u64 / 4),
46 commit_graph: true,
47 multi_pack_index: true,
48 bitmap: true,
49 }
50}
51
52fn build_template(scan: &Path) {
53 let layout = Layout::new(scan).with_default_branch(BranchName::new("main").unwrap());
54 let did = RepoDid::new(DID).unwrap();
55 let repo = layout.create(&did).unwrap();
56 let main = RefName::new("refs/heads/main").unwrap();
57 let empty = Oid::from_hex(EMPTY_TREE).unwrap();
58
59 let big_tree = repo
60 .write_staged_tree(
61 empty,
62 &[StagedChange {
63 path: knot_types::RepoPath::new("big.bin").unwrap(),
64 action: StagedAction::Put {
65 content: incompressible(BLOB_BYTES),
66 kind: EntryKind::Blob,
67 },
68 }],
69 )
70 .unwrap();
71 let mut tip = repo
72 .write_commit(&NewCommit {
73 tree: big_tree,
74 parents: Vec::new(),
75 author: identity(),
76 committer: identity(),
77 message: "big".to_string(),
78 extra_headers: Vec::new(),
79 })
80 .unwrap();
81 (0..HISTORY).for_each(|index| {
82 let tree = repo
83 .write_staged_tree(
84 empty,
85 &[StagedChange {
86 path: knot_types::RepoPath::new(format!("file{index}.txt")).unwrap(),
87 action: StagedAction::Put {
88 content: format!("contents {index}").into_bytes(),
89 kind: EntryKind::Blob,
90 },
91 }],
92 )
93 .unwrap();
94 let next = repo
95 .write_commit(&NewCommit {
96 tree,
97 parents: vec![tip],
98 author: identity(),
99 committer: identity(),
100 message: format!("commit {index}"),
101 extra_headers: Vec::new(),
102 })
103 .unwrap();
104 let update = match index {
105 0 => RefUpdate::Create {
106 name: main.clone(),
107 new: next,
108 },
109 _ => RefUpdate::Update {
110 name: main.clone(),
111 old: tip,
112 new: next,
113 },
114 };
115 repo.update_ref(&update).unwrap();
116 tip = next;
117 });
118}
119
120fn copy_tree(src: &Path, dst: &Path) {
121 walkdir::WalkDir::new(src)
122 .into_iter()
123 .filter_map(Result::ok)
124 .for_each(|entry| {
125 let relative = entry.path().strip_prefix(src).unwrap();
126 let target = dst.join(relative);
127 if entry.file_type().is_dir() {
128 std::fs::create_dir_all(&target).unwrap();
129 } else {
130 if let Some(parent) = target.parent() {
131 std::fs::create_dir_all(parent).unwrap();
132 }
133 std::fs::copy(entry.path(), &target).unwrap();
134 }
135 });
136}
137
138fn spawn_worker(scan: &Path) -> std::process::Child {
139 Command::new(std::env::current_exe().unwrap())
140 .args(["--exact", "chaos_maintenance_worker", "--nocapture"])
141 .env("KNOT_CHAOS_ROLE", "worker")
142 .env("KNOT_CHAOS_SCAN", scan)
143 .stdout(Stdio::null())
144 .stderr(Stdio::null())
145 .spawn()
146 .expect("spawn chaos worker")
147}
148
149fn fsck_clean(bare: &Path) -> Result<(), String> {
150 knot_fixtures::fsck(bare)
151}
152
153fn main_tip(scan: &Path) -> Option<Oid> {
154 let layout = Layout::new(scan);
155 let repo = layout
156 .open(&RepoDid::new(DID).unwrap())
157 .expect("repo must reopen cleanly after kill");
158 repo.find_ref(&RefName::new("refs/heads/main").unwrap())
159 .expect("references must be readable after kill")
160}
161
162#[test]
163fn chaos_maintenance_worker() {
164 if std::env::var("KNOT_CHAOS_ROLE").as_deref() != Ok("worker") {
165 return;
166 }
167 let scan = std::env::var("KNOT_CHAOS_SCAN").unwrap();
168 let layout = Layout::new(&scan);
169 let repo = layout.open(&RepoDid::new(DID).unwrap()).unwrap();
170 let _ = run_repo(&repo, NOW_SECONDS, &options());
171}
172
173#[test]
174fn kill9_during_maintenance_leaves_a_consistent_repo() {
175 let scratch = tempfile::tempdir().unwrap();
176 let template = scratch.path().join("template");
177 build_template(&template);
178 let did = RepoDid::new(DID).unwrap();
179 let tip = main_tip(&template).expect("template has a main tip");
180
181 let warm = scratch.path().join("warm");
182 copy_tree(&template, &warm);
183 let started = Instant::now();
184 let mut child = spawn_worker(&warm);
185 child.wait().unwrap();
186 let full = started.elapsed();
187 let warm_repo = Repo::open(Layout::new(&warm).repo_path(&did).unwrap()).unwrap();
188 assert!(
189 warm_repo
190 .git()
191 .git_dir()
192 .join("objects/info/commit-graph")
193 .exists(),
194 "uninterrupted maintenance run writes commit-graph"
195 );
196
197 let fractions = [0.20, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95, 1.10];
198 let delays: Vec<Duration> = std::iter::once(Duration::from_millis(1))
199 .chain(std::iter::once(Duration::from_millis(3)))
200 .chain(fractions.iter().map(|fraction| full.mul_f64(*fraction)))
201 .chain(std::iter::once(full.mul_f64(2.0)))
202 .collect();
203
204 delays.iter().enumerate().for_each(|(trial, delay)| {
205 let scan = scratch.path().join(format!("scan-{trial}"));
206 copy_tree(&template, &scan);
207 let mut child = spawn_worker(&scan);
208 std::thread::sleep(*delay);
209 let _ = child.kill();
210 child.wait().unwrap();
211
212 let bare = Layout::new(&scan).repo_path(&did).unwrap();
213 fsck_clean(&bare).unwrap_or_else(|errors| {
214 panic!("trial {trial}: killed maintenance run left corrupt repo:\n{errors}")
215 });
216 assert_eq!(
217 main_tip(&scan),
218 Some(tip),
219 "trial {trial}: maintenance never changes branch value, so main must still resolve to tip"
220 );
221
222 let recovered = Repo::open(&bare).unwrap();
223 run_repo(&recovered, NOW_SECONDS, &options()).unwrap_or_else(|error| {
224 panic!("trial {trial}: maintenance must self-heal after crash, got {error}")
225 });
226 fsck_clean(&bare).unwrap_or_else(|errors| {
227 panic!("trial {trial}: self-heal pass left corrupt repo:\n{errors}")
228 });
229 assert_eq!(
230 main_tip(&scan),
231 Some(tip),
232 "trial {trial}: recovered repo still resolves main to tip"
233 );
234 });
235}