Skip to main content

commonware_storage/index/partitioned/
unordered.rs

1//! The unordered variant of a partitioned index.
2
3#[commonware_macros::stability(ALPHA)]
4use crate::index::partitioned::{PartitionRange, Partitioned};
5use crate::{
6    index::{
7        Unordered as UnorderedTrait, partitioned::partition_index_and_sub_key,
8        unordered::Index as UnorderedIndex,
9    },
10    translator::Translator,
11};
12use commonware_runtime::Metrics;
13
14/// A partitioned index that maps translated keys to values. The first `P` bytes of the
15/// (untranslated) key are used to determine the partition, and the translator is used by the
16/// partition-specific indices on the key after stripping this prefix. The value of `P` should be
17/// small, typically 1 or 2. Anything larger than 3 will fail to compile.
18pub struct Index<T: Translator, V: Send + Sync, const P: usize> {
19    partitions: Vec<UnorderedIndex<T, V>>,
20}
21
22impl<T: Translator, V: Send + Sync, const P: usize> Index<T, V, P> {
23    /// Create a new [Index] with the given translator and metrics registry.
24    pub fn new(ctx: impl Metrics, translator: T) -> Self {
25        let partition_count = 1 << (P * 8);
26        let mut partitions = Vec::with_capacity(partition_count);
27        for i in 0..partition_count {
28            partitions.push(UnorderedIndex::new(
29                ctx.child("partition").with_attribute("index", i),
30                translator.clone(),
31            ));
32        }
33
34        Self { partitions }
35    }
36
37    /// Get the partition for the given key, along with the prefix-stripped key for probing it.
38    fn get_partition<'a>(&self, key: &'a [u8]) -> (&UnorderedIndex<T, V>, &'a [u8]) {
39        let (i, sub_key) = partition_index_and_sub_key::<P>(key);
40
41        (&self.partitions[i], sub_key)
42    }
43
44    /// Get the mutable partition for the given key, along with the prefix-stripped key for probing
45    /// it.
46    fn get_partition_mut<'a>(&mut self, key: &'a [u8]) -> (&mut UnorderedIndex<T, V>, &'a [u8]) {
47        let (i, sub_key) = partition_index_and_sub_key::<P>(key);
48
49        (&mut self.partitions[i], sub_key)
50    }
51}
52
53#[commonware_macros::stability(ALPHA)]
54impl<T: Translator, V: Send + Sync + 'static, const P: usize> Partitioned for Index<T, V, P> {
55    type Range = RangeIndex<T, V, P>;
56
57    fn partition_count(&self) -> usize {
58        self.partitions.len()
59    }
60
61    fn partition_of(key: &[u8]) -> usize {
62        partition_index_and_sub_key::<P>(key).0
63    }
64
65    /// The range allocates only `count` slots, so per-worker memory is the range rather than
66    /// the full `2^(8*P)`.
67    fn new_range(&self, offset: usize, count: usize) -> RangeIndex<T, V, P> {
68        let partitions = (0..count)
69            .map(|i| self.partitions[offset + i].empty())
70            .collect();
71        RangeIndex { partitions, offset }
72    }
73
74    /// Absorbs each slot's maps wholesale into the matching partition.
75    fn install_range(&mut self, worker: RangeIndex<T, V, P>) {
76        let lo = worker.offset;
77        for (local, slot) in worker.partitions.into_iter().enumerate() {
78            self.partitions[lo + local].absorb(slot);
79        }
80    }
81}
82
83/// A restricted view of an [Index] covering only a contiguous range of partitions, held by one
84/// parallel snapshot-build worker (created by [Index::new_range], folded back into a full index by
85/// [Index::install_range]). It exposes only the cursor operations, which map a key's global
86/// partition index to the worker's local slot. The other [UnorderedTrait] operations index
87/// partitions globally and are deliberately unavailable, so they cannot be miscalled on a worker.
88#[commonware_macros::stability(ALPHA)]
89pub(crate) struct RangeIndex<T: Translator, V: Send + Sync, const P: usize> {
90    /// The worker's partition slots, addressed by local slot (`global partition - offset`).
91    partitions: Vec<UnorderedIndex<T, V>>,
92
93    /// The first global partition index the worker covers.
94    offset: usize,
95}
96
97#[commonware_macros::stability(ALPHA)]
98impl<T: Translator, V: Send + Sync, const P: usize> PartitionRange for RangeIndex<T, V, P> {
99    type Value = V;
100    type Cursor<'a>
101        = <UnorderedIndex<T, V> as UnorderedTrait>::Cursor<'a>
102    where
103        Self: 'a;
104
105    fn get_mut(&mut self, key: &[u8]) -> Option<Self::Cursor<'_>> {
106        let (i, sub) = partition_index_and_sub_key::<P>(key);
107        self.partitions[i - self.offset].get_mut(sub)
108    }
109
110    fn get_mut_or_insert(&mut self, key: &[u8], value: V) -> Option<Self::Cursor<'_>> {
111        let (i, sub) = partition_index_and_sub_key::<P>(key);
112        self.partitions[i - self.offset].get_mut_or_insert(sub, value)
113    }
114
115    fn for_each_value(&self, mut f: impl FnMut(&V)) {
116        for partition in &self.partitions {
117            partition.for_each_value(&mut f);
118        }
119    }
120}
121
122impl<T: Translator, V: Send + Sync, const P: usize> super::super::Factory for Index<T, V, P> {
123    type Translator = T;
124
125    fn new(ctx: impl commonware_runtime::Metrics, translator: T) -> Self {
126        Self::new(ctx, translator)
127    }
128}
129
130impl<T: Translator, V: Send + Sync, const P: usize> UnorderedTrait for Index<T, V, P> {
131    type Value = V;
132    type Cursor<'a>
133        = <UnorderedIndex<T, V> as UnorderedTrait>::Cursor<'a>
134    where
135        Self: 'a;
136
137    fn get<'a>(&'a self, key: &[u8]) -> impl Iterator<Item = &'a Self::Value> + 'a
138    where
139        Self::Value: 'a,
140    {
141        let (partition, sub_key) = self.get_partition(key);
142
143        partition.get(sub_key)
144    }
145
146    fn get_mut<'a>(&'a mut self, key: &[u8]) -> Option<Self::Cursor<'a>> {
147        let (partition, sub_key) = self.get_partition_mut(key);
148
149        partition.get_mut(sub_key)
150    }
151
152    fn get_mut_or_insert<'a>(
153        &'a mut self,
154        key: &[u8],
155        value: Self::Value,
156    ) -> Option<Self::Cursor<'a>> {
157        let (partition, sub_key) = self.get_partition_mut(key);
158
159        partition.get_mut_or_insert(sub_key, value)
160    }
161
162    fn insert(&mut self, key: &[u8], value: Self::Value) {
163        let (partition, sub_key) = self.get_partition_mut(key);
164
165        partition.insert(sub_key, value);
166    }
167
168    fn insert_and_retain(
169        &mut self,
170        key: &[u8],
171        value: Self::Value,
172        should_retain: impl Fn(&Self::Value) -> bool,
173    ) {
174        let (partition, sub_key) = self.get_partition_mut(key);
175
176        partition.insert_and_retain(sub_key, value, should_retain);
177    }
178
179    fn remove(&mut self, key: &[u8]) {
180        let (partition, sub_key) = self.get_partition_mut(key);
181
182        partition.remove(sub_key);
183    }
184
185    #[cfg(test)]
186    fn keys(&self) -> usize {
187        // Note: this is really inefficient, but it's only used for testing.
188        let mut keys = 0;
189        for partition in &self.partitions {
190            keys += partition.keys();
191        }
192
193        keys
194    }
195
196    #[cfg(test)]
197    fn items(&self) -> usize {
198        // Note: this is really inefficient, but it's only used for testing.
199        let mut items = 0;
200        for partition in &self.partitions {
201            items += partition.items();
202        }
203
204        items
205    }
206
207    #[cfg(test)]
208    fn pruned(&self) -> usize {
209        // Note: this is really inefficient, but it's only used for testing.
210        let mut pruned = 0;
211        for partition in &self.partitions {
212            pruned += partition.pruned();
213        }
214
215        pruned
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::{index::Cursor as _, translator::OneCap};
223    use commonware_macros::test_traced;
224    use commonware_runtime::{Runner as _, Supervisor as _, deterministic};
225
226    /// A `P=1` index over `u64` values with a one-byte translator, so distinct keys sharing a
227    /// partition and translated sub-key collide into an overflow chain.
228    fn new_index(ctx: impl Metrics) -> Index<OneCap, u64, 1> {
229        Index::new(ctx, OneCap)
230    }
231
232    /// A worker's value walk must visit every value it holds exactly once (inline values and
233    /// overflow chains alike), since the snapshot build derives the activity bitmap and
234    /// active-key counts from it.
235    #[test_traced]
236    fn test_range_for_each_value_visits_all_values_once() {
237        deterministic::Runner::default().start(|context| async move {
238            let full = new_index(context.child("full"));
239
240            // Two keys in partition 0x80 that collide on the one-byte translated sub-key (an
241            // overflow chain), one distinct key beside them, and one key in partition 0x81.
242            let mut worker = full.new_range(0x80, 2);
243            assert!(worker.get_mut_or_insert(&[0x80, 0x01, 0xAA], 1).is_none());
244            assert!(worker.get_mut_or_insert(&[0x80, 0x01, 0xBB], 2).is_some());
245            {
246                let mut cursor = worker.get_mut(&[0x80, 0x01, 0xBB]).unwrap();
247                cursor.next();
248                cursor.insert(2);
249            }
250            assert!(worker.get_mut_or_insert(&[0x80, 0x02], 3).is_none());
251            assert!(worker.get_mut_or_insert(&[0x81, 0x07], 4).is_none());
252
253            let mut seen = Vec::new();
254            worker.for_each_value(|v| seen.push(*v));
255            seen.sort_unstable();
256            assert_eq!(seen, vec![1, 2, 3, 4]);
257        });
258    }
259
260    /// A worker with a nonzero offset must land its slots at the global partitions
261    /// `offset + local` when installed, carrying inline values and overflow chains (the
262    /// key/item counts are live through the shared handles, so the `get` asserts are what pin
263    /// the moved contents).
264    #[test_traced]
265    fn test_install_range_nonzero_offset() {
266        deterministic::Runner::default().start(|context| async move {
267            let mut full = new_index(context.child("full"));
268
269            // A worker covering global partitions [0x80, 0x82): two keys in partition 0x80, one
270            // in 0x81.
271            let mut worker = full.new_range(0x80, 2);
272            assert!(worker.get_mut_or_insert(&[0x80, 0x01], 1).is_none());
273            assert!(worker.get_mut_or_insert(&[0x80, 0x02], 2).is_none());
274            assert!(worker.get_mut_or_insert(&[0x81, 0x07], 3).is_none());
275
276            // A colliding key (same partition and translated sub-key as the first) joins the
277            // first key's chain through the cursor, the same path the build worker's collision
278            // resolution takes.
279            {
280                let mut cursor = worker.get_mut_or_insert(&[0x80, 0x01, 0xFF], 4).unwrap();
281                while cursor.next().is_some() {}
282                cursor.insert(4);
283            }
284
285            // Installing must remap the slots to the worker's global range and preserve every
286            // value, including the chained one.
287            full.install_range(worker);
288            assert_eq!(full.keys(), 3);
289            assert_eq!(full.items(), 4);
290            assert_eq!(
291                full.get(&[0x80, 0x01]).copied().collect::<Vec<_>>(),
292                vec![1, 4]
293            );
294            assert_eq!(
295                full.get(&[0x80, 0x02]).copied().collect::<Vec<_>>(),
296                vec![2]
297            );
298            assert_eq!(
299                full.get(&[0x81, 0x07]).copied().collect::<Vec<_>>(),
300                vec![3]
301            );
302        });
303    }
304
305    /// A worker's cursor deletes (the parallel build's delete path) count on the covered
306    /// partition's shared `pruned` handle as they happen, matching what the serial build
307    /// records.
308    #[test_traced]
309    fn test_worker_prunes_count_live() {
310        deterministic::Runner::default().start(|context| async move {
311            let mut full = new_index(context.child("full"));
312            assert_eq!(full.pruned(), 0);
313
314            // Give one translated key two values, then delete both through a cursor. The worker
315            // slots hold clones of their partitions' metric handles, so the prunes count on the
316            // full index immediately.
317            let mut worker = full.new_range(0, full.partition_count());
318            assert!(worker.get_mut_or_insert(&[0x10, 0x01], 1).is_none());
319            {
320                let mut cursor = worker.get_mut_or_insert(&[0x10, 0x01, 0xFF], 2).unwrap();
321                while cursor.next().is_some() {}
322                cursor.insert(2);
323            }
324            {
325                let mut cursor = worker.get_mut(&[0x10, 0x01]).unwrap();
326                while cursor.next().is_some() {
327                    cursor.delete();
328                }
329            }
330            assert_eq!(full.pruned(), 2);
331
332            // Installing moves the structures without touching the already-live counts.
333            full.install_range(worker);
334            assert_eq!(full.pruned(), 2);
335            assert_eq!(full.keys(), 0);
336            assert_eq!(full.items(), 0);
337        });
338    }
339}