use crate::config::Service;
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
fn fnv1a64(chunks: &[&[u8]]) -> u64 {
let mut h = FNV_OFFSET;
for chunk in chunks {
for &b in *chunk {
h ^= u64::from(b);
h = h.wrapping_mul(FNV_PRIME);
}
}
h
}
pub fn score(database: &str, service: Service, node_id: &str) -> u64 {
fnv1a64(&[
database.as_bytes(),
b"\0",
service.as_str().as_bytes(),
b"\0",
node_id.as_bytes(),
])
}
pub fn rank<'a>(database: &str, service: Service, candidates: &[&'a str]) -> Vec<&'a str> {
let mut ranked: Vec<(u64, &str)> = candidates
.iter()
.map(|&n| (score(database, service, n), n))
.collect();
ranked.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(b.1)));
ranked.into_iter().map(|(_, n)| n).collect()
}
pub fn owners<'a>(
database: &str,
service: Service,
count: usize,
candidates: &[&'a str],
) -> Vec<&'a str> {
let mut ranked = rank(database, service, candidates);
ranked.truncate(count);
ranked
}
pub fn score_target(database: &str, target: &str, node_id: &str) -> u64 {
fnv1a64(&[
database.as_bytes(),
b"\0",
Service::Mirror.as_str().as_bytes(),
b"\0",
target.as_bytes(),
b"\0",
node_id.as_bytes(),
])
}
pub fn rank_target<'a>(database: &str, target: &str, candidates: &[&'a str]) -> Vec<&'a str> {
let mut ranked: Vec<(u64, &str)> = candidates
.iter()
.map(|&n| (score_target(database, target, n), n))
.collect();
ranked.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(b.1)));
ranked.into_iter().map(|(_, n)| n).collect()
}
pub fn owner_target<'a>(database: &str, target: &str, candidates: &[&'a str]) -> Option<&'a str> {
rank_target(database, target, candidates).first().copied()
}
#[cfg(test)]
mod tests {
use super::*;
const NODES: &[&str] = &["sleet-1", "sleet-2", "sleet-3", "sleet-4"];
#[test]
fn scores_are_frozen() {
assert_eq!(
score("s3://b/db", Service::Gc, "sleet-1"),
0x0db7953ae9becf63
);
assert_eq!(
score("s3://b/db", Service::CompactorCoordinator, "sleet-1"),
0xf9bc77ef11433076
);
assert_eq!(
score("s3://b/db", Service::CompactionWorkers, "sleet-2"),
0xb6c679bdaed44473
);
}
#[test]
fn ranking_is_deterministic_and_service_dependent() {
let a = rank("s3://b/db", Service::Gc, NODES);
let b = rank("s3://b/db", Service::Gc, NODES);
assert_eq!(a, b);
assert_eq!(a.len(), NODES.len());
let c = rank("s3://b/db", Service::CompactorCoordinator, NODES);
let d = rank("s3://b/other", Service::Gc, NODES);
assert!(a != c || a != d);
}
#[test]
fn removing_a_node_preserves_the_order_of_the_rest() {
let full = rank("s3://b/db", Service::Gc, NODES);
let removed = full[1];
let remaining: Vec<&str> = NODES.iter().copied().filter(|&n| n != removed).collect();
let rehashed = rank("s3://b/db", Service::Gc, &remaining);
let expected: Vec<&str> = full.into_iter().filter(|&n| n != removed).collect();
assert_eq!(rehashed, expected);
}
#[test]
fn target_scores_are_frozen() {
assert_eq!(
score_target("s3://b/db", "dr", "sleet-1"),
0xa7057dde1da5335e
);
assert_eq!(
score_target("s3://b/db", "backup", "sleet-2"),
0x01282b68db71bebf
);
}
#[test]
fn target_ranking_is_deterministic_and_target_dependent() {
let a = rank_target("s3://b/db", "dr", NODES);
assert_eq!(a, rank_target("s3://b/db", "dr", NODES));
assert_eq!(a.len(), NODES.len());
let b = rank_target("s3://b/db", "backup", NODES);
assert!(a != b || rank_target("s3://b/other", "dr", NODES) != a);
assert_eq!(owner_target("s3://b/db", "dr", NODES), Some(a[0]));
assert_eq!(owner_target("s3://b/db", "dr", &[]), None);
}
#[test]
fn owners_are_distinct_and_capped() {
let two = owners("s3://b/db", Service::CompactionWorkers, 2, NODES);
assert_eq!(two.len(), 2);
assert_ne!(two[0], two[1]);
let all = owners("s3://b/db", Service::CompactionWorkers, 10, NODES);
assert_eq!(all.len(), NODES.len());
}
}