Our Personal Data Server from scratch!
0

Configure Feed

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

tranquil-pds / crates / tranquil-store / tests / metastore_crash.rs
12 kB 372 lines
1use std::path::Path; 2 3use proptest::prelude::*; 4use rayon::prelude::*; 5use tranquil_store::metastore::recovery::{ 6 BacklinkMutation, CommitMutationSet, RecordMutationDelete, RecordMutationUpsert, 7}; 8use tranquil_store::metastore::{Metastore, MetastoreConfig}; 9use tranquil_store::{sim_proptest_cases, sim_seed_range}; 10use tranquil_types::{CidLink, Did, Handle}; 11use uuid::Uuid; 12 13const NAMES: &[&str] = &["olaren", "teq", "nel", "lyna", "bailey"]; 14 15fn test_config() -> MetastoreConfig { 16 MetastoreConfig { 17 cache_size_bytes: 16 * 1024 * 1024, 18 } 19} 20 21fn open_metastore(path: &Path) -> Metastore { 22 Metastore::open(path, test_config()).unwrap() 23} 24 25fn test_did(seed: u64) -> Did { 26 let name = NAMES[(seed as usize) % NAMES.len()]; 27 Did::from(format!("did:plc:{name}{seed}")) 28} 29 30fn test_handle(seed: u64) -> Handle { 31 let name = NAMES[(seed as usize) % NAMES.len()]; 32 Handle::new(format!("{name}{seed}.test")).unwrap() 33} 34 35fn test_cid_link(seed: u8) -> CidLink { 36 let digest: [u8; 32] = std::array::from_fn(|i| seed.wrapping_add(i as u8)); 37 let mh = multihash::Multihash::<64>::wrap(0x12, &digest).unwrap(); 38 let c = cid::Cid::new_v1(0x71, mh); 39 CidLink::from_cid(&c) 40} 41 42fn test_uuid(seed: u64) -> Uuid { 43 Uuid::from_u128(seed as u128 | 0x4000_0000_0000_0000_8000_0000_0000_0000) 44} 45 46fn arb_mutation_set() -> impl Strategy<Value = CommitMutationSet> { 47 let arb_upsert = ( 48 "[a-z\\.]{5,20}", 49 "[a-z0-9]{3,10}", 50 prop::collection::vec(any::<u8>(), 4..36), 51 ) 52 .prop_map(|(collection, rkey, cid_bytes)| RecordMutationUpsert { 53 collection, 54 rkey, 55 cid_bytes, 56 }); 57 58 let arb_delete = ("[a-z\\.]{5,20}", "[a-z0-9]{3,10}") 59 .prop_map(|(collection, rkey)| RecordMutationDelete { collection, rkey }); 60 61 let arb_backlink = ( 62 "at://did:plc:[a-z]{3,8}/[a-z\\.]{5,20}/[a-z0-9]{3,8}", 63 0u8..4, 64 "at://did:plc:[a-z]{3,8}/[a-z\\.]{5,20}/[a-z0-9]{3,8}", 65 ) 66 .prop_map(|(uri, path, link_to)| BacklinkMutation { uri, path, link_to }); 67 68 ( 69 prop::collection::vec(any::<u8>(), 0..64), 70 "[a-z0-9]{1,16}", 71 prop::collection::vec(arb_upsert, 0..20), 72 prop::collection::vec(arb_delete, 0..20), 73 prop::collection::vec(prop::collection::vec(any::<u8>(), 4..36), 0..20), 74 prop::collection::vec(prop::collection::vec(any::<u8>(), 4..36), 0..20), 75 prop::collection::vec(arb_backlink, 0..5), 76 prop::collection::vec("at://did:plc:[a-z]{3,8}/[a-z\\.]{5,20}/[a-z0-9]{3,8}", 0..5), 77 ) 78 .prop_map( 79 |( 80 new_root_cid, 81 new_rev, 82 record_upserts, 83 record_deletes, 84 block_inserts, 85 block_deletes, 86 backlink_adds, 87 backlink_remove_uris, 88 )| { 89 CommitMutationSet { 90 new_root_cid, 91 new_rev, 92 record_upserts, 93 record_deletes, 94 block_inserts, 95 block_deletes, 96 backlink_adds, 97 backlink_remove_uris, 98 } 99 }, 100 ) 101} 102 103proptest! { 104 #![proptest_config(ProptestConfig::with_cases(sim_proptest_cases()))] 105 106 #[test] 107 fn mutation_set_roundtrip_fuzz(ms in arb_mutation_set()) { 108 let serialized = ms.serialize().unwrap(); 109 let recovered = CommitMutationSet::deserialize(&serialized).unwrap(); 110 prop_assert_eq!(recovered, ms); 111 } 112 113 #[test] 114 fn mutation_set_rejects_corrupt_version(ms in arb_mutation_set()) { 115 let mut bytes = ms.serialize().unwrap(); 116 bytes[0] = 0xFF; 117 prop_assert!(CommitMutationSet::deserialize(&bytes).is_none()); 118 } 119 120 #[test] 121 fn mutation_set_truncation_detected( 122 ms in arb_mutation_set(), 123 truncate_at in 0usize..64, 124 ) { 125 let bytes = ms.serialize().unwrap(); 126 let cut = truncate_at.min(bytes.len().saturating_sub(1)); 127 match cut { 128 0 => prop_assert!(CommitMutationSet::deserialize(&bytes[..0]).is_none()), 129 n => { 130 let truncated = &bytes[..n]; 131 match CommitMutationSet::deserialize(truncated) { 132 None => {} 133 Some(recovered) => prop_assert_eq!(recovered, ms), 134 } 135 } 136 } 137 } 138} 139 140#[test] 141fn metastore_survives_abrupt_drop() { 142 sim_seed_range().into_par_iter().for_each(|seed| { 143 let dir = tempfile::TempDir::new().unwrap(); 144 let user_count = (seed % 5) + 1; 145 146 let user_ids: Vec<Uuid> = (0..user_count).map(|i| test_uuid(seed * 100 + i)).collect(); 147 148 { 149 let ms = open_metastore(dir.path()); 150 let repo_ops = ms.repo_ops(); 151 let db = ms.database(); 152 153 user_ids.iter().enumerate().for_each(|(i, &uid)| { 154 let idx = seed * 100 + i as u64; 155 let did = test_did(idx); 156 let handle = test_handle(idx); 157 let cid = test_cid_link((idx & 0xFF) as u8); 158 let rev = format!("rev{idx}"); 159 repo_ops 160 .create_repo(db, uid, &did, &handle, &cid, &rev) 161 .unwrap(); 162 }); 163 } 164 165 { 166 let ms = open_metastore(dir.path()); 167 let repo_ops = ms.repo_ops(); 168 169 user_ids.iter().enumerate().for_each(|(i, &uid)| { 170 let result = repo_ops.get_repo_meta(uid).unwrap(); 171 assert!( 172 result.is_some(), 173 "seed={seed} user {i} repo_meta missing after abrupt drop" 174 ); 175 let (_, meta) = result.unwrap(); 176 let expected_rev = format!("rev{}", seed * 100 + i as u64); 177 assert_eq!( 178 meta.repo_rev, expected_rev, 179 "seed={seed} user {i} rev mismatch" 180 ); 181 }); 182 } 183 }); 184} 185 186#[test] 187fn metastore_multi_crash_cycle() { 188 sim_seed_range().into_par_iter().for_each(|seed| { 189 let dir = tempfile::TempDir::new().unwrap(); 190 let cycles = (seed % 4) + 2; 191 let mut expected_repos: Vec<(Uuid, u64)> = Vec::new(); 192 193 (0..cycles).for_each(|cycle| { 194 let new_per_cycle = (seed.wrapping_add(cycle) % 3) + 1; 195 196 { 197 let ms = open_metastore(dir.path()); 198 199 expected_repos.iter().for_each(|(uid, idx)| { 200 let meta = ms.repo_ops().get_repo_meta(*uid).unwrap(); 201 assert!( 202 meta.is_some(), 203 "seed={seed} cycle={cycle} user idx={idx} missing before new writes" 204 ); 205 }); 206 207 let repo_ops = ms.repo_ops(); 208 let db = ms.database(); 209 (0..new_per_cycle).for_each(|i| { 210 let idx = seed * 1000 + cycle * 100 + i; 211 let uid = test_uuid(idx); 212 let did = test_did(idx); 213 let handle = test_handle(idx); 214 let cid = test_cid_link((idx & 0xFF) as u8); 215 repo_ops 216 .create_repo(db, uid, &did, &handle, &cid, &format!("rev{idx}")) 217 .unwrap(); 218 expected_repos.push((uid, idx)); 219 }); 220 } 221 }); 222 223 { 224 let ms = open_metastore(dir.path()); 225 expected_repos.iter().for_each(|(uid, idx)| { 226 let meta = ms.repo_ops().get_repo_meta(*uid).unwrap(); 227 assert!( 228 meta.is_some(), 229 "seed={seed} final verify: user idx={idx} missing" 230 ); 231 }); 232 } 233 }); 234} 235 236#[test] 237fn metastore_persisted_survives_unpersisted_lost() { 238 sim_seed_range().into_par_iter().for_each(|seed| { 239 let dir = tempfile::TempDir::new().unwrap(); 240 let batch_size = (seed % 5) + 2; 241 242 { 243 let ms = open_metastore(dir.path()); 244 let db = ms.database(); 245 let repo_ops = ms.repo_ops(); 246 247 (0..batch_size).for_each(|i| { 248 let idx = seed * 100 + i; 249 repo_ops 250 .create_repo( 251 db, 252 test_uuid(idx), 253 &test_did(idx), 254 &test_handle(idx), 255 &test_cid_link((idx & 0xFF) as u8), 256 &format!("rev{idx}"), 257 ) 258 .unwrap(); 259 }); 260 261 ms.persist().unwrap(); 262 263 let extra_idx = seed * 100 + batch_size; 264 repo_ops 265 .create_repo( 266 db, 267 test_uuid(extra_idx), 268 &test_did(extra_idx), 269 &test_handle(extra_idx), 270 &test_cid_link((extra_idx & 0xFF) as u8), 271 &format!("rev{extra_idx}"), 272 ) 273 .unwrap(); 274 } 275 276 { 277 let ms = open_metastore(dir.path()); 278 let repo_ops = ms.repo_ops(); 279 280 (0..batch_size).for_each(|i| { 281 let idx = seed * 100 + i; 282 let meta = repo_ops.get_repo_meta(test_uuid(idx)).unwrap(); 283 assert!( 284 meta.is_some(), 285 "seed={seed} persisted user idx={idx} must survive crash" 286 ); 287 }); 288 } 289 }); 290} 291 292#[test] 293fn metastore_user_hashes_reload_after_crash() { 294 sim_seed_range().into_par_iter().for_each(|seed| { 295 let dir = tempfile::TempDir::new().unwrap(); 296 let count = (seed % 5) + 1; 297 let name = NAMES[(seed as usize) % NAMES.len()]; 298 299 let entries: Vec<(Uuid, u64)> = (0..count) 300 .map(|i| { 301 let idx = seed * 100 + i; 302 (test_uuid(idx), idx) 303 }) 304 .collect(); 305 306 { 307 let ms = open_metastore(dir.path()); 308 let db = ms.database(); 309 let repo_ops = ms.repo_ops(); 310 311 entries.iter().for_each(|&(uid, idx)| { 312 repo_ops 313 .create_repo( 314 db, 315 uid, 316 &test_did(idx), 317 &test_handle(idx), 318 &test_cid_link((idx & 0xFF) as u8), 319 &format!("rev{idx}"), 320 ) 321 .unwrap(); 322 }); 323 } 324 325 { 326 let ms = open_metastore(dir.path()); 327 328 entries.iter().for_each(|&(uid, idx)| { 329 let hash = ms.user_hashes().get(&uid); 330 assert!( 331 hash.is_some(), 332 "seed={seed} name={name} user_hash for idx={idx} not reloaded after crash" 333 ); 334 }); 335 } 336 }); 337} 338 339#[test] 340fn metastore_handle_lookup_survives_crash() { 341 sim_seed_range().into_par_iter().for_each(|seed| { 342 let dir = tempfile::TempDir::new().unwrap(); 343 let idx = seed; 344 let uid = test_uuid(idx); 345 let did = test_did(idx); 346 let handle = test_handle(idx); 347 348 { 349 let ms = open_metastore(dir.path()); 350 ms.repo_ops() 351 .create_repo( 352 ms.database(), 353 uid, 354 &did, 355 &handle, 356 &test_cid_link((idx & 0xFF) as u8), 357 &format!("rev{idx}"), 358 ) 359 .unwrap(); 360 } 361 362 { 363 let ms = open_metastore(dir.path()); 364 let resolved = ms.repo_ops().lookup_handle(&handle).unwrap(); 365 assert!( 366 resolved.is_some(), 367 "seed={seed} handle '{}' not resolvable after crash", 368 handle.as_str() 369 ); 370 } 371 }); 372}