use dynomite::cluster::ReplicaTarget;
use dynomite::msg::ConsistencyLevel;
use crate::proto::pb::{REPLICATION_STRATEGY_SUCCESSORS, REPLICATION_STRATEGY_TOPOLOGY};
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum ReplicationStrategy {
#[default]
Topology,
Successors,
}
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
#[non_exhaustive]
pub enum ReplicationStrategyError {
#[error("replication_strategy: unknown selector {0}")]
Unknown(u32),
}
impl ReplicationStrategy {
pub fn from_wire(value: u32) -> Result<Self, ReplicationStrategyError> {
match value {
REPLICATION_STRATEGY_TOPOLOGY => Ok(Self::Topology),
REPLICATION_STRATEGY_SUCCESSORS => Ok(Self::Successors),
other => Err(ReplicationStrategyError::Unknown(other)),
}
}
#[must_use]
pub fn to_wire(self) -> u32 {
match self {
Self::Topology => REPLICATION_STRATEGY_TOPOLOGY,
Self::Successors => REPLICATION_STRATEGY_SUCCESSORS,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RingPoint {
pub token: u64,
pub peer_idx: u32,
pub dc: String,
pub rack: String,
}
impl RingPoint {
pub fn new(token: u64, peer_idx: u32, dc: impl Into<String>, rack: impl Into<String>) -> Self {
Self {
token,
peer_idx,
dc: dc.into(),
rack: rack.into(),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RingView {
points: Vec<RingPoint>,
}
impl RingView {
#[must_use]
pub fn new(mut points: Vec<RingPoint>) -> Self {
points.sort_by_key(|p| p.token);
Self { points }
}
#[must_use]
pub fn points(&self) -> &[RingPoint] {
&self.points
}
#[must_use]
pub fn peer_count(&self) -> usize {
let mut idxs: Vec<u32> = self.points.iter().map(|p| p.peer_idx).collect();
idxs.sort_unstable();
idxs.dedup();
idxs.len()
}
#[must_use]
pub fn primary_index(&self, key_hash: u64) -> Option<usize> {
if self.points.is_empty() {
return None;
}
match self.points.binary_search_by_key(&key_hash, |p| p.token) {
Ok(i) => Some(i),
Err(i) => {
if i >= self.points.len() {
Some(0)
} else {
Some(i)
}
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ReplicationPlan {
Topology(Vec<ReplicaTarget>),
Successors {
primary: ReplicaTarget,
n: u8,
successors: Vec<ReplicaTarget>,
},
}
impl ReplicationPlan {
#[must_use]
pub fn into_replica_list(self) -> Vec<ReplicaTarget> {
match self {
Self::Topology(targets) => targets,
Self::Successors {
primary,
mut successors,
..
} => {
let mut out = Vec::with_capacity(1 + successors.len());
out.push(primary);
out.append(&mut successors);
out
}
}
}
}
#[must_use]
pub fn plan_replicas(
distribution: &RingView,
key_hash: u64,
n_val: u8,
strategy: ReplicationStrategy,
consistency: ConsistencyLevel,
) -> ReplicationPlan {
let _ = consistency;
if matches!(strategy, ReplicationStrategy::Topology) {
return ReplicationPlan::Topology(Vec::new());
}
plan_successors(distribution, key_hash, n_val)
}
fn plan_successors(distribution: &RingView, key_hash: u64, n_val: u8) -> ReplicationPlan {
let target_count = (n_val as usize).max(1);
let points = distribution.points();
let Some(start) = distribution.primary_index(key_hash) else {
return ReplicationPlan::Successors {
primary: ReplicaTarget {
peer_idx: 0,
dc: String::new(),
rack: String::new(),
is_local: false,
},
n: n_val,
successors: Vec::new(),
};
};
let mut chosen: Vec<ReplicaTarget> = Vec::with_capacity(target_count);
let len = points.len();
for step in 0..len {
if chosen.len() >= target_count {
break;
}
let idx = (start + step) % len;
let pt = &points[idx];
if chosen.iter().any(|t| t.peer_idx == pt.peer_idx) {
continue;
}
chosen.push(ReplicaTarget {
peer_idx: pt.peer_idx,
dc: pt.dc.clone(),
rack: pt.rack.clone(),
is_local: false,
});
}
let mut iter = chosen.into_iter();
let primary = iter
.next()
.expect("primary_index returned Some so len >= 1");
let successors: Vec<ReplicaTarget> = iter.collect();
ReplicationPlan::Successors {
primary,
n: n_val,
successors,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn five_peer_ring() -> RingView {
let span = u64::from(u32::MAX);
let mut pts: Vec<RingPoint> = Vec::with_capacity(5);
for i in 0..5u32 {
let token = u64::from(i) * span / 5;
pts.push(RingPoint::new(token, i, "dc1", "r1"));
}
RingView::new(pts)
}
fn primary_idx(plan: &ReplicationPlan) -> u32 {
match plan {
ReplicationPlan::Successors { primary, .. } => primary.peer_idx,
other @ ReplicationPlan::Topology(_) => {
panic!("expected successors plan, got {other:?}")
}
}
}
fn successor_idxs(plan: &ReplicationPlan) -> Vec<u32> {
match plan {
ReplicationPlan::Successors { successors, .. } => {
successors.iter().map(|t| t.peer_idx).collect()
}
other @ ReplicationPlan::Topology(_) => {
panic!("expected successors plan, got {other:?}")
}
}
}
#[test]
fn key_hashing_into_peer1_slot_returns_1_2_3() {
let ring = five_peer_ring();
let key_hash = 1;
let plan = plan_replicas(
&ring,
key_hash,
3,
ReplicationStrategy::Successors,
ConsistencyLevel::DcOne,
);
assert_eq!(primary_idx(&plan), 1);
assert_eq!(successor_idxs(&plan), vec![2, 3]);
}
#[test]
fn key_hashing_into_peer0_slot_returns_0_1_2() {
let ring = five_peer_ring();
let key_hash = 0;
let plan = plan_replicas(
&ring,
key_hash,
3,
ReplicationStrategy::Successors,
ConsistencyLevel::DcOne,
);
assert_eq!(primary_idx(&plan), 0);
assert_eq!(successor_idxs(&plan), vec![1, 2]);
}
#[test]
fn key_hashing_into_peer3_slot_wraps_around() {
let ring = five_peer_ring();
let span = u64::from(u32::MAX);
let key_hash = 2 * span / 5 + 1;
let plan = plan_replicas(
&ring,
key_hash,
3,
ReplicationStrategy::Successors,
ConsistencyLevel::DcOne,
);
assert_eq!(primary_idx(&plan), 3);
assert_eq!(successor_idxs(&plan), vec![4, 0]);
}
#[test]
fn n_val_two_returns_two_peers() {
let ring = five_peer_ring();
let plan = plan_replicas(
&ring,
1,
2,
ReplicationStrategy::Successors,
ConsistencyLevel::DcOne,
);
let flat = plan.into_replica_list();
assert_eq!(flat.len(), 2);
assert_eq!(flat[0].peer_idx, 1);
assert_eq!(flat[1].peer_idx, 2);
}
#[test]
fn n_val_larger_than_ring_caps_at_ring_size() {
let ring = five_peer_ring();
let plan = plan_replicas(
&ring,
1,
10,
ReplicationStrategy::Successors,
ConsistencyLevel::DcOne,
);
let flat = plan.into_replica_list();
assert_eq!(flat.len(), 5);
let idxs: Vec<u32> = flat.iter().map(|t| t.peer_idx).collect();
assert_eq!(idxs, vec![1, 2, 3, 4, 0]);
}
#[test]
fn topology_strategy_returns_empty_passthrough() {
let ring = five_peer_ring();
let plan = plan_replicas(
&ring,
1,
3,
ReplicationStrategy::Topology,
ConsistencyLevel::DcOne,
);
assert!(matches!(plan, ReplicationPlan::Topology(ref v) if v.is_empty()));
}
#[test]
fn duplicate_peer_slots_are_deduplicated() {
let pts = vec![
RingPoint::new(0, 0, "dc1", "r1"),
RingPoint::new(100, 0, "dc1", "r1"),
RingPoint::new(200, 1, "dc1", "r1"),
RingPoint::new(300, 2, "dc1", "r1"),
];
let ring = RingView::new(pts);
let plan = plan_replicas(
&ring,
1,
3,
ReplicationStrategy::Successors,
ConsistencyLevel::DcOne,
);
let flat = plan.into_replica_list();
let idxs: Vec<u32> = flat.iter().map(|t| t.peer_idx).collect();
assert_eq!(idxs, vec![0, 1, 2]);
}
#[test]
fn empty_ring_yields_synthetic_primary() {
let ring = RingView::new(Vec::new());
let plan = plan_replicas(
&ring,
42,
3,
ReplicationStrategy::Successors,
ConsistencyLevel::DcOne,
);
match plan {
ReplicationPlan::Successors {
primary,
successors,
..
} => {
assert!(successors.is_empty());
assert_eq!(primary.dc, "");
}
other @ ReplicationPlan::Topology(_) => {
panic!("expected successors plan, got {other:?}")
}
}
}
#[test]
fn from_wire_round_trips() {
for s in [
ReplicationStrategy::Topology,
ReplicationStrategy::Successors,
] {
assert_eq!(ReplicationStrategy::from_wire(s.to_wire()).unwrap(), s);
}
assert_eq!(
ReplicationStrategy::from_wire(7),
Err(ReplicationStrategyError::Unknown(7))
);
}
#[test]
fn primary_index_wraps_when_hash_exceeds_largest_token() {
let ring = five_peer_ring();
let huge = u64::from(u32::MAX) + 1_000_000;
assert_eq!(ring.primary_index(huge), Some(0));
}
#[test]
fn ring_view_counts_distinct_peers() {
let pts = vec![
RingPoint::new(0, 0, "dc1", "r1"),
RingPoint::new(10, 0, "dc1", "r1"),
RingPoint::new(20, 1, "dc1", "r1"),
];
let r = RingView::new(pts);
assert_eq!(r.peer_count(), 2);
}
}