diff --git a/libraries/persistent_store/src/driver.rs b/libraries/persistent_store/src/driver.rs index 3f02ab5..143f5f7 100644 --- a/libraries/persistent_store/src/driver.rs +++ b/libraries/persistent_store/src/driver.rs @@ -453,6 +453,12 @@ impl StoreDriverOn { self.apply(StoreOperation::Transaction { updates }) } + /// Applies a clear operation to the store and model without interruption. + #[cfg(feature = "std")] + pub fn clear(&mut self, min_key: usize) -> Result<(), StoreInvariant> { + self.apply(StoreOperation::Clear { min_key }) + } + /// Checks that the store and model are in sync. pub fn check(&self) -> Result<(), StoreInvariant> { self.recover_check(&[]) @@ -610,4 +616,12 @@ impl<'a> StoreInterruption<'a> { corrupt: Box::new(|_, _| {}), } } + + /// Builds an interruption without corruption. + pub fn pure(delay: usize) -> StoreInterruption<'a> { + StoreInterruption { + delay, + corrupt: Box::new(|_, _| {}), + } + } } diff --git a/libraries/persistent_store/tests/store.rs b/libraries/persistent_store/tests/store.rs index 8ec8e9b..7bdf5ab 100644 --- a/libraries/persistent_store/tests/store.rs +++ b/libraries/persistent_store/tests/store.rs @@ -1,7 +1,10 @@ -use persistent_store::{BufferOptions, StoreDriverOff, StoreInterruption, StoreOperation}; +use persistent_store::{ + BufferOptions, StoreDriverOff, StoreDriverOn, StoreInterruption, StoreOperation, +}; #[test] fn interrupted_overflowing_compaction() { + let num_pages = 7; let options = BufferOptions { word_size: 4, page_size: 32, @@ -9,7 +12,6 @@ fn interrupted_overflowing_compaction() { max_page_erases: 3, strict_mode: true, }; - let num_pages = 7; let mut driver = StoreDriverOff::new(options, num_pages).power_on().unwrap(); let v = driver.model().format().virt_size() as usize; let c = driver.model().format().total_capacity() as usize; @@ -39,13 +41,81 @@ fn interrupted_overflowing_compaction() { // We trigger 2 interrupted overflowing compactions, which would move the last non-deleted entry // out of the window unless additional compactions are done to restore the overflow. for _ in 0..2 { - let interruption = StoreInterruption { - delay: 0, - corrupt: Box::new(|old, new| old.copy_from_slice(new)), - }; - match driver.partial_apply(StoreOperation::Prepare { length: 1 }, interruption) { + match driver.partial_apply( + StoreOperation::Prepare { length: 1 }, + StoreInterruption::pure(1), + ) { Ok((None, d)) => driver = d.power_on().unwrap(), _ => unreachable!(), } } } + +#[test] +fn full_compaction_with_max_prefix() { + let num_pages = 7; + let options = BufferOptions { + word_size: 4, + page_size: 32, + max_word_writes: 2, + max_page_erases: 3, + strict_mode: true, + }; + let mut driver = StoreDriverOff::new(options, num_pages).power_on().unwrap(); + let max_key = driver.model().format().max_key() as usize; + let max_value_len = driver.model().format().max_value_len() as usize; + let n = driver.model().format().num_pages() as usize; + let v = driver.model().format().virt_size() as usize; + let c = driver.model().format().total_capacity() as usize; + let q = driver.model().format().virt_page_size() as usize; + let m = driver.model().format().max_prefix_len() as usize; + let get_tail = |driver: &StoreDriverOn| driver.store().lifetime().unwrap().used(); + let mut last_tail = 0; + let mut check_lifetime = |driver: &StoreDriverOn, used| { + last_tail += used; + assert_eq!(get_tail(driver), last_tail); + }; + + // We fill the first page with q + m words of padding. In particular, the last entry overlaps + // the next page with m words. + for _ in 0..q - 1 { + driver.clear(max_key).unwrap(); + } + driver.insert(0, &vec![0; max_value_len]).unwrap(); + driver.remove(0).unwrap(); + check_lifetime(&driver, q + m); + + // We fill the store with non-deleted entries making sure the last entry always overlaps the + // next page with m words for the first 3 pages. + let mut k = 0; + for _ in 0..c { + let tail = get_tail(&driver); + if tail % q == q - 1 && tail < 4 * q { + driver.insert(k, &vec![0; max_value_len]).unwrap(); + } else { + driver.insert(k, &[]).unwrap(); + } + k += 1; + } + // We lost 1 word of lifetime because of compacting the first page. + check_lifetime(&driver, c + 1); + + // We fill the store with padding until compaction would trigger. + for _ in 0..n - 1 { + driver.clear(max_key).unwrap(); + } + check_lifetime(&driver, n - 1); + assert_eq!(get_tail(&driver), q + m + v); + + // We interrupt all compactions to check the invariant between each compaction. + for _ in 0..n - 1 { + match driver.partial_apply( + StoreOperation::Clear { min_key: max_key }, + StoreInterruption::pure(1), + ) { + Ok((None, d)) => driver = d.power_on().unwrap(), + _ => unreachable!(), + } + } + check_lifetime(&mut driver, c + n - 1); +}