This repository has no description
0

Configure Feed

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

core / knot2 / crates / knot-sim / tests / lfs_chaos.rs
7.6 kB 227 lines
1use std::collections::HashSet; 2use std::path::{Path, PathBuf}; 3use std::process::{Command, Stdio}; 4use std::time::{Duration, Instant, SystemTime}; 5 6use knot_cob::{CobHome, CobStore}; 7use knot_cobs::{Registration, RegistryChange, RepoRef, RepoRegistryCob, deregister_repo}; 8use knot_git::{Layout, Repo}; 9use knot_lfs::{ClaimedSize, DiskStore, LfsOid, LfsStore, LfsStorePath}; 10use knot_runtime::OsEntropy; 11use knot_secrets::{MasterKey, SealedStore}; 12use knot_types::{KnotId, OwnerDid, RepoDid, RepoName, RepoRkey, UnixSeconds}; 13use sha2::{Digest, Sha256}; 14 15const REPO_DID: &str = "did:plc:squid"; 16const REPO_NAME: &str = "anemone"; 17const OWNER_DID: &str = "did:plc:nel"; 18const KNOT_DID: &str = "did:web:nel.pet"; 19const MEDIA: &[u8] = b"\xff\x00media that mustn't outlive its repo"; 20const GRACE: Duration = Duration::from_secs(86_400); 21const BACKDATE: Duration = Duration::from_secs(60 * 86_400); 22 23fn did() -> RepoDid { 24 RepoDid::new(REPO_DID).unwrap() 25} 26 27fn knot() -> KnotId { 28 KnotId::new(KNOT_DID).unwrap() 29} 30 31fn master() -> MasterKey { 32 MasterKey::new([7u8; 32]).unwrap() 33} 34 35fn media_oid() -> LfsOid { 36 LfsOid::from_digest(Sha256::digest(MEDIA).into()) 37} 38 39struct Paths { 40 meta: PathBuf, 41 scan: PathBuf, 42 store: PathBuf, 43 keys: PathBuf, 44} 45 46impl Paths { 47 fn under(root: &Path) -> Self { 48 Self { 49 meta: root.join("meta"), 50 scan: root.join("repos"), 51 store: root.join("lfs"), 52 keys: root.join("keys.sealed"), 53 } 54 } 55} 56 57fn build_fixture(root: &Path) -> Paths { 58 let paths = Paths::under(root); 59 Repo::create(&paths.meta).unwrap(); 60 Layout::new(&paths.scan).create(&did()).unwrap(); 61 62 let secrets = SealedStore::open(&paths.keys, &master(), Box::new(OsEntropy)).unwrap(); 63 secrets.ensure(&knot()).unwrap(); 64 let signer = secrets.signer(&knot()).unwrap(); 65 let meta = Repo::open(&paths.meta).unwrap(); 66 CobStore::new(&meta) 67 .create( 68 &CobHome::from(&knot()), 69 &RegistryChange::Register(Registration { 70 owner: OwnerDid::new(OWNER_DID).unwrap(), 71 rkey: RepoRkey::new(REPO_NAME).unwrap(), 72 name: RepoName::new(REPO_NAME).unwrap(), 73 repo: did(), 74 created_at: UnixSeconds::new(1), 75 }), 76 &signer, 77 UnixSeconds::new(1), 78 ) 79 .unwrap(); 80 81 std::fs::create_dir_all(&paths.store).unwrap(); 82 let store = DiskStore::open(LfsStorePath::new(&paths.store)).unwrap(); 83 store 84 .put( 85 &did(), 86 &media_oid(), 87 ClaimedSize::new(MEDIA.len() as u64), 88 &mut &MEDIA[..], 89 ) 90 .unwrap(); 91 let object_path = store.object_file(&did(), &media_oid()).unwrap().unwrap().1; 92 std::fs::OpenOptions::new() 93 .write(true) 94 .open(object_path) 95 .unwrap() 96 .set_modified(SystemTime::now() - BACKDATE) 97 .unwrap(); 98 paths 99} 100 101fn spawn_worker(paths: &Paths) -> std::process::Child { 102 Command::new(std::env::current_exe().unwrap()) 103 .args(["--exact", "chaos_delete_worker", "--nocapture"]) 104 .env("KNOT_CHAOS_ROLE", "delete") 105 .env("KNOT_CHAOS_META", &paths.meta) 106 .env("KNOT_CHAOS_SCAN", &paths.scan) 107 .env("KNOT_CHAOS_STORE", &paths.store) 108 .env("KNOT_CHAOS_KEYS", &paths.keys) 109 .stdout(Stdio::null()) 110 .stderr(Stdio::null()) 111 .spawn() 112 .expect("spawn chaos worker") 113} 114 115#[test] 116fn chaos_delete_worker() { 117 if std::env::var("KNOT_CHAOS_ROLE").as_deref() != Ok("delete") { 118 return; 119 } 120 let meta = Repo::open(std::env::var("KNOT_CHAOS_META").unwrap()).unwrap(); 121 let store = CobStore::new(&meta); 122 let secrets = SealedStore::open( 123 std::env::var("KNOT_CHAOS_KEYS").unwrap(), 124 &master(), 125 Box::new(OsEntropy), 126 ) 127 .unwrap(); 128 let signer = secrets.signer(&knot()).unwrap(); 129 let object = store.list::<RepoRegistryCob>().unwrap()[0]; 130 deregister_repo( 131 &store, 132 &CobHome::from(&knot()), 133 object, 134 RepoRef { 135 owner: OwnerDid::new(OWNER_DID).unwrap(), 136 rkey: RepoRkey::new(REPO_NAME).unwrap(), 137 }, 138 did(), 139 &signer, 140 UnixSeconds::new(2), 141 ) 142 .unwrap(); 143 Layout::new(std::env::var("KNOT_CHAOS_SCAN").unwrap()) 144 .remove(&did()) 145 .unwrap(); 146 DiskStore::open(LfsStorePath::new( 147 std::env::var("KNOT_CHAOS_STORE").unwrap(), 148 )) 149 .unwrap() 150 .remove_repo(&did()) 151 .unwrap(); 152} 153 154#[test] 155fn kill9_between_delete_steps_never_strands_the_store_prefix() { 156 let scratch = tempfile::tempdir().unwrap(); 157 158 let warm = build_fixture(&scratch.path().join("warm")); 159 let started = Instant::now(); 160 spawn_worker(&warm).wait().unwrap(); 161 let full = started.elapsed(); 162 let warm_store = DiskStore::open(LfsStorePath::new(&warm.store)).unwrap(); 163 assert_eq!( 164 warm_store.probe(&did(), &media_oid()).unwrap(), 165 None, 166 "an uninterrupted delete removes the store prefix itself" 167 ); 168 169 let fractions = [0.20, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95]; 170 let delays: Vec<Duration> = std::iter::once(Duration::from_millis(1)) 171 .chain(std::iter::once(Duration::from_millis(3))) 172 .chain(fractions.iter().map(|fraction| full.mul_f64(*fraction))) 173 .chain(std::iter::once(full.mul_f64(2.0))) 174 .collect(); 175 176 let outcomes: Vec<bool> = delays 177 .iter() 178 .enumerate() 179 .map(|(trial, delay)| { 180 let paths = build_fixture(&scratch.path().join(format!("trial-{trial}"))); 181 let mut child = spawn_worker(&paths); 182 std::thread::sleep(*delay); 183 let _ = child.kill(); 184 child.wait().unwrap(); 185 186 let index = knot_index::Index::new(paths.meta.clone(), Layout::new(&paths.scan)); 187 index.rebuild().unwrap_or_else(|error| { 188 panic!("trial {trial}: registry must rebuild after a killed delete: {error}") 189 }); 190 assert_eq!( 191 index.coverage().registry, 192 knot_index::Coverage::Ready, 193 "trial {trial}: a rebuilt projection is ready" 194 ); 195 let hosted: HashSet<RepoDid> = index.hosted_repos().into_iter().collect(); 196 let registered = hosted.contains(&did()); 197 198 let store = DiskStore::open(LfsStorePath::new(&paths.store)).unwrap(); 199 store 200 .sweep_orphans(&hosted, GRACE, SystemTime::now()) 201 .unwrap_or_else(|error| { 202 panic!("trial {trial}: orphan sweep must run after a killed delete: {error}") 203 }); 204 let present = store.probe(&did(), &media_oid()).unwrap().is_some(); 205 match registered { 206 true => assert!( 207 present, 208 "trial {trial}: a still-registered repo's store prefix is never condemned" 209 ), 210 false => assert!( 211 !present, 212 "trial {trial}: the orphan sweep reclaims the prefix the killed delete left behind" 213 ), 214 } 215 registered 216 }) 217 .collect(); 218 219 assert!( 220 outcomes.iter().any(|registered| *registered), 221 "some trial must die before the deregister lands, or the chaos window never opened" 222 ); 223 assert!( 224 outcomes.iter().any(|registered| !registered), 225 "some trial must land the deregister, or the kill delays are all too short" 226 ); 227}