diff --git a/Cargo.lock b/Cargo.lock index cebb2684bf..c4122a142e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2595,6 +2595,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "gettid" version = "0.1.3" @@ -3817,7 +3829,7 @@ dependencies = [ "procfs", "prometheus", "rand 0.8.5", - "rand_distr", + "rand_distr 0.4.3", "twox-hash", ] @@ -3906,10 +3918,10 @@ name = "neonart" version = "0.1.0" dependencies = [ "crossbeam-utils", - "rand 0.8.5", + "rand 0.9.1", + "rand_distr 0.5.1", "spin", "tracing", - "zerocopy 0.8.24", ] [[package]] @@ -5328,7 +5340,7 @@ dependencies = [ "postgres_backend", "pq_proto", "rand 0.8.5", - "rand_distr", + "rand_distr 0.4.3", "rcgen", "redis", "regex", @@ -5431,6 +5443,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.7.3" @@ -5455,6 +5473,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -5475,6 +5503,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -5493,6 +5531,15 @@ dependencies = [ "getrandom 0.2.11", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + [[package]] name = "rand_distr" version = "0.4.3" @@ -5503,6 +5550,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand 0.9.1", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -8240,6 +8297,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" @@ -8597,6 +8663,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "workspace_hack" version = "0.1.0" diff --git a/libs/neonart/Cargo.toml b/libs/neonart/Cargo.toml index 79a86d3f7e..915269e0cb 100644 --- a/libs/neonart/Cargo.toml +++ b/libs/neonart/Cargo.toml @@ -9,5 +9,6 @@ crossbeam-utils.workspace = true spin.workspace = true tracing.workspace = true -rand.workspace = true # for tests -zerocopy = "0.8" +[dev-dependencies] +rand = "0.9.1" +rand_distr = "0.5.1" diff --git a/libs/neonart/src/algorithm.rs b/libs/neonart/src/algorithm.rs index e476c14f12..3c7831f439 100644 --- a/libs/neonart/src/algorithm.rs +++ b/libs/neonart/src/algorithm.rs @@ -60,11 +60,11 @@ pub(crate) fn search<'e, K: Key, V: Value>( } } -pub(crate) fn update_fn<'e, K: Key, V: Value, A: ArtAllocator, F>( +pub(crate) fn update_fn<'e, 'g, K: Key, V: Value, A: ArtAllocator, F>( key: &K, value_fn: F, root: RootPtr, - guard: &'e TreeWriteGuard, + guard: &'g mut TreeWriteGuard<'e, K, V, A>, ) where F: FnOnce(Option<&V>) -> Option, { @@ -84,17 +84,17 @@ pub(crate) fn update_fn<'e, K: Key, V: Value, A: ArtAllocator, F>( key_bytes, ) { Ok(()) => break, - Err(ArtError::ConcurrentUpdate) => continue, // retry + Err(ArtError::ConcurrentUpdate) => { + eprintln!("retrying"); + continue; // retry + }, Err(ArtError::OutOfMemory) => { panic!("todo: OOM: try to GC, propagate to caller"); }, Err(ArtError::GarbageQueueFull) => { - if guard.collect_garbage() { - continue; - } // FIXME: This can happen if someone is holding back the epoch. We should // wait for the epoch to advance - panic!("todo: GC queue is full and couldn't free up space"); + panic!("todo: GC queue is full"); }, } } @@ -144,12 +144,12 @@ fn lookup_recurse<'e, V: Value>( } // This corresponds to the 'insertOpt' function in the paper -pub(crate) fn update_recurse<'e, K: Key, V: Value, A: ArtAllocator, F>( +pub(crate) fn update_recurse<'e, 'g, K: Key, V: Value, A: ArtAllocator, F>( key: &[u8], value_fn: F, node: NodeRef<'e, V>, rparent: Option<(ReadLockedNodeRef, u8)>, - guard: &'e TreeWriteGuard, + guard: &'g mut TreeWriteGuard<'e, K, V, A>, level: usize, orig_key: &[u8], ) -> Result<(), ArtError> @@ -211,7 +211,7 @@ where match next_node { ChildOrValue::Value(existing_value_ptr) => { assert!(key.len() == 1); - let wnode = rnode.upgrade_to_write_lock_or_restart()?; + let mut wnode = rnode.upgrade_to_write_lock_or_restart()?; // safety: Now that we have acquired the write lock, we have exclusive access to the // value @@ -219,7 +219,10 @@ where if let Some(new_value) = value_fn(Some(vmut)) { *vmut = new_value; } else { - // TODO: Treat this as deletion? + // TODO: Shrink the node + // TODO: If the node becomes empty, unlink it from parent + wnode.delete_value(key[0]); + } wnode.write_unlock(); @@ -320,11 +323,11 @@ fn insert_split_prefix<'e, K: Key, V: Value, A: ArtAllocator>( // Allocate a node for the new value. let new_value_node = - allocate_node_for_value(&key[common_prefix_len + 1..], value, guard.allocator)?; + allocate_node_for_value(&key[common_prefix_len + 1..], value, guard.tree_writer.allocator)?; // Allocate a new internal node with the common prefix // FIXME: deallocate 'new_value_node' on OOM - let mut prefix_node = node_ref::new_internal(&key[..common_prefix_len], guard.allocator)?; + let mut prefix_node = node_ref::new_internal(&key[..common_prefix_len], guard.tree_writer.allocator)?; // Add the old node and the new nodes to the new internal node prefix_node.insert_old_child(old_prefix[common_prefix_len], old_node); @@ -348,36 +351,34 @@ fn insert_to_node<'e, K: Key, V: Value, A: ArtAllocator>( if wnode.is_leaf() { wnode.insert_value(key[0], value); } else { - let value_child = allocate_node_for_value(&key[1..], value, guard.allocator)?; + let value_child = allocate_node_for_value(&key[1..], value, guard.tree_writer.allocator)?; wnode.insert_child(key[0], value_child.into_ptr()); } Ok(()) } // On entry: 'parent' and 'node' are locked -fn insert_and_grow<'e, K: Key, V: Value, A: ArtAllocator>( +fn insert_and_grow<'e, 'g, K: Key, V: Value, A: ArtAllocator>( key: &[u8], value: V, wnode: &WriteLockedNodeRef, parent: &mut WriteLockedNodeRef, parent_key_byte: u8, - guard: &'e TreeWriteGuard, + guard: &'g mut TreeWriteGuard<'e, K, V, A>, ) -> Result<(), ArtError> { - let mut bigger_node = wnode.grow(guard.allocator)?; - + let mut bigger_node = wnode.grow(guard.tree_writer.allocator)?; if wnode.is_leaf() { bigger_node.insert_value(key[0], value); } else { // FIXME: deallocate 'bigger_node' on OOM - let value_child = allocate_node_for_value(&key[1..], value, guard.allocator)?; + let value_child = allocate_node_for_value(&key[1..], value, guard.tree_writer.allocator)?; bigger_node.insert_new_child(key[0], value_child); } // Replace the pointer in the parent parent.replace_child(parent_key_byte, bigger_node.into_ptr()); - // FIXME: if this errors out, deallocate stuff we already allocated - guard.remember_obsolete_node(wnode.as_ptr())?; + guard.remember_obsolete_node(wnode.as_ptr()); Ok(()) } diff --git a/libs/neonart/src/algorithm/node_ptr.rs b/libs/neonart/src/algorithm/node_ptr.rs index 2f849323a0..d1a043b550 100644 --- a/libs/neonart/src/algorithm/node_ptr.rs +++ b/libs/neonart/src/algorithm/node_ptr.rs @@ -404,6 +404,19 @@ impl NodePtr { } } + pub(crate) fn delete_value(&mut self, key_byte: u8) { + match self.variant_mut() { + NodeVariantMut::Internal4(_) + | NodeVariantMut::Internal16(_) + | NodeVariantMut::Internal48(_) + | NodeVariantMut::Internal256(_) => panic!("delete_value called on internal node"), + NodeVariantMut::Leaf4(n) => n.delete_value(key_byte), + NodeVariantMut::Leaf16(n) => n.delete_value(key_byte), + NodeVariantMut::Leaf48(n) => n.delete_value(key_byte), + NodeVariantMut::Leaf256(n) => n.delete_value(key_byte), + } + } + pub(crate) fn deallocate(self, allocator: &impl ArtAllocator) { match self.variant() { NodeVariant::Internal4(_) => allocator.dealloc_node_internal4(self.ptr.cast()), @@ -766,7 +779,7 @@ impl NodeLeaf4 { } fn insert_value(&mut self, key_byte: u8, value: V) { - assert!(self.num_values < 16); + assert!(self.num_values < 4); let idx = self.num_values as usize; self.child_keys[idx] = key_byte; @@ -797,6 +810,23 @@ impl NodeLeaf4 { unsafe { ptr.write(init) }; ptr.into() } + + fn delete_value(&mut self, key_byte: u8) { + assert!(self.num_values <= 4); + + for i in 0..self.num_values as usize { + if self.child_keys[i] == key_byte { + assert!(self.child_values[i].is_some()); + if i < self.num_values as usize - 1 { + self.child_keys[i] = self.child_keys[self.num_values as usize - 1]; + self.child_values[i] = std::mem::replace(&mut self.child_values[self.num_values as usize - 1], None); + } + self.num_values -= 1; + return; + } + } + panic!("key to delete not found in leaf4 node"); + } } impl NodeLeaf16 { @@ -859,6 +889,23 @@ impl NodeLeaf16 { unsafe { ptr.write(init) }; ptr.into() } + + fn delete_value(&mut self, key_byte: u8) { + assert!(self.num_values <= 16); + + for i in 0..self.num_values as usize { + if self.child_keys[i as usize] == key_byte { + assert!(self.child_values[i as usize].is_some()); + if i < self.num_values as usize - 1 { + self.child_keys[i] = self.child_keys[self.num_values as usize - 1]; + self.child_values[i] = std::mem::replace(&mut self.child_values[self.num_values as usize - 1], None); + } + self.num_values -= 1; + return; + } + } + panic!("key to delete not found in leaf16 node"); + } } impl NodeLeaf48 { @@ -921,6 +968,34 @@ impl NodeLeaf48 { unsafe { ptr.write(init) }; ptr.into() } + + fn delete_value(&mut self, key_byte: u8) { + assert!(self.num_values <= 48); + + let idx = self.child_indexes[key_byte as usize]; + if idx == INVALID_CHILD_INDEX { + panic!("key to delete not found in leaf48 node"); + } + self.child_indexes[key_byte as usize] = INVALID_CHILD_INDEX; + self.num_values -= 1; + + if idx < self.num_values { + // Move all existing values with higher indexes down one position + for i in idx as usize ..self.num_values as usize { + self.child_values[i] = std::mem::replace(&mut self.child_values[i + 1], None); + } + + // Update all higher indexes + for i in 0..256 { + if self.child_indexes[i] != INVALID_CHILD_INDEX { + if self.child_indexes[i] > idx { + self.child_indexes[i] -= 1; + } + assert!(self.child_indexes[i] < self.num_values); + } + } + } + } } impl NodeLeaf256 { @@ -952,6 +1027,14 @@ impl NodeLeaf256 { self.child_values[key_byte as usize] = Some(value); self.num_values += 1; } + + fn delete_value(&mut self, key_byte: u8) { + if self.child_values[key_byte as usize].is_none() { + panic!("key to delete not found in leaf256 node"); + } + self.child_values[key_byte as usize] = None; + self.num_values -= 1; + } } impl NodeInternal256 { diff --git a/libs/neonart/src/algorithm/node_ref.rs b/libs/neonart/src/algorithm/node_ref.rs index 1e92e283d3..f1cd1cf749 100644 --- a/libs/neonart/src/algorithm/node_ref.rs +++ b/libs/neonart/src/algorithm/node_ref.rs @@ -152,6 +152,10 @@ impl<'e, V: Value> WriteLockedNodeRef<'e, V> { self.ptr.insert_value(key_byte, value) } + pub(crate) fn delete_value(&mut self, key_byte: u8) { + self.ptr.delete_value(key_byte) + } + pub(crate) fn grow<'a, A>( &self, allocator: &'a A, diff --git a/libs/neonart/src/lib.rs b/libs/neonart/src/lib.rs index 7ab184ae4e..4b45145c65 100644 --- a/libs/neonart/src/lib.rs +++ b/libs/neonart/src/lib.rs @@ -128,6 +128,7 @@ mod epoch; use algorithm::RootPtr; use algorithm::node_ptr::NodePtr; +use std::collections::VecDeque; use std::fmt::Debug; use std::marker::PhantomData; use std::ptr::NonNull; @@ -163,8 +164,6 @@ pub struct Tree { writer_attached: AtomicBool, epoch: epoch::EpochShared, - - garbage: spin::Mutex>, } unsafe impl Sync for Tree {} @@ -172,43 +171,29 @@ unsafe impl Send for Tree {} struct GarbageQueueFullError(); -struct GarbageQueue { - slots: [(NodePtr, u64); MAX_GARBAGE], - front: usize, - back: usize, -} +struct GarbageQueue(VecDeque<(NodePtr, u64)>); + +unsafe impl Sync for GarbageQueue {} +unsafe impl Send for GarbageQueue {} + impl GarbageQueue { fn new() -> GarbageQueue { - GarbageQueue { - slots: [const { (NodePtr::null(), 0) }; MAX_GARBAGE], - front: 0, - back: 0, - } + GarbageQueue(VecDeque::with_capacity(MAX_GARBAGE)) } fn remember_obsolete_node( &mut self, ptr: NodePtr, epoch: u64, - ) -> Result<(), GarbageQueueFullError> { - if self.front == self.back.wrapping_add(MAX_GARBAGE) { - return Err(GarbageQueueFullError()); - } - - self.slots[self.front % MAX_GARBAGE] = (ptr, epoch); - self.front = self.front.wrapping_add(1); - Ok(()) + ) { + self.0.push_front((ptr, epoch)); } fn next_obsolete(&mut self, cutoff_epoch: u64) -> Option> { - if self.front == self.back { - return None; - } - let slot = &self.slots[self.back % MAX_GARBAGE]; - // FIXME: performing wrapping comparison - if slot.1 < cutoff_epoch { - self.back += 1; - return Some(slot.0); + if let Some(back) = self.0.back() { + if back.1 < cutoff_epoch { + return Some(self.0.pop_back().unwrap().0); + } } None } @@ -237,6 +222,9 @@ where epoch_handle: epoch::LocalHandle<'t>, phantom_key: PhantomData, + + /// Obsolete nodes that cannot be recycled until their epoch expires. + garbage: spin::Mutex>, } /// The backends have a reference to this. It cannot be used to modify the tree @@ -260,7 +248,6 @@ impl<'a, 't: 'a, K: Key, V: Value, A: ArtAllocator> TreeInitStruct<'t, K, V, root: algorithm::new_root(allocator), writer_attached: AtomicBool::new(false), epoch: epoch::EpochShared::new(), - garbage: spin::Mutex::new(GarbageQueue::new()), }; unsafe { tree_ptr.write(init) }; @@ -281,6 +268,7 @@ impl<'a, 't: 'a, K: Key, V: Value, A: ArtAllocator> TreeInitStruct<'t, K, V, allocator: self.allocator, phantom_key: PhantomData, epoch_handle: self.tree.epoch.register(), + garbage: spin::Mutex::new(GarbageQueue::new()), } } @@ -294,13 +282,14 @@ impl<'a, 't: 'a, K: Key, V: Value, A: ArtAllocator> TreeInitStruct<'t, K, V, } impl<'t, K: Key + Clone, V: Value, A: ArtAllocator> TreeWriteAccess<'t, K, V, A> { - pub fn start_write(&'t self) -> TreeWriteGuard<'t, K, V, A> { - // TODO: grab epoch guard + pub fn start_write<'g>(&'t self) -> TreeWriteGuard<'g, K, V, A> + where 't: 'g + { TreeWriteGuard { - allocator: self.allocator, - tree: &self.tree, + tree_writer: self, epoch_pin: self.epoch_handle.pin(), phantom_key: PhantomData, + created_garbage: false } } @@ -344,49 +333,72 @@ pub struct TreeWriteGuard<'e, K, V, A> where K: Key, V: Value, + A: ArtAllocator, { - tree: &'e Tree, - allocator: &'e A, + tree_writer: &'e TreeWriteAccess<'e, K, V, A>, epoch_pin: EpochPin<'e>, phantom_key: PhantomData, + + created_garbage: bool, } impl<'t, K: Key, V: Value, A: ArtAllocator> TreeWriteGuard<'t, K, V, A> { - pub fn insert(&mut self, key: &K, value: V) { + + /// Get a value + pub fn get(&mut self, key: &K) -> Option { + algorithm::search(key, self.tree_writer.tree.root, &self.epoch_pin) + } + + /// Insert a value + pub fn insert(self, key: &K, value: V) { self.update_with_fn(key, |_| Some(value)) } - pub fn update_with_fn(&mut self, key: &K, value_fn: F) + /// Remove value + pub fn remove(self, key: &K) { + self.update_with_fn(key, |_| None) + } + + /// Update key using the given function. All the other modifying operations are based on this. + /// + /// The function is passed a reference to the existing value, if any. If the function + /// returns None, the value is removed from the tree (or if there was no existing value, + /// does nothing). If the function returns Some, the existing value is replaced, of if there + /// was no existing value, it is inserted. + pub fn update_with_fn(mut self, key: &K, value_fn: F) where F: FnOnce(Option<&V>) -> Option, { - algorithm::update_fn(key, value_fn, self.tree.root, self) + let result = algorithm::update_fn(key, value_fn, self.tree_writer.tree.root, &mut self); + + if self.created_garbage { + let n = self.collect_garbage(); + eprintln!("collected {n} obsolete nodes"); + } + result } - pub fn get(&mut self, key: &K) -> Option { - algorithm::search(key, self.tree.root, &self.epoch_pin) - } - - fn remember_obsolete_node(&'t self, ptr: NodePtr) -> Result<(), GarbageQueueFullError> { - self.tree + fn remember_obsolete_node(&mut self, ptr: NodePtr) { + self.tree_writer .garbage .lock() - .remember_obsolete_node(ptr, self.epoch_pin.epoch) + .remember_obsolete_node(ptr, self.epoch_pin.epoch); + self.created_garbage = true; } - // returns true if something was free'd up - fn collect_garbage(&'t self) -> bool { - let mut result = false; - self.tree.epoch.advance(); - self.tree.epoch.broadcast(); + // returns number of nodes recycled + fn collect_garbage(&self) -> usize { + self.tree_writer.tree.epoch.advance(); + self.tree_writer.tree.epoch.broadcast(); - let cutoff_epoch = self.tree.epoch.get_oldest(); + let cutoff_epoch = self.tree_writer.tree.epoch.get_oldest(); - let mut garbage_queue = self.tree.garbage.lock(); + let mut result = 0; + let mut garbage_queue = self.tree_writer.garbage.lock(); while let Some(ptr) = garbage_queue.next_obsolete(cutoff_epoch) { - ptr.deallocate(self.allocator); - result = true; + ptr.deallocate(self.tree_writer.allocator); + result += 1; } result } @@ -400,7 +412,7 @@ impl<'t, K: Key, V: Value + Debug> TreeReadGuard<'t, K, V> { } impl<'t, K: Key, V: Value + Debug> TreeWriteGuard<'t, K, V, ArtMultiSlabAllocator<'t, V>> { pub fn get_statistics(&self) -> ArtTreeStatistics { - self.allocator.get_statistics() + self.tree_writer.allocator.get_statistics() } } diff --git a/libs/neonart/src/tests.rs b/libs/neonart/src/tests.rs index fc79b32c11..3b315f456f 100644 --- a/libs/neonart/src/tests.rs +++ b/libs/neonart/src/tests.rs @@ -1,16 +1,19 @@ use std::collections::HashSet; +use std::collections::BTreeMap; +use crate::ArtAllocator; use crate::ArtMultiSlabAllocator; use crate::TreeInitStruct; use crate::{Key, Value}; use rand::seq::SliceRandom; -use rand::thread_rng; +use rand::Rng; +use rand_distr::Zipf; const TEST_KEY_LEN: usize = 16; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] struct TestKey([u8; TEST_KEY_LEN]); impl Key for TestKey { @@ -39,13 +42,10 @@ fn test_inserts + Copy>(keys: &[K]) { let tree_writer = init_struct.attach_writer(); for (idx, k) in keys.iter().enumerate() { - let mut w = tree_writer.start_write(); + let w = tree_writer.start_write(); w.insert(&(*k).into(), idx); - eprintln!("INSERTED {:?}", Into::::into(*k)); } - //tree_writer.start_read().dump(); - for (idx, k) in keys.iter().enumerate() { let r = tree_writer.start_read(); let value = r.get(&(*k).into()); @@ -67,7 +67,7 @@ fn dense() { // Do the same in random orders for _ in 1..10 { - keys.shuffle(&mut thread_rng()); + keys.shuffle(&mut rand::rng()); test_inserts(&keys); } } @@ -90,3 +90,56 @@ fn sparse() { } test_inserts(&keys); } + + + +#[derive(Clone, Copy, Debug)] +struct TestOp(TestKey, Option); + +fn apply_op>(op: &TestOp, tree: &crate::TreeWriteAccess, shadow: &mut BTreeMap) { + eprintln!("applying op: {op:?}"); + + // apply the change to the shadow tree first + let shadow_existing = if let Some(v) = op.1 { + shadow.insert(op.0, v) + } else { + shadow.remove(&op.0) + }; + + // apply to Art tree + let w = tree.start_write(); + w.update_with_fn(&op.0, |existing| { + assert_eq!(existing, shadow_existing.as_ref()); + return op.1; + }); +} + +#[test] +fn random_ops() { + const MEM_SIZE: usize = 10000000; + let mut area = Box::new_uninit_slice(MEM_SIZE); + + let allocator = ArtMultiSlabAllocator::new(&mut area); + + let init_struct = TreeInitStruct::::new(allocator); + let tree_writer = init_struct.attach_writer(); + + let mut shadow: std::collections::BTreeMap = BTreeMap::new(); + + let distribution = Zipf::new(u128::MAX as f64, 1.1).unwrap(); + let mut rng = rand::rng(); + for i in 0..100000 { + let key: TestKey = (rng.sample(distribution) as u128).into(); + + let op = TestOp( + key, + if rng.random_bool(0.75) { + Some(i) + } else { + None + }, + ); + + apply_op(&op, &tree_writer, &mut shadow); + } +} diff --git a/pgxn/neon/communicator/src/integrated_cache.rs b/pgxn/neon/communicator/src/integrated_cache.rs index e6766c9571..37cd65ec72 100644 --- a/pgxn/neon/communicator/src/integrated_cache.rs +++ b/pgxn/neon/communicator/src/integrated_cache.rs @@ -267,8 +267,7 @@ impl<'t> IntegratedCacheWriteAccess<'t> { } pub fn remember_rel_size(&'t self, rel: &RelTag, nblocks: u32) { - let mut w = self.cache_tree.start_write(); - + let w = self.cache_tree.start_write(); w.insert( &TreeKey::from(rel), TreeEntry::Rel(RelEntry { @@ -286,7 +285,7 @@ impl<'t> IntegratedCacheWriteAccess<'t> { lw_lsn: Lsn, ) { if let Some(file_cache) = self.file_cache.as_ref() { - let mut w = self.cache_tree.start_write(); + let w = self.cache_tree.start_write(); let key = TreeKey::from((rel, block_number)); @@ -324,7 +323,7 @@ impl<'t> IntegratedCacheWriteAccess<'t> { /// Forget information about given relation in the cache. (For DROP TABLE and such) pub fn forget_rel(&'t self, rel: &RelTag) { // FIXME: not implemented properly. smgrexists() would still return true for this - let mut w = self.cache_tree.start_write(); + let w = self.cache_tree.start_write(); w.insert( &TreeKey::from(rel), TreeEntry::Rel(RelEntry { nblocks: None }),