use crate::scope::Scope;
const BUCKETS: u64 = 100;
pub fn bucket(flag: &str, scope: &Scope) -> u8 {
(mix(hash(flag, scope)) % BUCKETS) as u8
}
pub fn in_rollout(flag: &str, scope: &Scope, percent: u8) -> bool {
(bucket(flag, scope) as u64) < (percent as u64).min(BUCKETS)
}
fn hash(flag: &str, scope: &Scope) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = OFFSET;
let mut eat = |bytes: &[u8]| {
for byte in bytes {
hash ^= *byte as u64;
hash = hash.wrapping_mul(PRIME);
}
};
eat(flag.as_bytes());
eat(&[0x1f]);
eat(scope.key().as_bytes());
hash
}
fn mix(mut hash: u64) -> u64 {
hash ^= hash >> 30;
hash = hash.wrapping_mul(0xbf58_476d_1ce4_e5b9);
hash ^= hash >> 27;
hash = hash.wrapping_mul(0x94d0_49bb_1331_11eb);
hash ^= hash >> 31;
hash
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_scope_always_lands_in_the_same_bucket() {
for id in 0..500 {
let scope = Scope::user_id(id);
assert_eq!(bucket("new-checkout", &scope), bucket("new-checkout", &scope));
}
}
#[test]
fn two_flags_do_not_roll_out_to_the_same_people() {
let differ = (0..200)
.map(Scope::user_id)
.filter(|scope| bucket("alpha", scope) != bucket("beta", scope))
.count();
assert!(differ > 180, "only {differ} of 200 scopes differed between two flags");
}
#[test]
fn the_buckets_are_spread_evenly_over_ten_thousand_scopes() {
let mut counts = [0usize; 100];
for id in 0..10_000 {
counts[bucket("new-checkout", &Scope::user_id(id)) as usize] += 1;
}
let low = *counts.iter().min().expect("a hundred buckets");
let high = *counts.iter().max().expect("a hundred buckets");
assert!(low >= 60, "the emptiest bucket held {low} of an expected 100");
assert!(high <= 150, "the fullest bucket held {high} of an expected 100");
}
#[test]
fn a_percentage_is_roughly_the_proportion_it_says() {
let inside =
(0..10_000).filter(|id| in_rollout("new-checkout", &Scope::user_id(*id), 25)).count();
assert!((2_300..=2_700).contains(&inside), "25% of 10,000 scopes came out as {inside}");
}
#[test]
fn raising_the_percentage_only_ever_adds_people() {
for id in 0..2_000 {
let scope = Scope::user_id(id);
if in_rollout("new-checkout", &scope, 10) {
assert!(in_rollout("new-checkout", &scope, 25), "user {id} lost the feature");
}
}
}
#[test]
fn zero_is_nobody_and_a_hundred_is_everybody() {
for id in 0..1_000 {
let scope = Scope::user_id(id);
assert!(!in_rollout("new-checkout", &scope, 0));
assert!(in_rollout("new-checkout", &scope, 100));
assert!(in_rollout("new-checkout", &scope, 255));
}
}
#[test]
fn one_percent_is_the_first_bucket_alone() {
for id in 0..1_000 {
let scope = Scope::user_id(id);
assert_eq!(in_rollout("new-checkout", &scope, 1), bucket("new-checkout", &scope) == 0);
}
}
#[test]
fn the_separator_keeps_a_flag_name_from_running_into_a_scope() {
assert_ne!(hash("new", &Scope::user("1")), hash("newuser", &Scope::tenant("1")));
}
#[test]
fn a_tenant_and_a_user_with_one_name_are_rolled_out_separately() {
let differ = (0..100)
.map(|n| format!("acme-{n}"))
.filter(|name| {
bucket("new-checkout", &Scope::user(name))
!= bucket("new-checkout", &Scope::tenant(name))
})
.count();
assert!(differ > 80, "only {differ} of 100 names differed by scope kind");
}
}