Our Personal Data Server from scratch! tranquil.farm
pds rust database fun oauth atproto
238
fork

Configure Feed

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

test(tranquil-store): migrate some tests to gauntlet

Lewis: May this revision serve well! <lu5a@proton.me>

+461 -90
+58 -3
Cargo.lock
··· 1558 1558 dependencies = [ 1559 1559 "confique-macro", 1560 1560 "serde", 1561 - "toml", 1561 + "toml 0.9.12+spec-1.1.0", 1562 1562 ] 1563 1563 1564 1564 [[package]] ··· 5050 5050 source = "registry+https://github.com/rust-lang/crates.io-index" 5051 5051 checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" 5052 5052 dependencies = [ 5053 - "toml_edit", 5053 + "toml_edit 0.25.5+spec-1.1.0", 5054 5054 ] 5055 5055 5056 5056 [[package]] ··· 6203 6203 6204 6204 [[package]] 6205 6205 name = "serde_spanned" 6206 + version = "0.6.9" 6207 + source = "registry+https://github.com/rust-lang/crates.io-index" 6208 + checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 6209 + dependencies = [ 6210 + "serde", 6211 + ] 6212 + 6213 + [[package]] 6214 + name = "serde_spanned" 6206 6215 version = "1.0.4" 6207 6216 source = "registry+https://github.com/rust-lang/crates.io-index" 6208 6217 checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" ··· 7146 7155 7147 7156 [[package]] 7148 7157 name = "toml" 7158 + version = "0.8.23" 7159 + source = "registry+https://github.com/rust-lang/crates.io-index" 7160 + checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 7161 + dependencies = [ 7162 + "serde", 7163 + "serde_spanned 0.6.9", 7164 + "toml_datetime 0.6.11", 7165 + "toml_edit 0.22.27", 7166 + ] 7167 + 7168 + [[package]] 7169 + name = "toml" 7149 7170 version = "0.9.12+spec-1.1.0" 7150 7171 source = "registry+https://github.com/rust-lang/crates.io-index" 7151 7172 checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" 7152 7173 dependencies = [ 7153 7174 "indexmap 2.13.0", 7154 7175 "serde_core", 7155 - "serde_spanned", 7176 + "serde_spanned 1.0.4", 7156 7177 "toml_datetime 0.7.5+spec-1.1.0", 7157 7178 "toml_parser", 7158 7179 "toml_writer", ··· 7161 7182 7162 7183 [[package]] 7163 7184 name = "toml_datetime" 7185 + version = "0.6.11" 7186 + source = "registry+https://github.com/rust-lang/crates.io-index" 7187 + checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 7188 + dependencies = [ 7189 + "serde", 7190 + ] 7191 + 7192 + [[package]] 7193 + name = "toml_datetime" 7164 7194 version = "0.7.5+spec-1.1.0" 7165 7195 source = "registry+https://github.com/rust-lang/crates.io-index" 7166 7196 checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" ··· 7179 7209 7180 7210 [[package]] 7181 7211 name = "toml_edit" 7212 + version = "0.22.27" 7213 + source = "registry+https://github.com/rust-lang/crates.io-index" 7214 + checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 7215 + dependencies = [ 7216 + "indexmap 2.13.0", 7217 + "serde", 7218 + "serde_spanned 0.6.9", 7219 + "toml_datetime 0.6.11", 7220 + "toml_write", 7221 + "winnow 0.7.15", 7222 + ] 7223 + 7224 + [[package]] 7225 + name = "toml_edit" 7182 7226 version = "0.25.5+spec-1.1.0" 7183 7227 source = "registry+https://github.com/rust-lang/crates.io-index" 7184 7228 checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" ··· 7197 7241 dependencies = [ 7198 7242 "winnow 1.0.0", 7199 7243 ] 7244 + 7245 + [[package]] 7246 + name = "toml_write" 7247 + version = "0.1.2" 7248 + source = "registry+https://github.com/rust-lang/crates.io-index" 7249 + checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" 7200 7250 7201 7251 [[package]] 7202 7252 name = "toml_writer" ··· 7866 7916 "bytes", 7867 7917 "chrono", 7868 7918 "cid", 7919 + "clap", 7869 7920 "dashmap", 7870 7921 "fjall", 7871 7922 "flume 0.11.1", ··· 7893 7944 "thiserror 2.0.18", 7894 7945 "tikv-jemallocator", 7895 7946 "tokio", 7947 + "toml 0.8.23", 7896 7948 "tracing", 7897 7949 "tracing-subscriber", 7898 7950 "tranquil-db", ··· 8854 8906 version = "0.7.15" 8855 8907 source = "registry+https://github.com/rust-lang/crates.io-index" 8856 8908 checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" 8909 + dependencies = [ 8910 + "memchr", 8911 + ] 8857 8912 8858 8913 [[package]] 8859 8914 name = "winnow"
+8 -23
crates/tranquil-lexicon/src/dynamic.rs
··· 247 247 self.wait_for_leader(nsid).await; 248 248 match self.get_cached(nsid) { 249 249 Some(doc) => Ok(doc), 250 - None if self.is_negative_cached(nsid) => { 251 - Err(ResolveError::NegativelyCached { 252 - nsid: nsid.to_string(), 253 - ttl_secs: NEGATIVE_CACHE_TTL.as_secs(), 254 - }) 255 - } 250 + None if self.is_negative_cached(nsid) => Err(ResolveError::NegativelyCached { 251 + nsid: nsid.to_string(), 252 + ttl_secs: NEGATIVE_CACHE_TTL.as_secs(), 253 + }), 256 254 None => Err(ResolveError::LeaderAborted { 257 255 nsid: nsid.to_string(), 258 256 }), ··· 429 427 let served = result.expect("stale entry must be served when refresh fails"); 430 428 assert_eq!(served.id, "pet.nel.flaky"); 431 429 assert!( 432 - registry 433 - .get_entry("pet.nel.flaky") 434 - .unwrap() 435 - .is_fresh(), 430 + registry.get_entry("pet.nel.flaky").unwrap().is_fresh(), 436 431 "failed refresh must bump expiry so subsequent lookups skip the resolver" 437 432 ); 438 433 assert!( ··· 492 487 registry.insert_schema(doc); 493 488 registry.expire_now("pet.nel.refresh"); 494 489 495 - assert!( 496 - !registry 497 - .get_entry("pet.nel.refresh") 498 - .unwrap() 499 - .is_fresh() 500 - ); 490 + assert!(!registry.get_entry("pet.nel.refresh").unwrap().is_fresh()); 501 491 502 492 let refreshed = registry 503 493 .resolve_and_cache_with("pet.nel.refresh", |n| async move { ··· 512 502 513 503 assert_eq!(refreshed.id, "pet.nel.refresh"); 514 504 assert!( 515 - registry 516 - .get_entry("pet.nel.refresh") 517 - .unwrap() 518 - .is_fresh(), 505 + registry.get_entry("pet.nel.refresh").unwrap().is_fresh(), 519 506 "refresh must restore freshness" 520 507 ); 521 508 } ··· 601 588 assert!(registry.is_negative_cached("pet.nel.failHerd")); 602 589 } 603 590 604 - async fn futures_collect<T>( 605 - handles: Vec<tokio::task::JoinHandle<T>>, 606 - ) -> Vec<T> { 591 + async fn futures_collect<T>(handles: Vec<tokio::task::JoinHandle<T>>) -> Vec<T> { 607 592 futures::future::join_all(handles) 608 593 .await 609 594 .into_iter()
+1 -1
crates/tranquil-pds/tests/gc_compaction_restart.rs
··· 111 111 let max_file_size = store 112 112 .list_data_files() 113 113 .ok() 114 - .and_then(|_| Some(4 * 1024 * 1024u64)) 114 + .map(|_| 4 * 1024 * 1024u64) 115 115 .unwrap_or(4 * 1024 * 1024); 116 116 117 117 let reopened_missing = tokio::task::spawn_blocking(move || {
+2 -2
crates/tranquil-store/src/lib.rs
··· 28 28 }; 29 29 #[cfg(any(test, feature = "test-harness"))] 30 30 pub use sim::{ 31 - FaultConfig, OpRecord, SimulatedIO, sim_proptest_cases, sim_seed_count, sim_seed_range, 32 - sim_single_seed, 31 + FaultConfig, LatencyNs, OpRecord, Probability, SimulatedIO, SyncReorderWindow, 32 + sim_proptest_cases, sim_seed_count, sim_seed_range, sim_single_seed, 33 33 }; 34 34 35 35 pub(crate) fn wall_clock_ms() -> blockstore::WallClockMs {
+7 -5
crates/tranquil-store/tests/eventlog_crash.rs
··· 7 7 SEGMENT_HEADER_SIZE, SegmentId, SegmentManager, SegmentReader, SegmentWriter, TimestampMicros, 8 8 ValidEvent, rebuild_from_segment, 9 9 }; 10 - use tranquil_store::{FaultConfig, OpenOptions, SimulatedIO, StorageIO, sim_seed_range}; 10 + use tranquil_store::{ 11 + FaultConfig, OpenOptions, Probability, SimulatedIO, StorageIO, sim_seed_range, 12 + }; 11 13 12 14 fn setup_manager(sim: SimulatedIO, max_segment_size: u64) -> Arc<SegmentManager<SimulatedIO>> { 13 15 Arc::new(SegmentManager::new(sim, PathBuf::from("/segments"), max_segment_size).unwrap()) ··· 540 542 ( 541 543 "partial_writes_only", 542 544 FaultConfig { 543 - partial_write_probability: 0.15, 545 + partial_write_probability: Probability::new(0.15), 544 546 ..FaultConfig::none() 545 547 }, 546 548 ), 547 549 ( 548 550 "sync_failures_only", 549 551 FaultConfig { 550 - sync_failure_probability: 0.10, 551 - dir_sync_failure_probability: 0.05, 552 + sync_failure_probability: Probability::new(0.10), 553 + dir_sync_failure_probability: Probability::new(0.05), 552 554 ..FaultConfig::none() 553 555 }, 554 556 ), ··· 556 558 ( 557 559 "bit_flips_only", 558 560 FaultConfig { 559 - bit_flip_on_read_probability: 0.05, 561 + bit_flip_on_read_probability: Probability::new(0.05), 560 562 ..FaultConfig::none() 561 563 }, 562 564 ),
+371 -41
crates/tranquil-store/tests/gauntlet_smoke.rs
··· 1 + use tranquil_store::FaultConfig; 1 2 use tranquil_store::blockstore::GroupCommitConfig; 2 3 use tranquil_store::gauntlet::{ 3 - CollectionName, Gauntlet, GauntletConfig, InvariantSet, IoBackend, KeySpaceSize, MaxFileSize, 4 - OpCount, OpInterval, OpWeights, RestartPolicy, RunLimits, Scenario, Seed, ShardCount, 5 - SizeDistribution, StoreConfig, ValueBytes, WallMs, WorkloadModel, config_for, farm, 4 + CollectionName, ConfigOverrides, DidSpaceSize, Gauntlet, GauntletConfig, GauntletReport, 5 + InvariantSet, IoBackend, KeySpaceSize, MaxFileSize, OpCount, OpInterval, OpWeights, 6 + RegressionRecord, RestartPolicy, RetentionMaxSecs, RunLimits, Scenario, Seed, ShardCount, 7 + SizeDistribution, StoreConfig, StoreOverrides, ValueBytes, WallMs, WorkloadModel, 8 + WriterConcurrency, config_for, farm, 6 9 }; 7 10 11 + #[track_caller] 12 + fn assert_clean(report: &GauntletReport) { 13 + let violations: Vec<String> = report 14 + .violations 15 + .iter() 16 + .map(|v| format!("{}: {}", v.invariant, v.detail)) 17 + .collect(); 18 + assert!(report.is_clean(), "violations: {violations:?}"); 19 + } 20 + 8 21 #[test] 9 22 #[ignore = "long running, 30 seeds of 10k ops each"] 10 23 fn smoke_pr_30_seeds() { ··· 38 51 workload: WorkloadModel { 39 52 weights: OpWeights { 40 53 add: 80, 41 - delete: 0, 42 54 compact: 10, 43 55 checkpoint: 10, 56 + ..OpWeights::default() 44 57 }, 45 58 size_distribution: SizeDistribution::Fixed(ValueBytes(64)), 46 59 collections: vec![CollectionName("app.bsky.feed.post".to_string())], 47 60 key_space: KeySpaceSize(100), 61 + did_space: DidSpaceSize(32), 62 + retention_max_secs: RetentionMaxSecs(3600), 48 63 }, 49 64 op_count: OpCount(200), 50 65 invariants: InvariantSet::REFCOUNT_CONSERVATION ··· 65 80 }, 66 81 shard_count: ShardCount(1), 67 82 }, 83 + eventlog: None, 84 + writer_concurrency: WriterConcurrency(1), 68 85 } 69 86 } 70 87 ··· 74 91 .expect("build gauntlet") 75 92 .run() 76 93 .await; 77 - assert!( 78 - report.is_clean(), 79 - "violations: {:?}", 80 - report 81 - .violations 82 - .iter() 83 - .map(|v| format!("{}: {}", v.invariant, v.detail)) 84 - .collect::<Vec<_>>() 85 - ); 86 - assert!( 87 - report.restarts.0 >= 2, 88 - "expected at least 2 restarts, got {}", 89 - report.restarts.0 90 - ); 94 + assert_clean(&report); 95 + assert!(report.restarts.0 >= 2); 91 96 assert_eq!(report.ops_executed.0, 200); 92 97 } 93 98 ··· 95 100 async fn full_stack_restart_port() { 96 101 let cfg = config_for(Scenario::FullStackRestart, Seed(1)); 97 102 let report = Gauntlet::new(cfg).expect("build gauntlet").run().await; 98 - assert!( 99 - report.is_clean(), 100 - "violations: {:?}", 101 - report 102 - .violations 103 - .iter() 104 - .map(|v| format!("{}: {}", v.invariant, v.detail)) 105 - .collect::<Vec<_>>() 106 - ); 103 + assert_clean(&report); 107 104 assert_eq!( 108 105 report.restarts.0, 10, 109 106 "FullStackRestart with EveryNOps(500) over 5000 ops must restart exactly 10 times", ··· 111 108 } 112 109 113 110 #[tokio::test] 111 + async fn compaction_idempotent_sanity() { 112 + let cfg = GauntletConfig { 113 + seed: Seed(3), 114 + io: IoBackend::Real, 115 + workload: WorkloadModel { 116 + weights: OpWeights { 117 + add: 70, 118 + delete: 10, 119 + compact: 15, 120 + checkpoint: 5, 121 + ..OpWeights::default() 122 + }, 123 + size_distribution: SizeDistribution::Fixed(ValueBytes(64)), 124 + collections: vec![CollectionName("app.bsky.feed.post".to_string())], 125 + key_space: KeySpaceSize(50), 126 + did_space: DidSpaceSize(32), 127 + retention_max_secs: RetentionMaxSecs(3600), 128 + }, 129 + op_count: OpCount(300), 130 + invariants: InvariantSet::REFCOUNT_CONSERVATION 131 + | InvariantSet::REACHABILITY 132 + | InvariantSet::READ_AFTER_WRITE 133 + | InvariantSet::COMPACTION_IDEMPOTENT, 134 + limits: RunLimits { 135 + max_wall_ms: Some(WallMs(30_000)), 136 + }, 137 + restart_policy: RestartPolicy::Never, 138 + store: StoreConfig { 139 + max_file_size: MaxFileSize(4096), 140 + group_commit: GroupCommitConfig::default(), 141 + shard_count: ShardCount(1), 142 + }, 143 + eventlog: None, 144 + writer_concurrency: WriterConcurrency(1), 145 + }; 146 + let report = Gauntlet::new(cfg).expect("build gauntlet").run().await; 147 + assert_clean(&report); 148 + } 149 + 150 + #[tokio::test] 151 + async fn no_orphan_files_sanity() { 152 + let cfg = GauntletConfig { 153 + seed: Seed(11), 154 + io: IoBackend::Real, 155 + workload: WorkloadModel { 156 + weights: OpWeights { 157 + add: 90, 158 + checkpoint: 10, 159 + ..OpWeights::default() 160 + }, 161 + size_distribution: SizeDistribution::Fixed(ValueBytes(128)), 162 + collections: vec![CollectionName("app.bsky.feed.post".to_string())], 163 + key_space: KeySpaceSize(80), 164 + did_space: DidSpaceSize(32), 165 + retention_max_secs: RetentionMaxSecs(3600), 166 + }, 167 + op_count: OpCount(200), 168 + invariants: InvariantSet::REFCOUNT_CONSERVATION 169 + | InvariantSet::REACHABILITY 170 + | InvariantSet::READ_AFTER_WRITE 171 + | InvariantSet::COMPACTION_IDEMPOTENT 172 + | InvariantSet::NO_ORPHAN_FILES, 173 + limits: RunLimits { 174 + max_wall_ms: Some(WallMs(30_000)), 175 + }, 176 + restart_policy: RestartPolicy::Never, 177 + store: StoreConfig { 178 + max_file_size: MaxFileSize(64 * 1024), 179 + group_commit: GroupCommitConfig::default(), 180 + shard_count: ShardCount(1), 181 + }, 182 + eventlog: None, 183 + writer_concurrency: WriterConcurrency(1), 184 + }; 185 + let report = Gauntlet::new(cfg).expect("build gauntlet").run().await; 186 + assert_clean(&report); 187 + } 188 + 189 + #[tokio::test] 190 + async fn simulated_pristine_roundtrip() { 191 + let cfg = GauntletConfig { 192 + seed: Seed(21), 193 + io: IoBackend::Simulated { 194 + fault: FaultConfig::none(), 195 + }, 196 + workload: WorkloadModel { 197 + weights: OpWeights { 198 + add: 80, 199 + delete: 10, 200 + compact: 5, 201 + checkpoint: 5, 202 + ..OpWeights::default() 203 + }, 204 + size_distribution: SizeDistribution::Fixed(ValueBytes(96)), 205 + collections: vec![CollectionName("app.bsky.feed.post".to_string())], 206 + key_space: KeySpaceSize(80), 207 + did_space: DidSpaceSize(32), 208 + retention_max_secs: RetentionMaxSecs(3600), 209 + }, 210 + op_count: OpCount(300), 211 + invariants: InvariantSet::REFCOUNT_CONSERVATION 212 + | InvariantSet::REACHABILITY 213 + | InvariantSet::ACKED_WRITE_PERSISTENCE 214 + | InvariantSet::READ_AFTER_WRITE 215 + | InvariantSet::RESTART_IDEMPOTENT 216 + | InvariantSet::CHECKSUM_COVERAGE, 217 + limits: RunLimits { 218 + max_wall_ms: Some(WallMs(60_000)), 219 + }, 220 + restart_policy: RestartPolicy::EveryNOps(OpInterval(100)), 221 + store: StoreConfig { 222 + max_file_size: MaxFileSize(8 * 1024), 223 + group_commit: GroupCommitConfig::default(), 224 + shard_count: ShardCount(1), 225 + }, 226 + eventlog: None, 227 + writer_concurrency: WriterConcurrency(1), 228 + }; 229 + let report = Gauntlet::new(cfg).expect("build gauntlet").run().await; 230 + assert_clean(&report); 231 + assert_eq!(report.ops_executed.0, 300); 232 + assert!(report.restarts.0 >= 2); 233 + } 234 + 235 + #[tokio::test] 236 + async fn firehose_fanout_pristine_smoke() { 237 + use tranquil_store::gauntlet::{EventLogConfig, MaxSegmentSize}; 238 + 239 + let cfg = GauntletConfig { 240 + seed: Seed(1), 241 + io: IoBackend::Simulated { 242 + fault: FaultConfig::none(), 243 + }, 244 + workload: WorkloadModel { 245 + weights: OpWeights { 246 + add: 20, 247 + compact: 2, 248 + checkpoint: 3, 249 + append_event: 60, 250 + sync_event_log: 10, 251 + run_retention: 5, 252 + ..OpWeights::default() 253 + }, 254 + size_distribution: SizeDistribution::Fixed(ValueBytes(128)), 255 + collections: vec![CollectionName("app.bsky.feed.post".to_string())], 256 + key_space: KeySpaceSize(100), 257 + did_space: DidSpaceSize(32), 258 + retention_max_secs: RetentionMaxSecs(60), 259 + }, 260 + op_count: OpCount(2_000), 261 + invariants: InvariantSet::REFCOUNT_CONSERVATION 262 + | InvariantSet::REACHABILITY 263 + | InvariantSet::ACKED_WRITE_PERSISTENCE 264 + | InvariantSet::READ_AFTER_WRITE 265 + | InvariantSet::RESTART_IDEMPOTENT 266 + | InvariantSet::MONOTONIC_SEQ 267 + | InvariantSet::FSYNC_ORDERING 268 + | InvariantSet::TOMBSTONE_BOUND, 269 + limits: RunLimits { 270 + max_wall_ms: Some(WallMs(60_000)), 271 + }, 272 + restart_policy: RestartPolicy::EveryNOps(OpInterval(500)), 273 + store: StoreConfig { 274 + max_file_size: MaxFileSize(16 * 1024), 275 + group_commit: GroupCommitConfig::default(), 276 + shard_count: ShardCount(1), 277 + }, 278 + eventlog: Some(EventLogConfig { 279 + max_segment_size: MaxSegmentSize(32 * 1024), 280 + }), 281 + writer_concurrency: WriterConcurrency(1), 282 + }; 283 + let report = Gauntlet::new(cfg).expect("build gauntlet").run().await; 284 + assert_clean(&report); 285 + assert_eq!(report.ops_executed.0, 2_000); 286 + assert!(report.restarts.0 >= 2); 287 + } 288 + 289 + #[tokio::test] 290 + async fn contended_readers_pristine_smoke() { 291 + let cfg = GauntletConfig { 292 + seed: Seed(1), 293 + io: IoBackend::Simulated { 294 + fault: FaultConfig::none(), 295 + }, 296 + workload: WorkloadModel { 297 + weights: OpWeights { 298 + add: 20, 299 + compact: 2, 300 + checkpoint: 3, 301 + read_record: 60, 302 + read_block: 15, 303 + ..OpWeights::default() 304 + }, 305 + size_distribution: SizeDistribution::Fixed(ValueBytes(128)), 306 + collections: vec![CollectionName("app.bsky.feed.post".to_string())], 307 + key_space: KeySpaceSize(200), 308 + did_space: DidSpaceSize(32), 309 + retention_max_secs: RetentionMaxSecs(3600), 310 + }, 311 + op_count: OpCount(1_000), 312 + invariants: InvariantSet::REFCOUNT_CONSERVATION 313 + | InvariantSet::REACHABILITY 314 + | InvariantSet::ACKED_WRITE_PERSISTENCE 315 + | InvariantSet::READ_AFTER_WRITE 316 + | InvariantSet::RESTART_IDEMPOTENT, 317 + limits: RunLimits { 318 + max_wall_ms: Some(WallMs(60_000)), 319 + }, 320 + restart_policy: RestartPolicy::EveryNOps(OpInterval(250)), 321 + store: StoreConfig { 322 + max_file_size: MaxFileSize(16 * 1024), 323 + group_commit: GroupCommitConfig::default(), 324 + shard_count: ShardCount(1), 325 + }, 326 + eventlog: None, 327 + writer_concurrency: WriterConcurrency(16), 328 + }; 329 + let report = Gauntlet::new(cfg).expect("build gauntlet").run().await; 330 + assert_clean(&report); 331 + assert_eq!(report.ops_executed.0, 1_000); 332 + assert!(report.restarts.0 >= 2); 333 + } 334 + 335 + #[tokio::test] 336 + async fn contended_writers_pristine_smoke() { 337 + let cfg = GauntletConfig { 338 + seed: Seed(2), 339 + io: IoBackend::Simulated { 340 + fault: FaultConfig::none(), 341 + }, 342 + workload: WorkloadModel { 343 + weights: OpWeights { 344 + add: 85, 345 + delete: 5, 346 + compact: 3, 347 + checkpoint: 2, 348 + read_record: 4, 349 + read_block: 1, 350 + ..OpWeights::default() 351 + }, 352 + size_distribution: SizeDistribution::Fixed(ValueBytes(128)), 353 + collections: vec![CollectionName("app.bsky.feed.post".to_string())], 354 + key_space: KeySpaceSize(500), 355 + did_space: DidSpaceSize(32), 356 + retention_max_secs: RetentionMaxSecs(3600), 357 + }, 358 + op_count: OpCount(1_000), 359 + invariants: InvariantSet::REFCOUNT_CONSERVATION 360 + | InvariantSet::REACHABILITY 361 + | InvariantSet::ACKED_WRITE_PERSISTENCE 362 + | InvariantSet::READ_AFTER_WRITE 363 + | InvariantSet::RESTART_IDEMPOTENT, 364 + limits: RunLimits { 365 + max_wall_ms: Some(WallMs(60_000)), 366 + }, 367 + restart_policy: RestartPolicy::EveryNOps(OpInterval(250)), 368 + store: StoreConfig { 369 + max_file_size: MaxFileSize(16 * 1024), 370 + group_commit: GroupCommitConfig::default(), 371 + shard_count: ShardCount(1), 372 + }, 373 + eventlog: None, 374 + writer_concurrency: WriterConcurrency(8), 375 + }; 376 + let report = Gauntlet::new(cfg).expect("build gauntlet").run().await; 377 + assert_clean(&report); 378 + assert_eq!(report.ops_executed.0, 1_000); 379 + assert!(report.restarts.0 >= 2); 380 + } 381 + 382 + #[tokio::test] 383 + async fn report_carries_generated_ops_when_clean() { 384 + let cfg = fast_sanity_config(Seed(5)); 385 + let expected_len = cfg.op_count.0; 386 + let report = Gauntlet::new(cfg).expect("build gauntlet").run().await; 387 + assert_clean(&report); 388 + assert_eq!( 389 + report.ops.len(), 390 + expected_len, 391 + "clean report missing op stream" 392 + ); 393 + } 394 + 395 + #[tokio::test] 396 + async fn regression_round_trip_replays_injected_ops() { 397 + let overrides = ConfigOverrides { 398 + op_count: Some(25), 399 + store: StoreOverrides { 400 + max_file_size: Some(8192), 401 + ..StoreOverrides::default() 402 + }, 403 + ..ConfigOverrides::default() 404 + }; 405 + let mut cfg = config_for(Scenario::SmokePR, Seed(99)); 406 + overrides.apply_to(&mut cfg); 407 + 408 + let original_report = Gauntlet::new(cfg.clone()) 409 + .expect("build gauntlet") 410 + .run() 411 + .await; 412 + let captured_ops = original_report.ops.clone(); 413 + assert_eq!( 414 + captured_ops.len(), 415 + 25, 416 + "captured op stream must match op_count override" 417 + ); 418 + 419 + let dir = tempfile::TempDir::new().unwrap(); 420 + let record = RegressionRecord::from_report( 421 + Scenario::SmokePR, 422 + overrides.clone(), 423 + &original_report, 424 + captured_ops.len(), 425 + captured_ops.clone(), 426 + ); 427 + let written = record.write_to(dir.path()).expect("write regression"); 428 + let loaded = RegressionRecord::load(&written).expect("load regression"); 429 + assert_eq!(loaded.overrides, overrides); 430 + assert_eq!(loaded.ops.len(), captured_ops.len()); 431 + 432 + let rebuilt = loaded.build_config().expect("rebuild config"); 433 + assert_eq!(rebuilt.op_count.0, 25); 434 + assert_eq!(rebuilt.store.max_file_size.0, 8192); 435 + 436 + let replay = Gauntlet::new(rebuilt) 437 + .expect("build gauntlet") 438 + .run_with_ops(loaded.op_stream()) 439 + .await; 440 + assert_eq!( 441 + replay.violations.len(), 442 + original_report.violations.len(), 443 + "replay from regression must produce same violation count", 444 + ); 445 + let original_inv: Vec<&'static str> = original_report 446 + .violations 447 + .iter() 448 + .map(|v| v.invariant) 449 + .collect(); 450 + let replay_inv: Vec<&'static str> = replay.violations.iter().map(|v| v.invariant).collect(); 451 + assert_eq!(original_inv, replay_inv); 452 + assert_eq!(replay.ops.len(), captured_ops.len()); 453 + } 454 + 455 + #[tokio::test] 114 456 #[ignore = "long running, 100k ops with around 20 restarts"] 115 457 async fn mst_restart_churn_single_seed() { 116 458 let cfg = config_for(Scenario::MstRestartChurn, Seed(42)); 117 459 let report = Gauntlet::new(cfg).expect("build gauntlet").run().await; 118 - assert!( 119 - report.is_clean(), 120 - "violations: {:?}", 121 - report 122 - .violations 123 - .iter() 124 - .map(|v| format!("{}: {}", v.invariant, v.detail)) 125 - .collect::<Vec<_>>() 126 - ); 127 - assert!( 128 - report.restarts.0 >= 1, 129 - "PoissonByOps(5000) over 100k ops should fire at least 1 restart, got {}", 130 - report.restarts.0 131 - ); 460 + assert_clean(&report); 461 + assert!(report.restarts.0 >= 1); 132 462 }
+3 -3
crates/tranquil-store/tests/proptests.rs
··· 2 2 use std::path::Path; 3 3 4 4 use tranquil_store::{ 5 - FaultConfig, HEADER_SIZE, OpenOptions, ReadRecord, RecordReader, RecordWriter, SimulatedIO, 6 - StorageIO, run_crash_test, run_pristine_comparison, sim_proptest_cases, 5 + FaultConfig, HEADER_SIZE, OpenOptions, Probability, ReadRecord, RecordReader, RecordWriter, 6 + SimulatedIO, StorageIO, run_crash_test, run_pristine_comparison, sim_proptest_cases, 7 7 }; 8 8 9 9 fn arb_payloads(max_count: usize, max_size: usize) -> BoxedStrategy<Vec<Vec<u8>>> { ··· 151 151 data in proptest::collection::vec(any::<u8>(), 64..4096), 152 152 ) { 153 153 let config = FaultConfig { 154 - partial_write_probability: 0.5, 154 + partial_write_probability: Probability::new(0.5), 155 155 ..FaultConfig::none() 156 156 }; 157 157 let dir = Path::new("/test");
+11 -12
crates/tranquil-store/tests/sim_eventlog.rs
··· 8 8 DidHash, EVENT_RECORD_OVERHEAD, EventLogWriter, EventSequence, EventTypeTag, MAX_EVENT_PAYLOAD, 9 9 SEGMENT_HEADER_SIZE, SegmentId, SegmentManager, SegmentReader, ValidEvent, 10 10 }; 11 - use tranquil_store::{FaultConfig, SimulatedIO, StorageIO, sim_seed_range}; 11 + use tranquil_store::{FaultConfig, Probability, SimulatedIO, StorageIO, sim_seed_range}; 12 12 13 13 use common::Rng; 14 14 ··· 529 529 fn group_sync_crash_mid_sync_partial_fsync() { 530 530 sim_seed_range().into_par_iter().for_each(|seed| { 531 531 let fault_config = FaultConfig { 532 - sync_failure_probability: 0.3, 533 - partial_write_probability: 0.1, 532 + sync_failure_probability: Probability::new(0.3), 533 + partial_write_probability: Probability::new(0.1), 534 534 ..FaultConfig::none() 535 535 }; 536 536 let sim = SimulatedIO::new(seed, fault_config); ··· 682 682 fn group_sync_contention_under_faults() { 683 683 sim_seed_range().into_par_iter().for_each(|seed| { 684 684 let fault_config = FaultConfig { 685 - partial_write_probability: 0.05, 686 - sync_failure_probability: 0.10, 687 - dir_sync_failure_probability: 0.05, 685 + partial_write_probability: Probability::new(0.05), 686 + sync_failure_probability: Probability::new(0.10), 687 + dir_sync_failure_probability: Probability::new(0.05), 688 688 ..FaultConfig::none() 689 689 }; 690 690 let sim = SimulatedIO::new(seed, fault_config); ··· 897 897 #[test] 898 898 fn aggressive_faults_group_sync_recovery() { 899 899 let fault_config = FaultConfig { 900 - partial_write_probability: 0.15, 901 - sync_failure_probability: 0.10, 902 - dir_sync_failure_probability: 0.05, 903 - misdirected_write_probability: 0.05, 904 - bit_flip_on_read_probability: 0.0, 905 - io_error_probability: 0.0, 900 + partial_write_probability: Probability::new(0.15), 901 + sync_failure_probability: Probability::new(0.10), 902 + dir_sync_failure_probability: Probability::new(0.05), 903 + misdirected_write_probability: Probability::new(0.05), 904 + ..FaultConfig::none() 906 905 }; 907 906 908 907 sim_seed_range().into_par_iter().for_each(|seed| {