diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7f63bdb..80a78c5 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -34,3 +34,5 @@ jobs: run: cargo -Z unstable-options -C candy-crasher run --release - name: Run longliving run: cargo -Z unstable-options -C candy-longliving run --release -- 10 40001 10000 + - name: Run mini-candy + run: cargo -Z unstable-options -C mini-candy run diff --git a/src/insertion.rs b/src/insertion.rs index fdd1705..851a8b7 100644 --- a/src/insertion.rs +++ b/src/insertion.rs @@ -10,41 +10,22 @@ use crate::{CandyError, Result, MAX_TOTAL_KEY_SIZE, MAX_VALUE_SIZE}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum ReplaceStatus { PrevValue(Vec), + WrongValue(Vec), DoesNotExist, } impl ReplaceStatus { pub fn was_replaced(&self) -> bool { matches!(*self, Self::PrevValue(_)) } - pub fn is_key_missing(&self) -> bool { - matches!(*self, Self::DoesNotExist) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ModifyStatus { - PrevValue(Vec), - DoesNotExist, - WrongLength(usize, usize), - ValueTooLong(usize, usize, usize), - ValueMismatch(Vec), -} -impl ModifyStatus { - pub fn was_replaced(&self) -> bool { - matches!(*self, Self::PrevValue(_)) - } - pub fn is_mismatch(&self) -> bool { - matches!(*self, Self::ValueMismatch(_)) - } - pub fn is_too_long(&self) -> bool { - matches!(*self, Self::ValueTooLong(_, _, _)) - } - pub fn is_wrong_length(&self) -> bool { - matches!(*self, Self::WrongLength(_, _)) + pub fn failed(&self) -> bool { + !matches!(*self, Self::PrevValue(_)) } pub fn is_key_missing(&self) -> bool { matches!(*self, Self::DoesNotExist) } + pub fn is_wrong_value(&self) -> bool { + matches!(*self, Self::WrongValue(_)) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -236,7 +217,7 @@ impl CandyStore { full_key: &[u8], val: &[u8], mode: InsertMode, - ) -> Result>> { + ) -> Result { let ph = PartedHash::new(&self.config.hash_seed, full_key); if full_key.len() > MAX_TOTAL_KEY_SIZE as usize { @@ -258,16 +239,16 @@ impl CandyStore { match status { InsertStatus::Added => { self.num_entries.fetch_add(1, Ordering::SeqCst); - return Ok(None); + return Ok(status); } InsertStatus::KeyDoesNotExist => { - return Ok(None); + return Ok(status); } - InsertStatus::Replaced(existing_val) => { - return Ok(Some(existing_val)); + InsertStatus::Replaced(_) => { + return Ok(status); } - InsertStatus::AlreadyExists(existing_val) => { - return Ok(Some(existing_val)); + InsertStatus::AlreadyExists(_) => { + return Ok(status); } InsertStatus::CompactionNeeded(write_offset) => { self.compact(shard_end, write_offset)?; @@ -282,10 +263,13 @@ impl CandyStore { } pub(crate) fn set_raw(&self, full_key: &[u8], val: &[u8]) -> Result { - if let Some(prev) = self.insert_internal(full_key, val, InsertMode::Set)? { - Ok(SetStatus::PrevValue(prev)) - } else { - Ok(SetStatus::CreatedNew) + match self.insert_internal(full_key, val, InsertMode::Set)? { + InsertStatus::Added => Ok(SetStatus::CreatedNew), + InsertStatus::Replaced(v) => Ok(SetStatus::PrevValue(v)), + InsertStatus::AlreadyExists(v) => Ok(SetStatus::PrevValue(v)), + InsertStatus::KeyDoesNotExist => unreachable!(), + InsertStatus::CompactionNeeded(_) => unreachable!(), + InsertStatus::SplitNeeded => unreachable!(), } } @@ -310,11 +294,19 @@ impl CandyStore { self.set_raw(&self.make_user_key(key), val) } - pub(crate) fn replace_raw(&self, full_key: &[u8], val: &[u8]) -> Result { - if let Some(prev) = self.insert_internal(full_key, val, InsertMode::Replace)? { - Ok(ReplaceStatus::PrevValue(prev)) - } else { - Ok(ReplaceStatus::DoesNotExist) + pub(crate) fn replace_raw( + &self, + full_key: &[u8], + val: &[u8], + expected_val: Option<&[u8]>, + ) -> Result { + match self.insert_internal(full_key, val, InsertMode::Replace(expected_val))? { + InsertStatus::Added => unreachable!(), + InsertStatus::Replaced(v) => Ok(ReplaceStatus::PrevValue(v)), + InsertStatus::AlreadyExists(v) => Ok(ReplaceStatus::WrongValue(v)), + InsertStatus::KeyDoesNotExist => Ok(ReplaceStatus::DoesNotExist), + InsertStatus::CompactionNeeded(_) => unreachable!(), + InsertStatus::SplitNeeded => unreachable!(), } } @@ -327,13 +319,23 @@ impl CandyStore { &self, key: &B1, val: &B2, + expected_val: Option<&B2>, ) -> Result { - self.owned_replace(key.as_ref().to_owned(), val.as_ref()) + self.owned_replace( + key.as_ref().to_owned(), + val.as_ref(), + expected_val.map(|ev| ev.as_ref()), + ) } /// Same as [Self::replace], but the key passed owned to this function - pub fn owned_replace(&self, key: Vec, val: &[u8]) -> Result { - self.replace_raw(&self.make_user_key(key), val.as_ref()) + pub fn owned_replace( + &self, + key: Vec, + val: &[u8], + expected_val: Option<&[u8]>, + ) -> Result { + self.replace_raw(&self.make_user_key(key), val.as_ref(), expected_val) } pub(crate) fn get_or_create_raw( @@ -341,11 +343,13 @@ impl CandyStore { full_key: &[u8], default_val: Vec, ) -> Result { - let res = self.insert_internal(&full_key, &default_val, InsertMode::GetOrCreate)?; - if let Some(prev) = res { - Ok(GetOrCreateStatus::ExistingValue(prev)) - } else { - Ok(GetOrCreateStatus::CreatedNew(default_val)) + match self.insert_internal(full_key, &default_val, InsertMode::GetOrCreate)? { + InsertStatus::Added => Ok(GetOrCreateStatus::CreatedNew(default_val)), + InsertStatus::AlreadyExists(v) => Ok(GetOrCreateStatus::ExistingValue(v)), + InsertStatus::Replaced(_) => unreachable!(), + InsertStatus::KeyDoesNotExist => unreachable!(), + InsertStatus::CompactionNeeded(_) => unreachable!(), + InsertStatus::SplitNeeded => unreachable!(), } } @@ -371,96 +375,4 @@ impl CandyStore { ) -> Result { self.get_or_create_raw(&self.make_user_key(key), default_val) } - - // this is NOT crash safe (may produce inconsistent results) - pub(crate) fn modify_inplace_raw( - &self, - full_key: &[u8], - patch: &[u8], - patch_offset: usize, - expected: Option<&[u8]>, - ) -> Result { - self.operate_on_key_mut(full_key, |shard, row, _ph, idx_kv| { - let Some((idx, _k, v)) = idx_kv else { - return Ok(ModifyStatus::DoesNotExist); - }; - - let (klen, vlen, offset) = Shard::extract_offset_and_size(row.offsets_and_sizes[idx]); - if patch_offset + patch.len() > vlen as usize { - return Ok(ModifyStatus::ValueTooLong(patch_offset, patch.len(), vlen)); - } - - if let Some(expected) = expected { - if &v[patch_offset..patch_offset + patch.len()] != expected { - return Ok(ModifyStatus::ValueMismatch(v)); - } - } - - shard.write_raw(patch, offset + klen as u64 + patch_offset as u64)?; - Ok(ModifyStatus::PrevValue(v)) - }) - } - - /// Modifies an existing entry in-place, instead of creating a new version. Note that the key must exist - /// and `patch.len() + patch_offset` must be less than or equal to the current value's length. - /// - /// This is operation is NOT crash-safe as it overwrites existing data, and thus may produce inconsistent - /// results on crashes (reading part old data, part new data). - /// - /// This method will never trigger a shard split or a shard compaction. - pub fn modify_inplace + ?Sized, B2: AsRef<[u8]> + ?Sized>( - &self, - key: &B1, - patch: &B2, - patch_offset: usize, - expected: Option<&B2>, - ) -> Result { - self.modify_inplace_raw( - &self.make_user_key(key.as_ref().to_owned()), - patch.as_ref(), - patch_offset, - expected.map(|b| b.as_ref()), - ) - } - - pub(crate) fn replace_inplace_raw( - &self, - full_key: &[u8], - new_value: &[u8], - ) -> Result { - self.operate_on_key_mut(full_key, |shard, row, _ph, idx_kv| { - let Some((idx, _k, v)) = idx_kv else { - return Ok(ModifyStatus::DoesNotExist); - }; - - let (klen, vlen, offset) = Shard::extract_offset_and_size(row.offsets_and_sizes[idx]); - if new_value.len() != vlen as usize { - return Ok(ModifyStatus::WrongLength(new_value.len(), vlen)); - } - - shard.write_raw(new_value, offset + klen as u64)?; - return Ok(ModifyStatus::PrevValue(v)); - }) - } - - /// Modifies an existing entry in-place, instead of creating a new version. Note that the key must exist - /// and `new_value.len()` must match exactly the existing value's length. This is mostly useful for binary - /// data. - /// - /// This is operation is NOT crash-safe as it overwrites existing data, and thus may produce inconsistent - /// results on crashes (reading part old data, part new data). - /// - /// This method will never trigger a shard split or a shard compaction. - pub fn replace_inplace + ?Sized, B2: AsRef<[u8]> + ?Sized>( - &self, - key: &B1, - new_value: &B2, - ) -> Result { - self.owned_replace_inplace(key.as_ref().to_owned(), new_value.as_ref()) - } - - /// Same as [Self::replace_inplace], but the key passed owned to this function - pub fn owned_replace_inplace(&self, key: Vec, new_value: &[u8]) -> Result { - self.replace_inplace_raw(&self.make_user_key(key), new_value) - } } diff --git a/src/lib.rs b/src/lib.rs index 561928e..e1aef2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,7 @@ //! to the disk IO latency, and operations generally require 1-4 disk IOs. //! //! The algorithm, for the most part, is crash-safe. That is, you can crash at any point and still be in a consistent -//! state. You might lose the ongoing operation, but we consider this acceptable. Some operations, like -//! `modify_inplace` or `set_promoting` are not crash safe, and are documented as such. +//! state. You might lose the ongoing operation, but we consider this acceptable. //! //! Candy is designed to consume very little memory: entries are written directly to the shard-file, and only a //! table of ~380KB is kept `mmap`-ed (it is also file-backed, so can be evicted if needed). A shard-file can @@ -57,7 +56,7 @@ mod store; mod typed; pub use hashing::HashSeed; -pub use insertion::{GetOrCreateStatus, ModifyStatus, ReplaceStatus, SetStatus}; +pub use insertion::{GetOrCreateStatus, ReplaceStatus, SetStatus}; use std::fmt::{Display, Formatter}; pub use store::{CandyStore, CoarseHistogram, SizeHistogram, Stats}; pub use typed::{CandyTypedDeque, CandyTypedKey, CandyTypedList, CandyTypedStore}; diff --git a/src/lists.rs b/src/lists.rs index 2cd6d84..a803934 100644 --- a/src/lists.rs +++ b/src/lists.rs @@ -6,7 +6,7 @@ use crate::{ }; use crate::encodable::EncodableUuid; -use anyhow::{anyhow, bail}; +use anyhow::bail; use bytemuck::{bytes_of, from_bytes, Pod, Zeroable}; use databuf::{config::num::LE, Encode}; use parking_lot::MutexGuard; @@ -41,6 +41,18 @@ const ITEM_SUFFIX_LEN: usize = size_of::() + ITEM_NAMESPACE.len(); fn chain_of(buf: &[u8]) -> Chain { bytemuck::pod_read_unaligned(&buf[buf.len() - size_of::()..]) } +fn update_chain_prev(val: &mut Vec, new_prev: PartedHash) { + let offset = val.len() - size_of::(); + let mut chain: Chain = bytemuck::pod_read_unaligned(&val[offset..]); + chain.prev = new_prev; + val[offset..].copy_from_slice(bytes_of(&chain)); +} +fn update_chain_next(val: &mut Vec, new_next: PartedHash) { + let offset = val.len() - size_of::(); + let mut chain: Chain = bytemuck::pod_read_unaligned(&val[offset..]); + chain.next = new_next; + val[offset..].copy_from_slice(bytes_of(&chain)); +} pub struct LinkedListIterator<'a> { store: &'a CandyStore, @@ -152,7 +164,7 @@ impl<'a> Iterator for RevLinkedListIterator<'a> { macro_rules! corrupted_list { ($($arg:tt)*) => { - return Err(anyhow!(CandyError::CorruptedLinkedList(format!($($arg)*)))); + bail!(CandyError::CorruptedLinkedList(format!($($arg)*))); }; } @@ -161,7 +173,7 @@ macro_rules! corrupted_if { if ($e1 == $e2) { let tmp = format!($($arg)*); let full = format!("{tmp} ({:?} == {:?})", $e1, $e2); - return Err(anyhow!(CandyError::CorruptedLinkedList(full))); + bail!(CandyError::CorruptedLinkedList(full)); } }; } @@ -176,11 +188,19 @@ macro_rules! corrupted_unless { if ($e1 != $e2) { let tmp = format!($($arg)*); let full = format!("{tmp} ({:?} != {:?})", $e1, $e2); - return Err(anyhow!(CandyError::CorruptedLinkedList(full))); + bail!(CandyError::CorruptedLinkedList(full)); } }; } +#[derive(Debug)] +enum InsertToListStatus { + ExistingValue(Vec), + WrongValue(Vec), + CreatedNew(Vec), + DoesNotExist, +} + impl CandyStore { fn make_list_key(&self, mut list_key: Vec) -> (PartedHash, Vec) { list_key.extend_from_slice(LIST_NAMESPACE); @@ -274,7 +294,7 @@ impl CandyStore { mut val: Vec, mode: InsertMode, pos: InsertPosition, - ) -> Result { + ) -> Result { let (list_ph, list_key) = self.make_list_key(list_key); let (item_ph, item_key) = self.make_item_key(list_ph, item_key); @@ -283,27 +303,42 @@ impl CandyStore { // if the item already exists, it means it belongs to this list. we just need to update the value and // keep the existing chain part if let Some(mut old_val) = self.get_raw(&item_key)? { - if matches!(mode, InsertMode::GetOrCreate) { - // don't replace the existing value - old_val.truncate(old_val.len() - size_of::()); - return Ok(GetOrCreateStatus::ExistingValue(old_val)); + match mode { + InsertMode::GetOrCreate => { + // don't replace the existing value + old_val.truncate(old_val.len() - size_of::()); + return Ok(InsertToListStatus::ExistingValue(old_val)); + } + InsertMode::Replace(ev) => { + if ev.is_some_and(|ev| ev != &old_val[..old_val.len() - size_of::()]) { + old_val.truncate(old_val.len() - size_of::()); + return Ok(InsertToListStatus::WrongValue(old_val)); + } + // fall through + } + _ => { + // fall through + } } val.extend_from_slice(&old_val[old_val.len() - size_of::()..]); - match self.replace_raw(&item_key, &val)? { + match self.replace_raw(&item_key, &val, None)? { ReplaceStatus::DoesNotExist => { corrupted_list!("failed replacing existing item"); } ReplaceStatus::PrevValue(mut v) => { v.truncate(v.len() - size_of::()); - return Ok(GetOrCreateStatus::ExistingValue(v)); + return Ok(InsertToListStatus::ExistingValue(v)); + } + ReplaceStatus::WrongValue(_) => { + unreachable!(); } } } - if matches!(mode, InsertMode::Replace) { + if matches!(mode, InsertMode::Replace(_)) { // not allowed to create - return Ok(GetOrCreateStatus::CreatedNew(val)); + return Ok(InsertToListStatus::DoesNotExist); } if let Some((origk, _)) = self._list_get(list_ph, item_ph)? { @@ -341,7 +376,7 @@ impl CandyStore { corrupted_list!("expected to create {item_key:?}"); } val.truncate(val.len() - size_of::()); - return Ok(GetOrCreateStatus::CreatedNew(val)); + return Ok(InsertToListStatus::CreatedNew(val)); } let v = match pos { @@ -353,7 +388,7 @@ impl CandyStore { } }; - Ok(GetOrCreateStatus::CreatedNew(v)) + Ok(InsertToListStatus::CreatedNew(v)) } fn _insert_to_list_head( @@ -367,22 +402,13 @@ impl CandyStore { ) -> Result> { // the list already exists. start at list.head and find the true head (it's possible list. // isn't up to date because of crashes) - let (head_ph, head_k, head_v) = self.find_true_head(list_ph, curr_list.head)?; + let (head_ph, head_k, mut head_v) = self.find_true_head(list_ph, curr_list.head)?; // modify the last item to point to the new item. if we crash after this, everything is okay because // find_true_tail will stop at this item - let mut head_chain = chain_of(&head_v); - head_chain.prev = item_ph; + update_chain_prev(&mut head_v, item_ph); - if !self - .modify_inplace_raw( - &head_k, - bytes_of(&head_chain), - head_v.len() - size_of::(), - Some(&head_v[head_v.len() - size_of::()..]), - )? - .was_replaced() - { + if self.replace_raw(&head_k, &head_v, None)?.failed() { corrupted_list!( "failed to update previous element {head_k:?} to point to this one {item_key:?}" ); @@ -399,7 +425,7 @@ impl CandyStore { }; val.extend_from_slice(bytes_of(&this_chain)); - if self.set_raw(&item_key, &val)?.was_replaced() { + if !self.set_raw(&item_key, &val)?.was_created() { corrupted_list!("tail element {item_key:?} already exists"); } @@ -410,14 +436,9 @@ impl CandyStore { anticollision_bits: 0, }; - if !self - .modify_inplace_raw( - &list_key, - bytes_of(&new_list), - 0, - Some(bytes_of(&curr_list)), - )? - .was_replaced() + if self + .replace_raw(&list_key, bytes_of(&new_list), Some(bytes_of(&curr_list)))? + .failed() { corrupted_list!("failed to update list tail to point to {item_key:?}"); } @@ -436,22 +457,13 @@ impl CandyStore { ) -> Result> { // the list already exists. start at list.tail and find the true tail (it's possible list.tail // isn't up to date because of crashes) - let (tail_ph, tail_k, tail_v) = self.find_true_tail(list_ph, curr_list.tail)?; + let (tail_ph, tail_k, mut tail_v) = self.find_true_tail(list_ph, curr_list.tail)?; // modify the last item to point to the new item. if we crash after this, everything is okay because // find_true_tail will stop at this item - let mut tail_chain = chain_of(&tail_v); - tail_chain.next = item_ph; + update_chain_next(&mut tail_v, item_ph); - if !self - .modify_inplace_raw( - &tail_k, - bytes_of(&tail_chain), - tail_v.len() - size_of::(), - Some(&tail_v[tail_v.len() - size_of::()..]), - )? - .was_replaced() - { + if self.replace_raw(&tail_k, &tail_v, None)?.failed() { corrupted_list!( "failed to update previous element {tail_k:?} to point to this one {item_key:?}" ); @@ -479,14 +491,9 @@ impl CandyStore { anticollision_bits: 0, }; - if !self - .modify_inplace_raw( - &list_key, - bytes_of(&new_list), - 0, - Some(bytes_of(&curr_list)), - )? - .was_replaced() + if self + .replace_raw(&list_key, bytes_of(&new_list), Some(bytes_of(&curr_list)))? + .failed() { corrupted_list!("failed to update list tail to point to {item_key:?}"); } @@ -565,8 +572,10 @@ impl CandyStore { InsertMode::Set, InsertPosition::Tail, )? { - GetOrCreateStatus::CreatedNew(_) => Ok(SetStatus::CreatedNew), - GetOrCreateStatus::ExistingValue(v) => Ok(SetStatus::PrevValue(v)), + InsertToListStatus::CreatedNew(_) => Ok(SetStatus::CreatedNew), + InsertToListStatus::ExistingValue(v) => Ok(SetStatus::PrevValue(v)), + InsertToListStatus::DoesNotExist => unreachable!(), + InsertToListStatus::WrongValue(_) => unreachable!(), } } @@ -581,11 +590,13 @@ impl CandyStore { list_key: &B1, item_key: &B2, val: &B3, + expected_val: Option<&B3>, ) -> Result { self.owned_replace_in_list( list_key.as_ref().to_owned(), item_key.as_ref().to_owned(), val.as_ref().to_owned(), + expected_val.map(|ev| ev.as_ref()), ) } @@ -595,16 +606,19 @@ impl CandyStore { list_key: Vec, item_key: Vec, val: Vec, + expected_val: Option<&[u8]>, ) -> Result { match self._insert_to_list( list_key, item_key, val, - InsertMode::Replace, + InsertMode::Replace(expected_val), InsertPosition::Tail, )? { - GetOrCreateStatus::CreatedNew(_) => Ok(ReplaceStatus::DoesNotExist), - GetOrCreateStatus::ExistingValue(v) => Ok(ReplaceStatus::PrevValue(v)), + InsertToListStatus::DoesNotExist => Ok(ReplaceStatus::DoesNotExist), + InsertToListStatus::ExistingValue(v) => Ok(ReplaceStatus::PrevValue(v)), + InsertToListStatus::WrongValue(v) => Ok(ReplaceStatus::WrongValue(v)), + InsertToListStatus::CreatedNew(_) => unreachable!(), } } @@ -636,13 +650,18 @@ impl CandyStore { item_key: Vec, val: Vec, ) -> Result { - self._insert_to_list( + match self._insert_to_list( list_key, item_key, val, InsertMode::GetOrCreate, InsertPosition::Tail, - ) + )? { + InsertToListStatus::CreatedNew(v) => Ok(GetOrCreateStatus::CreatedNew(v)), + InsertToListStatus::ExistingValue(v) => Ok(GetOrCreateStatus::ExistingValue(v)), + InsertToListStatus::DoesNotExist => unreachable!(), + InsertToListStatus::WrongValue(_) => unreachable!(), + } } /// @@ -714,7 +733,7 @@ impl CandyStore { ); // we surely have a next element - let Some((next_k, next_v)) = self._list_get(list_ph, chain.next)? else { + let Some((next_k, mut next_v)) = self._list_get(list_ph, chain.next)? else { corrupted_list!("failed getting next of {item_key:?}"); }; @@ -722,25 +741,16 @@ impl CandyStore { // at the expected place. XXX: head.prev will not be INVALID, which might break asserts. // we will need to remove the asserts, or add find_true_head list.head = chain.next; - if !self - .modify_inplace_raw(&list_key, bytes_of(&list), 0, Some(&list_buf))? - .was_replaced() + if self + .replace_raw(&list_key, bytes_of(&list), Some(&list_buf))? + .failed() { corrupted_list!("failed updating list head to point to next"); } // set the new head's prev link to INVALID. if we crash afterwards, everything is good. - let mut next_chain = chain_of(&next_v); - next_chain.prev = PartedHash::INVALID; - if !self - .modify_inplace_raw( - &next_k, - bytes_of(&next_chain), - next_v.len() - size_of::(), - Some(&next_v[next_v.len() - size_of::()..]), - )? - .was_replaced() - { + update_chain_prev(&mut next_v, PartedHash::INVALID); + if self.replace_raw(&next_k, &next_v, None)?.failed() { corrupted_list!("failed updating prev=INVALID on the now-first element"); } @@ -769,7 +779,7 @@ impl CandyStore { "last element must have a valid prev" ); - let Some((prev_k, prev_v)) = self._list_get(list_ph, chain.prev)? else { + let Some((prev_k, mut prev_v)) = self._list_get(list_ph, chain.prev)? else { corrupted_list!("missing prev element {item_key:?}"); }; @@ -777,7 +787,7 @@ impl CandyStore { // part of the list (find_true_tai will find it) list.tail = chain.prev; if !self - .modify_inplace_raw(&list_key, bytes_of(&list), 0, Some(&list_buf))? + .replace_raw(&list_key, bytes_of(&list), Some(&list_buf))? .was_replaced() { corrupted_list!("failed updating list tail to point to prev"); @@ -785,14 +795,10 @@ impl CandyStore { // update the new tail's next to INVALID. if we crash afterwards, the removed tail is no longer // considered part of the list - let mut prev_chain = chain_of(&prev_v); - prev_chain.next = PartedHash::INVALID; - self.modify_inplace_raw( - &prev_k, - bytes_of(&prev_chain), - prev_v.len() - size_of::(), - Some(&prev_v[prev_v.len() - size_of::()..]), - )?; + update_chain_next(&mut prev_v, PartedHash::INVALID); + if self.replace_raw(&prev_k, &prev_v, None)?.failed() { + corrupted_list!("failed updating next=INVALID on the now-last element"); + } // finally remove the item, sealing the deal self.remove_raw(&item_key)?; @@ -819,10 +825,10 @@ impl CandyStore { "a middle element must have a valid next" ); - let Some((prev_k, prev_v)) = self._list_get(list_ph, chain.prev)? else { + let Some((prev_k, mut prev_v)) = self._list_get(list_ph, chain.prev)? else { corrupted_list!("missing prev element of {item_key:?}"); }; - let Some((next_k, next_v)) = self._list_get(list_ph, chain.next)? else { + let Some((next_k, mut next_v)) = self._list_get(list_ph, chain.next)? else { corrupted_list!("missing next element of {item_key:?}"); }; @@ -830,36 +836,18 @@ impl CandyStore { // of the list, but will no longer appear in iterations. // note: we only do that if the previous item thinks that we're its next item, otherwise it means // we crashed in the middle of such an operation before - let mut prev_chain = chain_of(&prev_v); - if prev_chain.next == item_ph { - prev_chain.next = chain.next; - if !self - .modify_inplace_raw( - &prev_k, - bytes_of(&prev_chain), - prev_v.len() - size_of::(), - Some(&prev_v[prev_v.len() - size_of::()..]), - )? - .was_replaced() - { + if chain_of(&prev_v).next == item_ph { + update_chain_next(&mut prev_v, chain.next); + if self.replace_raw(&prev_k, &prev_v, None)?.failed() { corrupted_list!("failed updating prev.next on {prev_k:?}"); } } // disconnect the item from its next. if we crash afterwards, the item is truly no longer linked to the // list, so everything's good - let mut next_chain = chain_of(&next_v); - if next_chain.prev == item_ph { - next_chain.prev = chain.prev; - if !self - .modify_inplace_raw( - &next_k, - bytes_of(&next_chain), - next_v.len() - size_of::(), - Some(&next_v[next_v.len() - size_of::()..]), - )? - .was_replaced() - { + if chain_of(&next_v).prev == item_ph { + update_chain_prev(&mut next_v, chain.prev); + if self.replace_raw(&next_k, &next_v, None)?.failed() { corrupted_list!("failed updating next.prev on {next_k:?}"); } } @@ -1106,8 +1094,8 @@ impl CandyStore { InsertMode::GetOrCreate, InsertPosition::Tail, )?; - if !status.was_created() { - corrupted_list!("uuid collision {uuid}"); + if !matches!(status, InsertToListStatus::CreatedNew(_)) { + corrupted_list!("uuid collision {uuid} {status:?}"); } Ok(uuid) } @@ -1141,8 +1129,8 @@ impl CandyStore { InsertMode::GetOrCreate, InsertPosition::Head, )?; - if !status.was_created() { - corrupted_list!("uuid collision {uuid}"); + if !matches!(status, InsertToListStatus::CreatedNew(_)) { + corrupted_list!("uuid collision {uuid} {status:?}"); } Ok(uuid) } diff --git a/src/shard.rs b/src/shard.rs index 35503c9..e50949c 100644 --- a/src/shard.rs +++ b/src/shard.rs @@ -164,9 +164,9 @@ pub(crate) enum InsertStatus { } #[derive(Debug, Clone, Copy)] -pub(crate) enum InsertMode { +pub(crate) enum InsertMode<'a> { Set, - Replace, + Replace(Option<&'a [u8]>), GetOrCreate, } @@ -191,6 +191,40 @@ impl<'a> Iterator for ByHashIterator<'a> { } } +#[derive(Default, Debug, Clone, Copy)] +struct Backpointer(u32); + +impl Backpointer { + // MSB LSB + // +-----+-----+----------+ + // + row | sig | entry | + // | idx | idx | size | + // | (6) | (9) | (17) | + // +-----+-----+----------+ + fn new(row_idx: u16, sig_idx: u16, entry_size: usize) -> Self { + debug_assert!((row_idx as usize % NUM_ROWS) < (1 << 6), "{row_idx}"); + debug_assert!(sig_idx < (1 << 9), "{sig_idx}"); + Self( + (((row_idx % (NUM_ROWS as u16)) as u32) << 26) + | ((sig_idx as u32) << 17) + | (entry_size as u32 & 0x1ffff), + ) + } + + #[allow(dead_code)] + fn entry_size(&self) -> u32 { + self.0 & 0x1ffff + } + #[allow(dead_code)] + fn row(&self) -> usize { + (self.0 >> 26) as usize + } + #[allow(dead_code)] + fn sig_idx(&self) -> usize { + ((self.0 >> 17) & 0x1ff) as usize + } +} + // Note: it's possible to reduce the number row_locks, it we make them per-store rather than per-shard. // the trivial way that would be to use NUM_ROWS (without risking deadlocks), which means you can have 64 // concurrent operations. if you'd want more concurrency, it's possible to take the number of shards, @@ -231,9 +265,9 @@ impl Shard { .open(&filename)?; let md = file.metadata()?; if md.len() < HEADER_SIZE { - // when creating, set the file's length so that we won't need to extend it every time we write file.set_len(0)?; if config.truncate_up { + // when creating, set the file's length so that we won't need to extend it every time we write file.set_len(HEADER_SIZE + config.max_shard_size as u64)?; } } @@ -278,9 +312,12 @@ impl Shard { // reading doesn't require holding any locks - we only ever extend the file, never overwrite data fn read_kv(&self, offset_and_size: u64) -> Result { + const BP: u64 = size_of::() as u64; + let (klen, vlen, offset) = Self::extract_offset_and_size(offset_and_size); let mut buf = vec![0u8; klen + vlen]; - self.file.read_exact_at(&mut buf, HEADER_SIZE + offset)?; + self.file + .read_exact_at(&mut buf, HEADER_SIZE + BP + offset)?; let val = buf[klen..klen + vlen].to_owned(); buf.truncate(klen); @@ -289,11 +326,15 @@ impl Shard { } // writing doesn't require holding any locks since we write with an offset - fn write_kv(&self, key: &[u8], val: &[u8]) -> Result { + fn write_kv(&self, row_idx: u16, sig_idx: u16, key: &[u8], val: &[u8]) -> Result { + const BP: usize = size_of::(); + let entry_size = key.len() + val.len(); - let mut buf = vec![0u8; entry_size]; - buf[..key.len()].copy_from_slice(key); - buf[key.len()..].copy_from_slice(val); + let mut buf = vec![0u8; BP + entry_size]; + let bp = Backpointer::new(row_idx, sig_idx, entry_size); + buf[..BP].copy_from_slice(&bp.0.to_le_bytes()); + buf[BP..BP + key.len()].copy_from_slice(key); + buf[BP + key.len()..].copy_from_slice(val); // atomically allocate some area. it may leak if the IO below fails or if we crash before updating the // offsets_and_size array, but we're okay with leaks @@ -303,18 +344,12 @@ impl Shard { .fetch_add(buf.len() as u32, Ordering::SeqCst) as u64; // now writing can be non-atomic (pwrite) - self.write_raw(&buf, write_offset)?; + self.file.write_all_at(&buf, HEADER_SIZE + write_offset)?; self.header.size_histogram.insert(entry_size); Ok(((key.len() as u64) << 48) | ((val.len() as u64) << 32) | write_offset) } - #[inline] - pub(crate) fn write_raw(&self, buf: &[u8], offset: u64) -> Result<()> { - self.file.write_all_at(&buf, HEADER_SIZE + offset)?; - Ok(()) - } - pub(crate) fn read_at(&self, row_idx: usize, entry_idx: usize) -> Option> { let _guard = self.row_locks[row_idx].read(); let row = &self.header.rows.0[row_idx]; @@ -370,26 +405,37 @@ impl Shard { ) -> Result { let mut start = 0; while let Some(idx) = row.lookup(ph.signature(), &mut start) { - let (k, v) = self.read_kv(row.offsets_and_sizes[idx])?; - if key == k { - match mode { - InsertMode::GetOrCreate => { - // no-op, key already exists - return Ok(TryReplaceStatus::KeyExistsNotReplaced(v)); - } - InsertMode::Set | InsertMode::Replace => { - // optimization - if val != v { - row.offsets_and_sizes[idx] = self.write_kv(key, val)?; - self.header - .wasted_bytes - .fetch_add((k.len() + v.len()) as u64, Ordering::SeqCst); - } - return Ok(TryReplaceStatus::KeyExistsReplaced(v)); + let (k, existing_val) = self.read_kv(row.offsets_and_sizes[idx])?; + if key != k { + continue; + } + match mode { + InsertMode::GetOrCreate => { + // no-op, key already exists + return Ok(TryReplaceStatus::KeyExistsNotReplaced(existing_val)); + } + InsertMode::Set => { + // fall through + } + InsertMode::Replace(expected_val) => { + if expected_val.is_some_and(|expected_val| expected_val != existing_val) { + return Ok(TryReplaceStatus::KeyExistsNotReplaced(existing_val)); } } } + + // optimization + if val != existing_val { + row.offsets_and_sizes[idx] = + self.write_kv(ph.row_selector(), idx as u16, key, val)?; + self.header.wasted_bytes.fetch_add( + (size_of::() + k.len() + existing_val.len()) as u64, + Ordering::SeqCst, + ); + } + return Ok(TryReplaceStatus::KeyExistsReplaced(existing_val)); } + Ok(TryReplaceStatus::KeyDoesNotExist) } @@ -404,14 +450,15 @@ impl Shard { (guard, row) } - pub(crate) fn insert_unlocked( + pub(crate) fn insert( &self, - row: &mut ShardRow, ph: PartedHash, full_key: &[u8], val: &[u8], mode: InsertMode, ) -> Result { + let (_guard, row) = self.get_row_mut(ph); + if self.header.write_offset.load(Ordering::Relaxed) as u64 + (full_key.len() + val.len()) as u64 > self.config.max_shard_size as u64 @@ -429,20 +476,20 @@ impl Shard { match self.try_replace(row, ph, &full_key, val, mode)? { TryReplaceStatus::KeyDoesNotExist => { - if matches!(mode, InsertMode::Replace) { + if matches!(mode, InsertMode::Replace(_)) { return Ok(InsertStatus::KeyDoesNotExist); } // find an empty slot let mut start = 0; if let Some(idx) = row.lookup(INVALID_SIG, &mut start) { - let new_off = self.write_kv(&full_key, val)?; + let new_off = self.write_kv(ph.row_selector(), idx as u16, &full_key, val)?; // we don't want a reorder to happen here - first write the offset, then the signature row.offsets_and_sizes[idx] = new_off; std::sync::atomic::fence(Ordering::SeqCst); row.signatures[idx] = ph.signature(); - self.header.num_inserted.fetch_add(1, Ordering::SeqCst); + self.header.num_inserted.fetch_add(1, Ordering::Relaxed); Ok(InsertStatus::Added) } else { // no room in this row, must split @@ -456,55 +503,24 @@ impl Shard { } } - pub(crate) fn insert( - &self, - ph: PartedHash, - full_key: &[u8], - val: &[u8], - mode: InsertMode, - ) -> Result { - let (_guard, row) = self.get_row_mut(ph); - self.insert_unlocked(row, ph, full_key, val, mode) - } - - pub(crate) fn operate_on_key_mut( - &self, - ph: PartedHash, - key: &[u8], - func: impl FnOnce( - &Shard, - &mut ShardRow, - PartedHash, - Option<(usize, Vec, Vec)>, - ) -> Result, - ) -> Result { + pub(crate) fn remove(&self, ph: PartedHash, key: &[u8]) -> Result>> { let (_guard, row) = self.get_row_mut(ph); let mut start = 0; while let Some(idx) = row.lookup(ph.signature(), &mut start) { let (k, v) = self.read_kv(row.offsets_and_sizes[idx])?; if key == k { - return func(&self, row, ph, Some((idx, k, v))); + row.signatures[idx] = INVALID_SIG; + // we managed to remove this key + self.header.num_removed.fetch_add(1, Ordering::Relaxed); + self.header.wasted_bytes.fetch_add( + (size_of::() + k.len() + v.len()) as u64, + Ordering::Relaxed, + ); + return Ok(Some(v)); } } - func(&self, row, ph, None) - } - - pub(crate) fn remove(&self, ph: PartedHash, key: &[u8]) -> Result>> { - self.operate_on_key_mut(ph, key, |shard, row, _, idx_kv| { - let Some((idx, k, v)) = idx_kv else { - return Ok(None); - }; - - row.signatures[idx] = INVALID_SIG; - // we managed to remove this key - shard.header.num_removed.fetch_add(1, Ordering::Relaxed); - shard - .header - .wasted_bytes - .fetch_add((k.len() + v.len()) as u64, Ordering::Relaxed); - Ok(Some(v)) - }) + Ok(None) } } diff --git a/src/store.rs b/src/store.rs index b9948a1..958cadb 100644 --- a/src/store.rs +++ b/src/store.rs @@ -15,7 +15,7 @@ use crate::{ shard::{KVPair, HEADER_SIZE}, }; use crate::{ - shard::{Shard, ShardRow, NUM_ROWS, ROW_WIDTH}, + shard::{Shard, NUM_ROWS, ROW_WIDTH}, CandyError, }; use crate::{Config, Result}; @@ -292,9 +292,9 @@ impl CandyStore { config, dir_path, shards: RwLock::new(shards), - num_entries: 0.into(), - num_compactions: 0.into(), - num_splits: 0.into(), + num_entries: Default::default(), + num_compactions: Default::default(), + num_splits: Default::default(), keyed_locks_mask: num_keyed_locks - 1, keyed_locks, }) @@ -482,26 +482,6 @@ impl CandyStore { .collect::>()) } - pub(crate) fn operate_on_key_mut( - &self, - key: &[u8], - func: impl FnOnce( - &Shard, - &mut ShardRow, - PartedHash, - Option<(usize, Vec, Vec)>, - ) -> Result, - ) -> Result { - let ph = PartedHash::new(&self.config.hash_seed, key); - self.shards - .read() - .lower_bound(Bound::Excluded(&(ph.shard_selector() as u32))) - .peek_next() - .with_context(|| format!("missing shard for 0x{:04x}", ph.shard_selector()))? - .1 - .operate_on_key_mut(ph, key, func) - } - pub(crate) fn get_raw(&self, full_key: &[u8]) -> Result>> { let ph = PartedHash::new(&self.config.hash_seed, full_key); self.shards diff --git a/src/typed.rs b/src/typed.rs index b52b4f6..c477072 100644 --- a/src/typed.rs +++ b/src/typed.rs @@ -4,7 +4,7 @@ use std::{borrow::Borrow, marker::PhantomData, sync::Arc}; use crate::{ insertion::{ReplaceStatus, SetStatus}, store::TYPED_NAMESPACE, - CandyStore, ModifyStatus, + CandyStore, }; use crate::encodable::EncodableUuid; @@ -120,6 +120,7 @@ where &self, key: &Q1, val: &Q2, + expected_val: Option<&Q2>, ) -> Result> where K: Borrow, @@ -127,31 +128,14 @@ where { let kbytes = Self::make_key(key); let vbytes = val.to_bytes::(); - match self.store.replace_raw(&kbytes, &vbytes)? { + let ebytes = expected_val.map(|ev| ev.to_bytes::()).unwrap_or(vec![]); + match self + .store + .replace_raw(&kbytes, &vbytes, expected_val.map(|_| &*ebytes))? + { ReplaceStatus::DoesNotExist => Ok(None), ReplaceStatus::PrevValue(v) => Ok(Some(from_bytes::(&v)?)), - } - } - - /// Same as [CandyStore::replace_inplace] but serializes the key and the value. - /// Note: not crash safe! - pub fn replace_inplace( - &self, - key: &Q1, - val: &Q2, - ) -> Result> - where - K: Borrow, - V: Borrow, - { - let kbytes = Self::make_key(key); - let vbytes = val.to_bytes::(); - match self.store.replace_inplace_raw(&kbytes, &vbytes)? { - ModifyStatus::DoesNotExist => Ok(None), - ModifyStatus::PrevValue(v) => Ok(Some(from_bytes::(&v)?)), - ModifyStatus::ValueMismatch(_) => unreachable!(), - ModifyStatus::ValueTooLong(_, _, _) => Ok(None), - ModifyStatus::WrongLength(_, _) => Ok(None), + ReplaceStatus::WrongValue(_) => Ok(None), } } @@ -363,6 +347,7 @@ where list_key: &Q1, item_key: &Q2, val: &Q3, + expected_val: Option<&Q3>, ) -> Result> where L: Borrow, @@ -372,9 +357,18 @@ where let list_key = Self::make_list_key(list_key); let item_key = item_key.to_bytes::(); let val = val.to_bytes::(); - match self.store.owned_replace_in_list(list_key, item_key, val)? { + let ebytes = expected_val + .map(|ev| ev.to_bytes::()) + .unwrap_or_default(); + match self.store.owned_replace_in_list( + list_key, + item_key, + val, + expected_val.map(|_| &*ebytes), + )? { ReplaceStatus::DoesNotExist => Ok(None), ReplaceStatus::PrevValue(v) => Ok(Some(from_bytes::(&v)?)), + ReplaceStatus::WrongValue(_) => Ok(None), } } diff --git a/tests/test_atomics.rs b/tests/test_atomics.rs index 093108a..3c96404 100644 --- a/tests/test_atomics.rs +++ b/tests/test_atomics.rs @@ -1,6 +1,6 @@ mod common; -use candystore::{CandyStore, Config, GetOrCreateStatus, Result, SetStatus}; +use candystore::{CandyStore, Config, GetOrCreateStatus, ReplaceStatus, Result, SetStatus}; use crate::common::run_in_tempdir; @@ -11,11 +11,13 @@ fn test_atomics() -> Result<()> { assert!(db.get_or_create("aaa", "1111")?.was_created()); - assert!(db.replace("aaa", "2222")?.was_replaced()); + assert!(db.replace("aaa", "2222", None)?.was_replaced()); + + assert_eq!(db.get("aaa")?, Some("2222".into())); assert!(db.get_or_create("aaa", "1111")?.already_exists()); - assert!(!db.replace("bbb", "3333")?.was_replaced()); + assert!(!db.replace("bbb", "3333", None)?.was_replaced()); assert!(db.set("bbb", "4444")?.was_created()); assert_eq!(db.set("bbb", "5555")?, SetStatus::PrevValue("4444".into())); @@ -28,6 +30,11 @@ fn test_atomics() -> Result<()> { assert_eq!(db.get_or_create("cccc", "6666")?.value(), b"6666"); assert_eq!(db.get_or_create("aaa", "6666")?.value(), b"2222"); + assert_eq!( + db.replace("aaa", "6666", Some("2222"))?, + ReplaceStatus::PrevValue("2222".into()) + ); + Ok(()) }) } diff --git a/tests/test_lists.rs b/tests/test_lists.rs index ddc45a8..3394141 100644 --- a/tests/test_lists.rs +++ b/tests/test_lists.rs @@ -202,12 +202,12 @@ fn test_list_atomics() -> Result<()> { ); assert_eq!( - db.replace_in_list("xxx", "yyy", "3")?, + db.replace_in_list("xxx", "yyy", "3", None)?, ReplaceStatus::PrevValue("1".into()) ); assert_eq!( - db.replace_in_list("xxx", "zzz", "3")?, + db.replace_in_list("xxx", "zzz", "3", None)?, ReplaceStatus::DoesNotExist ); diff --git a/tests/test_logic.rs b/tests/test_logic.rs index 6ff59b3..18423d2 100644 --- a/tests/test_logic.rs +++ b/tests/test_logic.rs @@ -47,10 +47,9 @@ fn test_logic() -> Result<()> { .is_some()); } - let compactions1 = db._num_compactions(); let splits1 = db._num_splits(); assert_eq!(db._num_entries(), 1); - assert!(compactions1 >= 2); + assert!(db._num_compactions() >= 2); assert_eq!(splits1, 0); for i in 0..1000 { @@ -58,7 +57,6 @@ fn test_logic() -> Result<()> { } assert_eq!(db._num_entries(), 1001); - assert_eq!(db._num_compactions(), compactions1); assert!(db._num_splits() > splits1); assert_eq!(db.get("your_name")?, Some("vizzini".into())); diff --git a/tests/test_modify_inplace.rs b/tests/test_modify_inplace.rs deleted file mode 100644 index 4be56ff..0000000 --- a/tests/test_modify_inplace.rs +++ /dev/null @@ -1,57 +0,0 @@ -mod common; - -use candystore::{CandyStore, Config, ModifyStatus, Result}; - -use crate::common::run_in_tempdir; - -#[test] -fn test_modify_inplace() -> Result<()> { - run_in_tempdir(|dir| { - let db = CandyStore::open(dir, Config::default())?; - - db.set("aaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")?; - - assert!(db.modify_inplace("zzz", "bbb", 7, None)?.is_key_missing()); - - assert!(matches!( - db.modify_inplace( - "aaa", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - 7, - None - )?, - ModifyStatus::ValueTooLong(_, _, _) - )); - - assert!(db.modify_inplace("aaa", "bbb", 7, None)?.was_replaced()); - assert_eq!( - db.get("aaa")?, - Some("aaaaaaabbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into()) - ); - - assert!(db - .modify_inplace("aaa", "ccc", 10, Some("aaa"))? - .was_replaced()); - assert_eq!( - db.get("aaa")?, - Some("aaaaaaabbbcccaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into()) - ); - - assert!(db - .modify_inplace("aaa", "ddd", 10, Some("aaa"))? - .is_mismatch()); - assert_eq!( - db.get("aaa")?, - Some("aaaaaaabbbcccaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into()) - ); - - assert!(db.replace_inplace("bbb", "ABCD")?.is_key_missing()); - db.set("bbb", "ABCD")?; - assert!(db.replace_inplace("bbb", "BCDE")?.was_replaced()); - assert!(db.replace_inplace("bbb", "xyz")?.is_wrong_length()); - assert!(db.replace_inplace("bbb", "wyxyz")?.is_wrong_length()); - assert_eq!(db.get("bbb")?, Some("BCDE".into())); - - Ok(()) - }) -} diff --git a/tests/test_pre_split.rs b/tests/test_pre_split.rs index 22faba3..a97af3e 100644 --- a/tests/test_pre_split.rs +++ b/tests/test_pre_split.rs @@ -46,7 +46,7 @@ fn test_pre_split() -> Result<()> { // namespace byte as well assert_eq!( stats.wasted_bytes, - "aaa?".len() + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".len() + "????aaa?".len() + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".len() ); db.remove("aaa")?; @@ -55,9 +55,9 @@ fn test_pre_split() -> Result<()> { assert_eq!(stats.num_removed, 1); assert_eq!( stats.wasted_bytes, - "aaa?".len() + "????aaa?".len() + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".len() - + "aaa?".len() + + "????aaa?".len() + "xxx".len() ); @@ -77,19 +77,16 @@ fn test_compaction() -> Result<()> { }, )?; - db.set("aaa", "111122223333444455556666777788889999000011112222333344445555666677778888999900001111222233334444")?; - let stats = db.stats(); - assert_eq!(stats.num_inserted, 1); - assert_eq!(stats.used_bytes, 100); - // fill the shard to the rim, creating waste - for i in 0..9 { - db.set("aaa", &format!("11112222333344445555666677778888999900001111222233334444555566667777888899990000111122223333xxx{i}"))?; + for i in 0..10 { + db.set("aaa", &format!("1111222233334444555566667777888899990000111122223333444455556666777788889999000011112222333{:x}", i))?; + + let stats = db.stats(); + assert_eq!(stats.num_inserted, 1, "i={i}"); + assert_eq!(stats.used_bytes, 100 * (i + 1), "i={i}"); + assert_eq!(stats.wasted_bytes, 100 * i, "i={i}"); } - let stats = db.stats(); - assert_eq!(stats.used_bytes, 1000); - assert_eq!(stats.wasted_bytes, 900); assert_eq!(db._num_compactions(), 0); // insert a new entry, which will cause a compaction @@ -97,7 +94,7 @@ fn test_compaction() -> Result<()> { assert_eq!(db._num_compactions(), 1); let stats = db.stats(); - assert_eq!(stats.used_bytes, 100 + "bbb?".len() + "x".len()); + assert_eq!(stats.used_bytes, 100 + "????bbb?".len() + "x".len()); assert_eq!(stats.wasted_bytes, 0); Ok(())