Our Personal Data Server from scratch!
0

Configure Feed

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

fix(store): torn hint-file tail should be recoverable on reopen

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

author
Lewis
date (May 31, 2026, 11:37 AM +0300) commit 31ee12ec parent ea106d52 change-id wzvuuwxv
+245 -58
+3 -1
crates/tranquil-store/Cargo.toml
··· 38 38 clap = { workspace = true, optional = true } 39 39 toml = { version = "0.8", optional = true } 40 40 tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true } 41 + tikv-jemallocator = { version = "0.6", optional = true } 41 42 libc = "0.2" 42 43 43 44 [features] 44 45 test-harness = ["dep:tempfile"] 45 - gauntlet-cli = ["test-harness", "dep:clap", "dep:toml", "dep:tracing-subscriber"] 46 + jemalloc = ["dep:tikv-jemallocator"] 47 + gauntlet-cli = ["test-harness", "dep:clap", "dep:toml", "dep:tracing-subscriber", "jemalloc"] 46 48 gauntlet-jemalloc-prof = [] 47 49 48 50 [[bin]]
+4
crates/tranquil-store/benches/blockstore.rs
··· 15 15 }; 16 16 use tranquil_store::{RealIO, SystemClock}; 17 17 18 + #[cfg(feature = "jemalloc")] 19 + #[global_allocator] 20 + static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; 21 + 18 22 const DAG_CBOR_CODEC: u64 = 0x71; 19 23 const SHA2_256_CODE: u64 = 0x12; 20 24
+70 -37
crates/tranquil-store/src/bin/tranquil_gauntlet.rs
··· 14 14 shrink::{DEFAULT_MAX_SHRINK_ITERATIONS, shrink_failure}, 15 15 }; 16 16 17 + #[cfg(feature = "jemalloc")] 18 + #[global_allocator] 19 + static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; 20 + 17 21 const MAX_HOURS: f64 = 1.0e6; 18 22 const DEFAULT_SWEEP_RUN_CAP: u64 = 10_000; 19 23 ··· 223 227 commit_batch_size: Vec<usize>, 224 228 #[serde(default)] 225 229 max_file_size: Vec<u64>, 230 + #[serde(default)] 231 + advance_time: Vec<u32>, 232 + #[serde(default)] 233 + advance_max_secs: Vec<u32>, 226 234 } 227 235 228 236 #[derive(Debug, Clone, Copy, Default)] ··· 235 243 restart_every_n_ops: Option<usize>, 236 244 commit_batch_size: Option<usize>, 237 245 max_file_size: Option<u64>, 246 + advance_time: Option<u32>, 247 + advance_max_secs: Option<u32>, 238 248 } 239 249 240 250 impl SweepAxisValues { ··· 263 273 if let Some(v) = self.max_file_size { 264 274 o.store.max_file_size = Some(v); 265 275 } 276 + if let Some(v) = self.advance_time { 277 + o.advance_time = Some(v); 278 + } 279 + if let Some(v) = self.advance_max_secs { 280 + o.advance_max_secs = Some(v); 281 + } 266 282 } 267 283 } 268 284 269 285 impl SweepAxes { 270 286 fn axis_values(&self) -> Vec<SweepAxisValues> { 271 - expand(&self.writer_concurrency) 272 - .into_iter() 273 - .flat_map(|wc| { 274 - expand(&self.key_space).into_iter().flat_map(move |ks| { 275 - expand(&self.value_bytes).into_iter().flat_map(move |vb| { 276 - expand(&self.fault_density_scale) 277 - .into_iter() 278 - .flat_map(move |fds| { 279 - expand(&self.fault_density_uniform).into_iter().flat_map( 280 - move |fdu| { 281 - expand(&self.restart_every_n_ops).into_iter().flat_map( 282 - move |rc| { 283 - expand(&self.commit_batch_size) 284 - .into_iter() 285 - .flat_map(move |cb| { 286 - expand(&self.max_file_size).into_iter().map( 287 - move |mfs| SweepAxisValues { 288 - writer_concurrency: wc, 289 - key_space: ks, 290 - value_bytes: vb, 291 - fault_density_scale: fds, 292 - fault_density_uniform: fdu, 293 - restart_every_n_ops: rc, 294 - commit_batch_size: cb, 295 - max_file_size: mfs, 296 - }, 297 - ) 298 - }) 299 - }, 300 - ) 301 - }, 302 - ) 303 - }) 304 - }) 305 - }) 287 + let base = vec![SweepAxisValues::default()]; 288 + let base = cross(base, &self.writer_concurrency, |a, v| a.writer_concurrency = Some(v)); 289 + let base = cross(base, &self.key_space, |a, v| a.key_space = Some(v)); 290 + let base = cross(base, &self.value_bytes, |a, v| a.value_bytes = Some(v)); 291 + let base = cross(base, &self.fault_density_scale, |a, v| { 292 + a.fault_density_scale = Some(v) 293 + }); 294 + let base = cross(base, &self.fault_density_uniform, |a, v| { 295 + a.fault_density_uniform = Some(v) 296 + }); 297 + let base = cross(base, &self.restart_every_n_ops, |a, v| { 298 + a.restart_every_n_ops = Some(v) 299 + }); 300 + let base = cross(base, &self.commit_batch_size, |a, v| { 301 + a.commit_batch_size = Some(v) 302 + }); 303 + let base = cross(base, &self.max_file_size, |a, v| a.max_file_size = Some(v)); 304 + let base = cross(base, &self.advance_time, |a, v| a.advance_time = Some(v)); 305 + cross(base, &self.advance_max_secs, |a, v| { 306 + a.advance_max_secs = Some(v) 307 + }) 308 + } 309 + } 310 + 311 + fn cross<T: Copy>( 312 + acc: Vec<SweepAxisValues>, 313 + values: &[T], 314 + set: impl Fn(&mut SweepAxisValues, T) + Copy, 315 + ) -> Vec<SweepAxisValues> { 316 + acc.into_iter() 317 + .flat_map(|base| { 318 + expand(values).into_iter().map(move |opt| { 319 + let mut next = base; 320 + if let Some(v) = opt { 321 + set(&mut next, v); 322 + } 323 + next 306 324 }) 307 - .collect() 308 - } 325 + }) 326 + .collect() 309 327 } 310 328 311 329 fn expand<T: Copy>(values: &[T]) -> Vec<Option<T>> { ··· 613 631 flag 614 632 } 615 633 634 + fn raise_fd_limit() { 635 + let mut lim = libc::rlimit { 636 + rlim_cur: 0, 637 + rlim_max: 0, 638 + }; 639 + let read = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut lim) } == 0; 640 + if read && lim.rlim_cur < lim.rlim_max { 641 + lim.rlim_cur = lim.rlim_max; 642 + unsafe { 643 + let _ = libc::setrlimit(libc::RLIMIT_NOFILE, &lim); 644 + } 645 + } 646 + } 647 + 616 648 fn main() -> ExitCode { 649 + raise_fd_limit(); 617 650 let _ = tracing_subscriber::fmt() 618 651 .with_env_filter( 619 652 tracing_subscriber::EnvFilter::try_from_default_env()
+7 -2
crates/tranquil-store/src/blockstore/group_commit.rs
··· 14 14 15 15 use super::data_file::{CID_SIZE, DataFileWriter, ReadBlockRecord, decode_block_record}; 16 16 use super::hash_index::{BlockIndex, BlockIndexError, CheckpointPositions}; 17 - use super::hint::{HintFileWriter, hint_file_path}; 17 + use super::hint::{HINT_RECORD_SIZE, HintFileWriter, hint_file_path}; 18 18 use super::manager::DataFileManager; 19 19 use super::types::{ 20 20 BlockLocation, BlockOffset, BlockstoreSnapshot, CommitEpoch, DataFileId, EpochCounter, ··· 605 605 let hint_path = hint_file_path(data_dir, wc.file_id); 606 606 let hint_fd = manager.io().open(&hint_path, OpenOptions::read_write())?; 607 607 let hint_size = manager.io().file_size(hint_fd)?; 608 + let aligned_hint = hint_size - hint_size % HINT_RECORD_SIZE as u64; 609 + if aligned_hint != hint_size { 610 + manager.io().truncate(hint_fd, aligned_hint)?; 611 + manager.io().sync(hint_fd)?; 612 + } 608 613 609 614 Ok(ActiveState { 610 615 file_id: wc.file_id, 611 616 fd, 612 617 position, 613 618 hint_fd, 614 - hint_position: HintOffset::new(hint_size), 619 + hint_position: HintOffset::new(aligned_hint), 615 620 }) 616 621 } 617 622 None => {
+19 -6
crates/tranquil-store/src/blockstore/hint.rs
··· 469 469 } 470 470 471 471 pub fn resume(io: &'a S, fd: FileId, position: HintOffset) -> io::Result<Self> { 472 - assert!( 473 - position.raw().is_multiple_of(HINT_RECORD_SIZE as u64), 474 - "hint resume position {} not aligned to HINT_RECORD_SIZE {}", 475 - position.raw(), 476 - HINT_RECORD_SIZE, 477 - ); 472 + if !position.raw().is_multiple_of(HINT_RECORD_SIZE as u64) { 473 + return Err(io::Error::new( 474 + io::ErrorKind::InvalidData, 475 + format!( 476 + "hint resume position {} not aligned to HINT_RECORD_SIZE {}", 477 + position.raw(), 478 + HINT_RECORD_SIZE, 479 + ), 480 + )); 481 + } 478 482 let file_size = io.file_size(fd)?; 479 483 Ok(Self { 480 484 io, ··· 1198 1202 }) 1199 1203 .count(); 1200 1204 assert_eq!(valid_count, 2); 1205 + } 1206 + 1207 + #[test] 1208 + fn hint_reader_resume_rejects_unaligned_position_without_panicking() { 1209 + let (sim, fd) = setup(); 1210 + match HintFileReader::resume(&sim, fd, HintOffset::new(HINT_RECORD_SIZE as u64 + 5)) { 1211 + Err(e) => assert_eq!(e.kind(), io::ErrorKind::InvalidData), 1212 + Ok(_) => panic!("non-aligned resume position must surface as a recoverable error"), 1213 + } 1201 1214 } 1202 1215 1203 1216 #[test]
+80
crates/tranquil-store/src/blockstore/store.rs
··· 954 954 ); 955 955 } 956 956 957 + #[test] 958 + fn reopen_recovers_from_torn_hint_tail_without_aborting() { 959 + use crate::blockstore::HINT_RECORD_SIZE; 960 + use std::io::Write; 961 + 962 + let tmp = tempfile::TempDir::new().unwrap(); 963 + let data_dir = tmp.path().join("data"); 964 + let index_dir = tmp.path().join("index"); 965 + let cfg = || BlockStoreConfig::new(data_dir.clone(), index_dir.clone()); 966 + 967 + let payload_a = b"alpha-block-payload".to_vec(); 968 + let payload_b = b"bravo-block-payload".to_vec(); 969 + let cid_a = crate::blockstore::hash_to_cid_bytes(&payload_a); 970 + let cid_b = crate::blockstore::hash_to_cid_bytes(&payload_b); 971 + 972 + { 973 + let store = TranquilBlockStore::open(cfg()).unwrap(); 974 + store 975 + .apply_commit_blocking(vec![(cid_a, payload_a.clone())], vec![]) 976 + .unwrap(); 977 + store.apply_commit_blocking(vec![], vec![]).unwrap(); 978 + } 979 + 980 + let hint_path = std::fs::read_dir(&data_dir) 981 + .unwrap() 982 + .filter_map(|e| e.ok().map(|e| e.path())) 983 + .find(|p| p.extension().and_then(|s| s.to_str()) == Some("tqh")) 984 + .expect("active hint file must exist after commit"); 985 + 986 + let clean = std::fs::metadata(&hint_path).unwrap().len(); 987 + assert_eq!( 988 + clean % HINT_RECORD_SIZE as u64, 989 + 0, 990 + "precondition: synced hint file must be record-aligned, got {clean}" 991 + ); 992 + { 993 + let mut f = std::fs::OpenOptions::new() 994 + .append(true) 995 + .open(&hint_path) 996 + .unwrap(); 997 + f.write_all(&[0x5Au8; 50]).unwrap(); 998 + f.sync_all().unwrap(); 999 + } 1000 + assert_eq!( 1001 + std::fs::metadata(&hint_path).unwrap().len() % HINT_RECORD_SIZE as u64, 1002 + 50, 1003 + "torn tail must leave a non-aligned hint file" 1004 + ); 1005 + 1006 + { 1007 + let store = TranquilBlockStore::open(cfg()).unwrap(); 1008 + assert!( 1009 + store.get_block_sync(&cid_a).unwrap().is_some(), 1010 + "block A lost after torn-tail recovery" 1011 + ); 1012 + store 1013 + .apply_commit_blocking(vec![(cid_b, payload_b.clone())], vec![]) 1014 + .unwrap(); 1015 + } 1016 + 1017 + let healed = std::fs::metadata(&hint_path).unwrap().len(); 1018 + assert_eq!( 1019 + healed % HINT_RECORD_SIZE as u64, 1020 + 0, 1021 + "recovery must realign the hint file, got {healed}" 1022 + ); 1023 + 1024 + { 1025 + let store = TranquilBlockStore::open(cfg()).unwrap(); 1026 + assert!( 1027 + store.get_block_sync(&cid_a).unwrap().is_some(), 1028 + "block A lost across second reopen" 1029 + ); 1030 + assert!( 1031 + store.get_block_sync(&cid_b).unwrap().is_some(), 1032 + "block B lost across second reopen" 1033 + ); 1034 + } 1035 + } 1036 + 957 1037 fn instant_policy(max_attempts: u8) -> OpenRetryPolicy { 958 1038 OpenRetryPolicy { 959 1039 max_attempts: NonZeroU8::new(max_attempts).expect("max_attempts must be nonzero"),
+27 -1
crates/tranquil-store/src/gauntlet/overrides.rs
··· 4 4 GauntletConfig, IoBackend, MaxFileSize, OpInterval, RestartPolicy, RunLimits, ShardCount, 5 5 WallMs, WriterConcurrency, 6 6 }; 7 - use super::workload::{KeySpaceSize, OpCount, SizeDistribution, ValueBytes}; 7 + use super::workload::{AdvanceMaxSecs, KeySpaceSize, OpCount, SizeDistribution, ValueBytes}; 8 8 use crate::sim::FaultConfig; 9 9 10 10 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] ··· 26 26 pub fault_density_uniform: Option<f64>, 27 27 #[serde(default, skip_serializing_if = "Option::is_none")] 28 28 pub restart_every_n_ops: Option<usize>, 29 + #[serde(default, skip_serializing_if = "Option::is_none")] 30 + pub advance_time: Option<u32>, 31 + #[serde(default, skip_serializing_if = "Option::is_none")] 32 + pub advance_max_secs: Option<u32>, 29 33 #[serde(default, skip_serializing_if = "StoreOverrides::is_empty")] 30 34 pub store: StoreOverrides, 31 35 } ··· 110 114 RestartPolicy::EveryNOps(OpInterval(n)) 111 115 }; 112 116 } 117 + if let Some(n) = self.advance_time { 118 + cfg.workload.weights.advance_time = n; 119 + } 120 + if let Some(n) = self.advance_max_secs { 121 + cfg.workload.advance_max_secs = AdvanceMaxSecs(n.max(1)); 122 + } 113 123 if let Some(n) = self.store.max_file_size { 114 124 cfg.store.max_file_size = MaxFileSize(n); 115 125 } ··· 223 233 }; 224 234 o.apply_to(&mut cfg); 225 235 assert!(matches!(cfg.io, IoBackend::Real)); 236 + } 237 + 238 + #[test] 239 + fn advance_time_overrides_inject_time_travel() { 240 + use crate::gauntlet::op::Seed; 241 + use crate::gauntlet::scenarios::{Scenario, config_for}; 242 + let mut cfg = config_for(Scenario::FirehoseFanout, Seed(1)); 243 + assert_eq!(cfg.workload.weights.advance_time, 0); 244 + let o = ConfigOverrides { 245 + advance_time: Some(40), 246 + advance_max_secs: Some(1_209_600), 247 + ..ConfigOverrides::default() 248 + }; 249 + o.apply_to(&mut cfg); 250 + assert_eq!(cfg.workload.weights.advance_time, 40); 251 + assert_eq!(cfg.workload.advance_max_secs.0, 1_209_600); 226 252 } 227 253 228 254 #[test]
+26 -8
crates/tranquil-store/src/gauntlet/runner.rs
··· 397 397 } 398 398 }; 399 399 if config.writer_concurrency.0 > 1 { 400 - run_inner_generic_concurrent::<RealIO, SystemClock, _, _>( 400 + run_inner_generic_concurrent::<RealIO, SystemClock, _, _, _>( 401 401 config, 402 402 ops, 403 403 ops_counter, ··· 408 408 tolerate_op_errors, 409 409 reopen_backoff, 410 410 SystemClock, 411 + |_| {}, 411 412 ) 412 413 .await 413 414 } else { 414 - run_inner_generic::<RealIO, SystemClock, _, _>( 415 + run_inner_generic::<RealIO, SystemClock, _, _, _>( 415 416 config, 416 417 ops, 417 418 ops_counter, ··· 422 423 tolerate_op_errors, 423 424 reopen_backoff, 424 425 SystemClock, 426 + |_| {}, 425 427 ) 426 428 .await 427 429 } ··· 473 475 }; 474 476 let sim_for_crash = Arc::clone(&sim); 475 477 let crash = move || sim_for_crash.crash(); 478 + let sim_for_quiesce = Arc::clone(&sim); 479 + let quiesce = move |on: bool| sim_for_quiesce.set_pristine_mode(on); 476 480 if config.writer_concurrency.0 > 1 { 477 - run_inner_generic_concurrent::<Arc<SimulatedIO>, SimClock, _, _>( 481 + run_inner_generic_concurrent::<Arc<SimulatedIO>, SimClock, _, _, _>( 478 482 config, 479 483 ops, 480 484 ops_counter, ··· 485 489 tolerate_errors, 486 490 Duration::ZERO, 487 491 clock, 492 + quiesce, 488 493 ) 489 494 .await 490 495 } else { 491 - run_inner_generic::<Arc<SimulatedIO>, SimClock, _, _>( 496 + run_inner_generic::<Arc<SimulatedIO>, SimClock, _, _, _>( 492 497 config, 493 498 ops, 494 499 ops_counter, ··· 499 504 tolerate_errors, 500 505 Duration::ZERO, 501 506 clock, 507 + quiesce, 502 508 ) 503 509 .await 504 510 } ··· 528 534 } 529 535 530 536 #[allow(clippy::too_many_arguments)] 531 - async fn run_inner_generic<S, C, Open, Crash>( 537 + async fn run_inner_generic<S, C, Open, Crash, Quiesce>( 532 538 config: GauntletConfig, 533 539 op_stream: OpStream, 534 540 ops_counter: Arc<AtomicUsize>, ··· 539 545 tolerate_op_errors: bool, 540 546 reopen_backoff: Duration, 541 547 clock: C, 548 + quiesce_faults: Quiesce, 542 549 ) -> GauntletReport 543 550 where 544 551 S: StorageIO + Send + Sync + 'static, 545 552 C: Clock, 546 553 Open: FnMut(usize) -> Result<Harness<S, C>, String>, 547 554 Crash: FnMut(), 555 + Quiesce: Fn(bool), 548 556 { 549 557 let mut oracle = Oracle::new(); 550 558 let mut violations: Vec<InvariantViolation> = Vec::new(); ··· 617 625 let n = restarts_counter.fetch_add(1, Ordering::Relaxed) + 1; 618 626 let live = harness.as_ref().expect("just reopened"); 619 627 let before = violations.len(); 628 + quiesce_faults(true); 620 629 violations.extend( 621 630 run_quick_check( 622 631 &live.store, ··· 628 637 ) 629 638 .await, 630 639 ); 640 + quiesce_faults(false); 631 641 if violations.len() > before { 632 642 halt_ops = true; 633 643 } ··· 643 653 } 644 654 } 645 655 656 + quiesce_faults(true); 646 657 if !halt_ops && tolerate_op_errors && harness.is_some() { 647 658 crash(); 648 659 oracle.record_crash(); ··· 1660 1671 } 1661 1672 1662 1673 #[allow(clippy::too_many_arguments)] 1663 - async fn run_inner_generic_concurrent<S, C, Open, Crash>( 1674 + async fn run_inner_generic_concurrent<S, C, Open, Crash, Quiesce>( 1664 1675 config: GauntletConfig, 1665 1676 op_stream: OpStream, 1666 1677 ops_counter: Arc<AtomicUsize>, ··· 1671 1682 tolerate_op_errors: bool, 1672 1683 reopen_backoff: Duration, 1673 1684 clock: C, 1685 + quiesce_faults: Quiesce, 1674 1686 ) -> GauntletReport 1675 1687 where 1676 1688 S: StorageIO + Send + Sync + 'static, 1677 1689 C: Clock, 1678 1690 Open: FnMut(usize) -> Result<Harness<S, C>, String>, 1679 1691 Crash: FnMut(), 1692 + Quiesce: Fn(bool), 1680 1693 { 1681 1694 let ops: Vec<Op> = op_stream.into_vec(); 1682 1695 let total_ops = ops.len(); ··· 1806 1819 let n = restarts_counter.fetch_add(1, Ordering::Relaxed) + 1; 1807 1820 let live = harness.as_ref().expect("just reopened"); 1808 1821 let before = violations.len(); 1822 + quiesce_faults(true); 1809 1823 violations.extend( 1810 1824 run_quick_check( 1811 1825 &live.store, ··· 1817 1831 ) 1818 1832 .await, 1819 1833 ); 1834 + quiesce_faults(false); 1820 1835 if violations.len() > before { 1821 1836 halt_ops = true; 1822 1837 } ··· 1833 1848 } 1834 1849 } 1835 1850 1851 + quiesce_faults(true); 1836 1852 if !halt_ops && tolerate_op_errors && harness.is_some() { 1837 1853 crash(); 1838 1854 oracle.record_crash(); ··· 1978 1994 let clock = sim.clock(); 1979 1995 let attempts = Arc::new(AtomicUsize::new(0)); 1980 1996 1981 - let report = run_inner_generic::<Arc<SimulatedIO>, SimClock, _, _>( 1997 + let report = run_inner_generic::<Arc<SimulatedIO>, SimClock, _, _, _>( 1982 1998 cfg, 1983 1999 OpStream::empty(), 1984 2000 Arc::new(AtomicUsize::new(0)), ··· 1994 2010 true, 1995 2011 Duration::ZERO, 1996 2012 clock, 2013 + |_| {}, 1997 2014 ) 1998 2015 .await; 1999 2016 ··· 2023 2040 let clock = sim.clock(); 2024 2041 let attempts = Arc::new(AtomicUsize::new(0)); 2025 2042 2026 - let report = run_inner_generic_concurrent::<Arc<SimulatedIO>, SimClock, _, _>( 2043 + let report = run_inner_generic_concurrent::<Arc<SimulatedIO>, SimClock, _, _, _>( 2027 2044 cfg, 2028 2045 OpStream::empty(), 2029 2046 Arc::new(AtomicUsize::new(0)), ··· 2039 2056 true, 2040 2057 Duration::ZERO, 2041 2058 clock, 2059 + |_| {}, 2042 2060 ) 2043 2061 .await; 2044 2062
+9 -3
crates/tranquil-store/src/sim.rs
··· 376 376 self.pristine_mode.store(on, Ordering::Relaxed); 377 377 } 378 378 379 + pub fn pristine_mode(&self) -> bool { 380 + self.pristine_mode.load(Ordering::Relaxed) 381 + } 382 + 379 383 fn jitter(&self) { 380 384 let max_ns = self.effective_fault_config().latency_distribution_ns.0; 381 385 let extra_ns = match max_ns { ··· 458 462 459 463 pub struct PristineGuard { 460 464 sim: Arc<SimulatedIO>, 465 + prev: bool, 461 466 } 462 467 463 468 impl PristineGuard { 464 469 pub fn new(sim: Arc<SimulatedIO>, on: bool) -> Self { 465 - sim.set_pristine_mode(on); 466 - Self { sim } 470 + let prev = sim.pristine_mode(); 471 + sim.set_pristine_mode(on || prev); 472 + Self { sim, prev } 467 473 } 468 474 } 469 475 470 476 impl Drop for PristineGuard { 471 477 fn drop(&mut self) { 472 - self.sim.set_pristine_mode(false); 478 + self.sim.set_pristine_mode(self.prev); 473 479 } 474 480 } 475 481