commonware_storage/index/
immutable.rs1use crate::translator::Translator;
30use commonware_runtime::{
31 Metrics,
32 telemetry::metrics::{Counter, Gauge, MetricsExt as _},
33};
34use commonware_utils::{Widen, handles::Table};
35use core::cmp::Ordering;
36use std::collections::VecDeque;
37
38pub struct Index<T: Translator> {
40 translator: T,
41 table: Table<u64>,
43 keys: VecDeque<T::Key>,
46 start: u64,
48
49 items: Gauge,
50 pruned: Counter,
51}
52
53impl<T: Translator> Index<T> {
54 pub fn new(ctx: impl Metrics, translator: T, start: u64, capacity: usize) -> Self {
57 Self {
58 translator,
59 table: Table::with_capacity(capacity),
60 keys: VecDeque::with_capacity(capacity),
61 start,
62 items: ctx.gauge("items", "Number of positions in the index"),
63 pruned: ctx.counter("pruned", "Number of positions pruned from the index"),
64 }
65 }
66
67 fn offset(start: u64, position: u64) -> usize {
69 usize::try_from(position - start).expect("retained positions must fit in usize")
70 }
71
72 fn key_at(keys: &VecDeque<T::Key>, start: u64, position: u64) -> T::Key {
74 keys[Self::offset(start, position)]
75 }
76
77 #[inline]
79 pub fn insert(&mut self, key: &[u8], position: u64) {
80 assert!(
81 position >= self.start,
82 "position {position} must be at or above the index start {}",
83 self.start
84 );
85 let translated = self.translator.transform(key);
86
87 let offset = Self::offset(self.start, position);
91 match offset.cmp(&self.keys.len()) {
92 Ordering::Equal => self.keys.push_back(translated),
93 Ordering::Less => self.keys[offset] = translated,
94 Ordering::Greater => self.keys.resize(offset + 1, translated),
95 }
96
97 self.table
100 .insert(self.translator.hash_one(translated), position, |position| {
101 self.translator
102 .hash_one(Self::key_at(&self.keys, self.start, position))
103 });
104 self.items.inc();
105 }
106
107 #[inline]
110 pub fn get(&self, key: &[u8]) -> impl Iterator<Item = u64> + '_ {
111 let translated = self.translator.transform(key);
112 self.table
113 .get(self.translator.hash_one(translated))
114 .filter(move |&position| Self::key_at(&self.keys, self.start, position) == translated)
115 }
116
117 pub fn prune(&mut self, start: u64) {
119 if start <= self.start {
120 return;
121 }
122
123 let retired = Self::offset(self.start, start).min(self.keys.len());
126 let mut removed = 0u64;
127 for (offset, key) in self.keys.drain(..retired).enumerate() {
128 let hash = self.translator.hash_one(key);
129 if self.table.remove(hash, self.start + Widen::widen(offset)) {
130 removed += 1;
131 }
132 }
133 self.start = start;
134 self.items.dec_by(removed as i64);
135 self.pruned.inc_by(removed);
136 }
137
138 pub const fn len(&self) -> usize {
140 self.table.len()
141 }
142
143 pub const fn is_empty(&self) -> bool {
145 self.table.is_empty()
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::translator::{EightCap, TwoCap};
153 use commonware_macros::test_traced;
154 use commonware_runtime::{Runner as _, deterministic};
155 use commonware_utils::GOLDEN_RATIO;
156 use core::hash::BuildHasher as _;
157
158 fn positions<T: Translator>(index: &Index<T>, key: &[u8]) -> Vec<u64> {
159 let mut positions: Vec<u64> = index.get(key).collect();
160 positions.sort_unstable();
161 positions
162 }
163
164 #[test_traced]
165 fn test_get_confirms_translated_key() {
166 deterministic::Runner::default().start(|context| async move {
167 let mut index = Index::new(context, TwoCap, 0, 0);
168 index.insert(b"aa1", 1);
170 index.insert(b"aa2", 3);
171 index.insert(b"bb1", 4);
172
173 assert_eq!(positions(&index, b"aa1"), [1, 3]);
174 assert_eq!(positions(&index, b"aa9"), [1, 3]);
175 assert_eq!(positions(&index, b"bb1"), [4]);
176 assert!(positions(&index, b"cc1").is_empty());
177 assert_eq!(index.len(), 3);
178 });
179 }
180
181 #[test_traced]
182 fn test_get_rejects_candidates_sharing_slot_and_fingerprint() {
183 deterministic::Runner::default().start(|context| async move {
184 const INVERSE: u64 = 0xf1de_83e1_9937_733d;
188 assert_eq!(GOLDEN_RATIO.wrapping_mul(INVERSE), 1);
189 let k1 = 0x1234_5678_9abc_def0u64;
190 let k2 = k1.wrapping_add(INVERSE << 16);
191
192 let mut index = Index::new(context, EightCap, 0, 0);
193 let (h1, h2) = (index.translator.hash_one(k1), index.translator.hash_one(k2));
194 assert_eq!(h2, h1.wrapping_add(1 << 16));
195 assert_eq!(h1 >> 56, h2 >> 56);
196
197 index.insert(&k1.to_be_bytes(), 1);
198 index.insert(&k2.to_be_bytes(), 3);
199 assert_eq!(positions(&index, &k1.to_be_bytes()), [1]);
200 assert_eq!(positions(&index, &k2.to_be_bytes()), [3]);
201 });
202 }
203
204 #[test_traced]
205 fn test_prune_retires_exactly_the_pruned_positions() {
206 deterministic::Runner::default().start(|context| async move {
207 let mut index = Index::new(context, EightCap, 0, 0);
208 for (i, position) in [1, 3, 4, 9, 20].into_iter().enumerate() {
210 index.insert(&[i as u8; 8], position);
211 }
212 assert_eq!(index.len(), 5);
213 assert_eq!(index.keys.len(), 21);
214
215 index.prune(2);
217 assert_eq!(index.len(), 4);
218 assert_eq!(index.keys.len(), 19);
219 assert!(positions(&index, &[0u8; 8]).is_empty());
220 assert_eq!(positions(&index, &[1u8; 8]), [3]);
221 assert_eq!(index.pruned.get(), 1);
222
223 index.prune(1);
225 assert_eq!(index.len(), 4);
226
227 index.prune(9);
229 assert_eq!(index.len(), 2);
230 assert_eq!(positions(&index, &[3u8; 8]), [9]);
231 assert_eq!(positions(&index, &[4u8; 8]), [20]);
232
233 index.prune(100);
235 assert!(index.is_empty());
236 assert!(index.keys.is_empty());
237 assert_eq!(index.pruned.get(), 5);
238 index.insert(&[9u8; 8], 101);
239 assert_eq!(positions(&index, &[9u8; 8]), [101]);
240 assert_eq!(index.keys.len(), 2);
241 });
242 }
243
244 #[test_traced]
245 fn test_start_above_zero() {
246 deterministic::Runner::default().start(|context| async move {
247 let start = 40_967;
248 let mut index = Index::new(context, EightCap, start, 0);
249 index.insert(b"first___", start);
250 index.insert(b"second__", start + 5_000);
251 assert_eq!(positions(&index, b"first___"), [start]);
252
253 index.prune(start + 1);
254 assert!(positions(&index, b"first___").is_empty());
255 assert_eq!(positions(&index, b"second__"), [start + 5_000]);
256 assert_eq!(index.len(), 1);
257 });
258 }
259
260 #[test_traced]
261 fn test_growth_rehashes_through_the_keys() {
262 deterministic::Runner::default().start(|context| async move {
263 let mut index = Index::new(context, TwoCap, 0, 0);
265 let key = |i: u32| {
266 let [hi, lo] = ((i % 5_000) as u16).to_be_bytes();
267 [hi, lo, (i / 5_000) as u8]
268 };
269 for i in 0..20_000u32 {
270 index.insert(&key(i), 2 * i as u64 + 1);
271 }
272 assert_eq!(index.len(), 20_000);
273 for i in (0..20_000u32).step_by(997) {
274 assert!(positions(&index, &key(i)).contains(&(2 * i as u64 + 1)));
275 }
276
277 index.prune(20_001);
279 assert_eq!(index.len(), 10_000);
280 assert!(!positions(&index, &key(0)).contains(&1));
281 assert!(positions(&index, &key(10_000)).contains(&20_001));
282 assert!(positions(&index, &key(19_999)).contains(&39_999));
283 assert_eq!(index.pruned.get(), 10_000);
284 });
285 }
286}