use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[must_use]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShardSelector {
shards: usize,
seed: u64,
}
impl ShardSelector {
pub fn new(shards: usize, seed: u64) -> Self {
Self {
shards: shards.max(1),
seed,
}
}
pub fn shard_count(&self) -> usize {
self.shards
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn shard_for_key<K: Hash + ?Sized>(&self, key: &K) -> usize {
let mut hasher = DefaultHasher::new();
self.seed.hash(&mut hasher);
key.hash(&mut hasher);
(hasher.finish() as usize) % self.shards
}
}
impl Default for ShardSelector {
fn default() -> Self {
Self::new(1, 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shard_selector_is_deterministic() {
let selector = &ShardSelector::new(8, 123);
let a = selector.shard_for_key(&"key");
let b = selector.shard_for_key(&"key");
assert_eq!(a, b);
assert!(a < selector.shard_count());
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_deterministic_mapping(
shard_count in 1usize..64,
seed in any::<u64>(),
key in any::<u32>()
) {
let selector = ShardSelector::new(shard_count, seed);
let shard1 = selector.shard_for_key(&key);
let shard2 = selector.shard_for_key(&key);
let shard3 = selector.shard_for_key(&key);
prop_assert_eq!(shard1, shard2);
prop_assert_eq!(shard2, shard3);
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_deterministic_batch(
shard_count in 1usize..64,
seed in any::<u64>(),
keys in prop::collection::vec(any::<u32>(), 0..50)
) {
let selector = ShardSelector::new(shard_count, seed);
let shards1: Vec<_> = keys.iter().map(|k| selector.shard_for_key(k)).collect();
let shards2: Vec<_> = keys.iter().map(|k| selector.shard_for_key(k)).collect();
prop_assert_eq!(shards1, shards2);
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_shard_in_range(
shard_count in 1usize..128,
seed in any::<u64>(),
key in any::<u64>()
) {
let selector = ShardSelector::new(shard_count, seed);
let shard = selector.shard_for_key(&key);
prop_assert!(shard < shard_count);
prop_assert!(shard < selector.shard_count());
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_all_keys_valid_range(
shard_count in 1usize..64,
seed in any::<u64>(),
keys in prop::collection::vec(any::<u32>(), 0..100)
) {
let selector = ShardSelector::new(shard_count, seed);
for key in keys {
let shard = selector.shard_for_key(&key);
prop_assert!(shard < shard_count);
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_shard_count_matches(
shard_count in 1usize..128,
seed in any::<u64>()
) {
let selector = ShardSelector::new(shard_count, seed);
prop_assert_eq!(selector.shard_count(), shard_count);
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_zero_shards_clamped(seed in any::<u64>()) {
let selector = ShardSelector::new(0, seed);
prop_assert_eq!(selector.shard_count(), 1);
for i in 0..10u32 {
let shard = selector.shard_for_key(&i);
prop_assert_eq!(shard, 0);
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_single_shard_returns_zero(
seed in any::<u64>(),
keys in prop::collection::vec(any::<u32>(), 0..50)
) {
let selector = ShardSelector::new(1, seed);
for key in keys {
let shard = selector.shard_for_key(&key);
prop_assert_eq!(shard, 0);
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_different_seeds_different_selectors(
shard_count in 1usize..64,
seed1 in any::<u64>(),
seed2 in any::<u64>()
) {
prop_assume!(seed1 != seed2);
let selector1 = ShardSelector::new(shard_count, seed1);
let selector2 = ShardSelector::new(shard_count, seed2);
prop_assert_ne!(selector1, selector2);
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_seed_affects_mapping(
shard_count in 2usize..16,
seed1 in any::<u64>(),
seed2 in any::<u64>(),
keys in prop::collection::vec(any::<u32>(), 10..50)
) {
prop_assume!(seed1 != seed2);
let selector1 = ShardSelector::new(shard_count, seed1);
let selector2 = ShardSelector::new(shard_count, seed2);
for key in &keys {
let _shard1 = selector1.shard_for_key(key);
let _shard2 = selector2.shard_for_key(key);
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_keys_use_shards(
shard_count in 2usize..16,
seed in any::<u64>(),
keys in prop::collection::vec(any::<u32>(), 20..100)
) {
let selector = ShardSelector::new(shard_count, seed);
let mut shard_counts = vec![0usize; shard_count];
for key in &keys {
let shard = selector.shard_for_key(key);
shard_counts[shard] += 1;
}
let used_shards = shard_counts.iter().filter(|&&count| count > 0).count();
prop_assert!(used_shards > 0);
let unique_keys: std::collections::HashSet<_> = keys.iter().collect();
if unique_keys.len() >= shard_count * 2 {
prop_assert!(used_shards > 1);
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_works_with_u32(
shard_count in 1usize..32,
seed in any::<u64>(),
keys in prop::collection::vec(any::<u32>(), 0..30)
) {
let selector = ShardSelector::new(shard_count, seed);
for key in keys {
let shard = selector.shard_for_key(&key);
prop_assert!(shard < shard_count);
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_works_with_u64(
shard_count in 1usize..32,
seed in any::<u64>(),
keys in prop::collection::vec(any::<u64>(), 0..30)
) {
let selector = ShardSelector::new(shard_count, seed);
for key in keys {
let shard = selector.shard_for_key(&key);
prop_assert!(shard < shard_count);
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_works_with_string(
shard_count in 1usize..32,
seed in any::<u64>(),
keys in prop::collection::vec("[a-z]{1,10}", 0..30)
) {
let selector = ShardSelector::new(shard_count, seed);
for key in keys {
let shard = selector.shard_for_key(&key);
prop_assert!(shard < shard_count);
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_default_single_shard(keys in prop::collection::vec(any::<u32>(), 0..30)) {
let selector = ShardSelector::default();
prop_assert_eq!(selector.shard_count(), 1);
for key in keys {
let shard = selector.shard_for_key(&key);
prop_assert_eq!(shard, 0);
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_same_config_equal(
shard_count in 1usize..64,
seed in any::<u64>()
) {
let selector1 = ShardSelector::new(shard_count, seed);
let selector2 = ShardSelector::new(shard_count, seed);
prop_assert_eq!(selector1, selector2);
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_different_shard_count_not_equal(
shard_count1 in 1usize..32,
shard_count2 in 32usize..64,
seed in any::<u64>()
) {
let selector1 = ShardSelector::new(shard_count1, seed);
let selector2 = ShardSelector::new(shard_count2, seed);
prop_assert_ne!(selector1, selector2);
}
}
}