This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-maintenance / tests / chaos.rs
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}