1use serde::{Deserialize, Serialize};
16
17use crate::id::VShardId;
18
19pub const MAX_PREFIX_BITS: u8 = 16;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
24#[error("array routing: prefix_bits must be 1-{}, got {}", MAX_PREFIX_BITS, .0)]
25pub struct PrefixBitsOutOfRange(pub u8);
26
27pub const fn fmix64(key: u64) -> u64 {
30 let mut k = key;
31 k ^= k >> 33;
32 k = k.wrapping_mul(0xff51_afd7_ed55_8ccd);
33 k ^= k >> 33;
34 k = k.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
35 k ^= k >> 33;
36 k
37}
38
39pub const fn prefix_bucket(tile_key: u64, prefix_bits: u8) -> u32 {
43 let bits = if prefix_bits > 32 { 32 } else { prefix_bits };
44 if bits == 0 {
45 return 0;
46 }
47 (fmix64(tile_key) >> (64 - bits as u32)) as u32
48}
49
50#[derive(
54 Debug,
55 Clone,
56 Copy,
57 PartialEq,
58 Eq,
59 Hash,
60 Serialize,
61 Deserialize,
62 zerompk::ToMessagePack,
63 zerompk::FromMessagePack,
64)]
65pub struct ShardBucket {
66 pub bucket: u32,
68 pub prefix_bits: u8,
70}
71
72impl ShardBucket {
73 pub const fn contains(&self, tile_key: u64) -> bool {
75 prefix_bucket(tile_key, self.prefix_bits) == self.bucket
76 }
77}
78
79pub fn shard_bucket_admits(filter: Option<ShardBucket>, tile_key: u64) -> bool {
82 filter.is_none_or(|b| b.contains(tile_key))
83}
84
85pub fn routing_bits(prefix_bits: u8) -> Result<u8, PrefixBitsOutOfRange> {
88 if prefix_bits == 0 || prefix_bits > MAX_PREFIX_BITS {
89 return Err(PrefixBitsOutOfRange(prefix_bits));
90 }
91 Ok(prefix_bits.min(VShardId::BITS))
92}
93
94pub const fn bucket_to_vshard(bucket: u32, bits: u8) -> u32 {
97 bucket << (VShardId::BITS - bits) as u32
98}
99
100pub fn array_vshard_for_tile(tile_key: u64, prefix_bits: u8) -> Result<u32, PrefixBitsOutOfRange> {
105 let bits = routing_bits(prefix_bits)?;
106 Ok(bucket_to_vshard(prefix_bucket(tile_key, bits), bits))
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn fmix64_is_injective_on_a_sample() {
115 let mut seen = std::collections::HashSet::new();
116 for k in 0u64..100_000 {
117 assert!(seen.insert(fmix64(k)), "collision at {k}");
118 }
119 assert_eq!(fmix64(0), 0);
120 }
121
122 #[test]
123 fn prefix_bucket_stays_in_range() {
124 for bits in 1u8..=16 {
125 for k in [0u64, 1, 2, 63, 1 << 40, u64::MAX / 3, u64::MAX] {
126 assert!(prefix_bucket(k, bits) < (1u32 << bits), "bits={bits} k={k}");
127 }
128 }
129 assert_eq!(prefix_bucket(u64::MAX, 0), 0);
130 }
131
132 #[test]
133 fn small_keys_spread_over_buckets() {
134 let buckets: std::collections::HashSet<u32> =
136 (0u64..4096).map(|k| prefix_bucket(k, 8)).collect();
137 assert!(
138 buckets.len() >= 240,
139 "only {} of 256 buckets used",
140 buckets.len()
141 );
142 }
143
144 #[test]
145 fn shard_bucket_partitions_keys() {
146 let bits = 4u8;
147 for k in 0u64..2048 {
148 let owners = (0u32..(1 << bits))
149 .filter(|&b| {
150 ShardBucket {
151 bucket: b,
152 prefix_bits: bits,
153 }
154 .contains(k)
155 })
156 .count();
157 assert_eq!(owners, 1, "key {k} has {owners} owners");
158 }
159 assert!(shard_bucket_admits(None, 7));
160 }
161
162 #[test]
163 fn tile_routing_invalid_prefix_bits() {
164 assert_eq!(array_vshard_for_tile(0, 0), Err(PrefixBitsOutOfRange(0)));
165 assert_eq!(array_vshard_for_tile(0, 17), Err(PrefixBitsOutOfRange(17)));
166 }
167
168 #[test]
169 fn tile_routing_uses_the_mixed_bucket() {
170 for key in [0u64, 1, 2, 0x0100_0000_0000_0000, u64::MAX] {
172 let vshard = array_vshard_for_tile(key, 8).unwrap();
173 assert_eq!(vshard, prefix_bucket(key, 8) * 4, "key={key:#x}");
174 }
175 let key = 0x0040_0000_0000_0000;
177 assert_eq!(
178 array_vshard_for_tile(key, 10).unwrap(),
179 prefix_bucket(key, 10)
180 );
181 }
182
183 #[test]
184 fn tile_routing_output_in_vshard_range() {
185 for bits in 1u8..=MAX_PREFIX_BITS {
188 for &key in &[0u64, 1, u64::MAX / 3, u64::MAX / 2, u64::MAX] {
189 let vshard = array_vshard_for_tile(key, bits).unwrap();
190 assert!(
191 vshard < VShardId::COUNT,
192 "bits={bits} key={key:#x} → vshard={vshard}"
193 );
194 }
195 }
196 }
197
198 #[test]
199 fn prefix_bits_above_vshard_bits_route_like_vshard_bits() {
200 for key in [3u64, 99, 1 << 50, u64::MAX - 7] {
201 let at_max = array_vshard_for_tile(key, VShardId::BITS).unwrap();
202 for bits in VShardId::BITS..=MAX_PREFIX_BITS {
203 assert_eq!(array_vshard_for_tile(key, bits).unwrap(), at_max);
204 }
205 }
206 }
207
208 #[test]
209 fn small_tile_keys_spread_over_many_vshards() {
210 let vshards: std::collections::BTreeSet<u32> = (0u64..4096)
212 .map(|key| array_vshard_for_tile(key, 8).unwrap())
213 .collect();
214 assert!(
215 vshards.len() >= 240,
216 "only {} of 256 buckets used",
217 vshards.len()
218 );
219 }
220
221 #[test]
222 fn bucket_to_vshard_strides() {
223 assert_eq!(VShardId::COUNT, 1 << VShardId::BITS);
224 assert_eq!(VShardId::BITS, 10);
225 assert_eq!(bucket_to_vshard(1, 1), 512);
226 assert_eq!(bucket_to_vshard(1, 8), 4);
227 assert_eq!(bucket_to_vshard(1, 10), 1);
228 assert_eq!(routing_bits(16), Ok(10));
229 }
230
231 #[test]
232 fn shard_bucket_roundtrips_through_msgpack() {
233 let b = ShardBucket {
234 bucket: 41,
235 prefix_bits: 8,
236 };
237 let bytes = zerompk::to_msgpack_vec(&b).expect("encode");
238 let back: ShardBucket = zerompk::from_msgpack(&bytes).expect("decode");
239 assert_eq!(b, back);
240 }
241}