1use std::ops::RangeInclusive;
9
10use crate::meta_file::StaticSortedFileRange;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
14pub struct ShardBits(u8);
15
16impl ShardBits {
17 pub const MAX: ShardBits = ShardBits(12);
19
20 pub const fn new(bits: u8) -> Self {
22 assert!(bits <= Self::MAX.0, "too many shard bits");
23 Self(bits)
24 }
25
26 pub fn try_new(bits: u8) -> Option<Self> {
28 (bits <= Self::MAX.0).then_some(Self(bits))
29 }
30
31 pub fn get(self) -> u8 {
33 self.0
34 }
35
36 pub fn count(self) -> u32 {
38 1 << self.0
39 }
40
41 pub fn shard_of(self, hash: u64) -> u32 {
43 if self.0 == 0 {
44 0
45 } else {
46 (hash >> (u64::BITS - u32::from(self.0))) as u32
47 }
48 }
49
50 pub fn range(self, index: u32) -> RangeInclusive<u64> {
52 debug_assert!(index < self.count());
53 if self.0 == 0 {
54 return 0..=u64::MAX;
55 }
56 let shift = u64::BITS - u32::from(self.0);
57 let start = u64::from(index) << shift;
58 let end = start | (u64::MAX >> u32::from(self.0));
59 start..=end
60 }
61
62 pub fn maybe_reshard(self, bytes: u64, target_shard_size: u64) -> Self {
68 let target = target_shard_size.max(1);
69 let shard_size = |bits: Self| bytes >> bits.0;
70 let mut bits = self;
71 while bits < Self::MAX && shard_size(bits) > target.saturating_add(target / 2) {
73 bits.0 += 1;
74 }
75 while bits.0 > 0 && shard_size(bits) < target / 2 {
77 bits.0 -= 1;
78 }
79 bits
80 }
81
82 pub fn maybe_grow(self, bytes: u64, target_shard_size: u64) -> Self {
84 self.maybe_reshard(bytes, target_shard_size).max(self)
85 }
86}
87
88#[derive(PartialEq, Eq, Copy, Clone, Debug)]
89pub(crate) struct MetaEntryIndex {
90 pub meta_index: u32,
91 pub entry_index: u32,
92}
93#[derive(Default)]
96pub(crate) struct ShardFiles {
97 pub ranges: Box<[StaticSortedFileRange]>,
98 pub locations: Box<[MetaEntryIndex]>,
100}
101
102pub(crate) struct ShardIndex {
105 bits: ShardBits,
106 shards: Box<[ShardFiles]>,
107}
108
109impl Default for ShardIndex {
110 fn default() -> Self {
111 Self {
112 bits: ShardBits::default(),
113 shards: Box::new([ShardFiles::default()]),
114 }
115 }
116}
117
118impl ShardIndex {
119 pub(crate) fn build<'l>(
123 bits: ShardBits,
124 meta_files: impl DoubleEndedIterator<Item = &'l [StaticSortedFileRange]> + ExactSizeIterator,
125 ) -> Self {
126 let mut shards = (0..bits.count())
127 .map(|_| (Vec::new(), Vec::new()))
128 .collect::<Vec<_>>();
129 for (meta_index, ranges) in meta_files.enumerate().rev() {
130 for (entry_index, range) in ranges.iter().enumerate().rev() {
131 let first = bits.shard_of(range.min_hash);
132 let last = bits.shard_of(range.max_hash);
133 for (ranges, locations) in &mut shards[first as usize..=last as usize] {
134 ranges.push(*range);
135 locations.push(MetaEntryIndex {
136 meta_index: meta_index as u32,
137 entry_index: entry_index as u32,
138 });
139 }
140 }
141 }
142 Self {
143 bits,
144 shards: shards
145 .into_iter()
146 .map(|(ranges, locations)| ShardFiles {
147 ranges: ranges.into_boxed_slice(),
148 locations: locations.into_boxed_slice(),
149 })
150 .collect(),
151 }
152 }
153
154 pub(crate) fn bits(&self) -> ShardBits {
155 self.bits
156 }
157
158 pub(crate) fn candidates(&self, hash: u64) -> &ShardFiles {
160 &self.shards[self.bits.shard_of(hash) as usize]
161 }
162
163 pub(crate) fn shard(&self, shard: u32) -> &ShardFiles {
165 &self.shards[shard as usize]
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_shard_ranges_cover_the_key_space() {
175 for bits in [0, 1, 2, 3, 10] {
176 let bits = ShardBits::new(bits);
177 let mut next = 0u64;
178 for index in 0..bits.count() {
179 let range = bits.range(index);
180 assert_eq!(*range.start(), next);
181 assert_eq!(bits.shard_of(*range.start()), index);
182 assert_eq!(bits.shard_of(*range.end()), index);
183 next = range.end().wrapping_add(1);
184 }
185 assert_eq!(next, 0, "the last shard ends at u64::MAX");
186 }
187 }
188
189 #[test]
190 fn test_maybe_grow_never_shrinks() {
191 const MB: u64 = 1024 * 1024;
192 let bits = ShardBits::new(3);
193 assert!(bits.maybe_reshard(80 * MB, 256 * MB) < bits);
195 assert_eq!(bits.maybe_grow(80 * MB, 256 * MB), bits);
196 assert_eq!(
198 bits.maybe_grow(8 * 1024 * MB, 256 * MB),
199 bits.maybe_reshard(8 * 1024 * MB, 256 * MB)
200 );
201 assert!(bits.maybe_grow(8 * 1024 * MB, 256 * MB) > bits);
202 }
203
204 #[test]
205 fn test_one_more_bit_splits_shards() {
206 let coarse = ShardBits::new(2).range(3);
207 let fine = [ShardBits::new(3).range(6), ShardBits::new(3).range(7)];
208 assert_eq!(coarse.start(), fine[0].start());
209 assert_eq!(fine[0].end() + 1, *fine[1].start());
210 assert_eq!(coarse.end(), fine[1].end());
211 }
212
213 #[test]
214 fn test_shard_index() {
215 let range = |shard, bits| {
216 let r = ShardBits::new(bits).range(shard);
217 StaticSortedFileRange {
218 min_hash: *r.start(),
219 max_hash: *r.end(),
220 }
221 };
222 let old = [range(0, 1), range(1, 1)];
224 let new = [range(1, 2), range(2, 2), range(3, 2)];
225 let index = ShardIndex::build(ShardBits::new(2), [&old[..], &new[..]].into_iter());
226 let locations = |shard| index.shard(shard).locations.to_vec();
227 assert_eq!(
228 locations(0),
229 vec![MetaEntryIndex {
230 meta_index: 0,
231 entry_index: 0
232 }]
233 );
234 assert_eq!(
235 locations(1),
236 vec![
237 MetaEntryIndex {
238 meta_index: 1,
239 entry_index: 0
240 },
241 MetaEntryIndex {
242 meta_index: 0,
243 entry_index: 0
244 }
245 ]
246 );
247 assert_eq!(
248 locations(2),
249 vec![
250 MetaEntryIndex {
251 meta_index: 1,
252 entry_index: 1
253 },
254 MetaEntryIndex {
255 meta_index: 0,
256 entry_index: 1
257 }
258 ]
259 );
260 assert_eq!(
261 locations(3),
262 vec![
263 MetaEntryIndex {
264 meta_index: 1,
265 entry_index: 2
266 },
267 MetaEntryIndex {
268 meta_index: 0,
269 entry_index: 1
270 }
271 ]
272 );
273
274 assert_eq!(
275 index.candidates(u64::MAX).locations,
276 index.shard(3).locations
277 );
278 for shard in 0..4 {
279 let files = index.shard(shard);
280 assert_eq!(files.ranges.len(), files.locations.len());
281 for (
282 range,
283 &MetaEntryIndex {
284 meta_index: meta,
285 entry_index: entry,
286 },
287 ) in files.ranges.iter().zip(&files.locations)
288 {
289 let expected = [&old[..], &new[..]][meta as usize][entry as usize];
290 assert_eq!(range.min_hash, expected.min_hash);
291 }
292 }
293 }
294
295 #[test]
296 fn test_maybe_reshard_has_hysteresis() {
297 const T: u64 = 100;
298 let bits = |b| ShardBits::new(b);
299 assert_eq!(bits(1).maybe_reshard(280, T), bits(1));
301 assert_eq!(bits(1).maybe_reshard(320, T), bits(2));
302 assert_eq!(bits(2).maybe_reshard(240, T), bits(2));
304 assert_eq!(bits(2).maybe_reshard(160, T), bits(1));
305 assert_eq!(bits(0).maybe_reshard(1000, T), bits(3));
307 assert_eq!(bits(4).maybe_reshard(10, T), bits(0));
308 assert_eq!(bits(0).maybe_reshard(u64::MAX, 1), ShardBits::MAX);
309 for bytes in [0, 50, 149, 151, 499, 1000, 12345] {
311 for start in 0..6 {
312 let once = bits(start).maybe_reshard(bytes, T);
313 assert_eq!(once.maybe_reshard(bytes, T), once, "{bytes} {start}");
314 }
315 }
316 }
317}