Skip to main content

dynomite/cluster/
vnode.rs

1//! Token ring math: building and querying per-rack continuums.
2//!
3//! [`rebuild_continuums`] walks the pool's peer list,
4//! pushes each peer's tokens onto the owning rack's `continuum`
5//! array, then sorts the array per rack. The dispatcher then calls
6//! [`dispatch`] to find the peer
7//! that owns a key. The function uses a left-leaning binary search:
8//!
9//! * if the search token falls outside the ring (less than the first
10//!   token or strictly greater than the last), wrap to the first
11//!   continuum point;
12//! * otherwise, find the smallest continuum entry whose token is
13//!   greater than or equal to the search token (mirrors
14//!   `(a, b]` semantics from the reference).
15//!
16//! Both behaviours are reproduced verbatim by [`dispatch`] below.
17//!
18//! # Examples
19//!
20//! ```
21//! use dynomite::cluster::vnode::dispatch;
22//! use dynomite::cluster::datacenter::{Continuum, Rack};
23//! use dynomite::hashkit::DynToken;
24//!
25//! let mut r = Rack::new("r".into(), "d".into());
26//! r.add_peer_tokens(0, &[DynToken::from_u32(10)]);
27//! r.add_peer_tokens(1, &[DynToken::from_u32(20)]);
28//! r.add_peer_tokens(2, &[DynToken::from_u32(30)]);
29//! r.sort_continuums();
30//! assert_eq!(dispatch(r.continuums(), &DynToken::from_u32(15)), Some(1));
31//! assert_eq!(dispatch(r.continuums(), &DynToken::from_u32(35)), Some(0));
32//! ```
33
34use std::cmp::Ordering;
35
36use crate::cluster::datacenter::Continuum;
37use crate::hashkit::DynToken;
38
39/// Find the peer that owns `token` on the continuum.
40///
41/// Returns the peer index for the continuum point that owns
42/// `token`, or `None` when the slice is empty.
43///
44/// # Examples
45///
46/// ```
47/// use dynomite::cluster::vnode::dispatch;
48/// use dynomite::cluster::datacenter::Continuum;
49/// use dynomite::hashkit::DynToken;
50/// let cs: [Continuum; 0] = [];
51/// assert_eq!(dispatch(&cs, &DynToken::from_u32(0)), None);
52/// ```
53#[must_use]
54pub fn dispatch(continuums: &[Continuum], token: &DynToken) -> Option<u32> {
55    let n = continuums.len();
56    if n == 0 {
57        return None;
58    }
59    let first = &continuums[0];
60    let last = &continuums[n - 1];
61
62    // Wraparound: token greater than the largest continuum token, or
63    // less than or equal to the first one. Reference returns
64    // `left->index` in either case.
65    if last.token.cmp(token) == Ordering::Less {
66        return Some(first.peer_idx);
67    }
68    if first.token.cmp(token) != Ordering::Less {
69        return Some(first.peer_idx);
70    }
71
72    // Binary search for the smallest continuum entry with token >=
73    // search token.
74    let mut left = 0usize;
75    let mut right = n - 1;
76    while left < right {
77        let middle = left + (right - left) / 2;
78        match continuums[middle].token.cmp(token) {
79            Ordering::Equal => return Some(continuums[middle].peer_idx),
80            Ordering::Less => left = middle + 1,
81            Ordering::Greater => right = middle,
82        }
83    }
84    Some(continuums[right].peer_idx)
85}
86
87/// Per-peer token-list shape consumed by the rebuild pass.
88///
89/// `peer_idx` is the index into the pool's peer array; `tokens`
90/// is the token list for that peer. Mirrors the data shape
91/// `vnode_update` walks but decoupled from the live pool so the
92/// rebuild can be unit-tested.
93#[derive(Clone, Debug)]
94pub struct PeerTokens<'a> {
95    /// Peer index in the pool's peer array.
96    pub peer_idx: u32,
97    /// Datacenter name.
98    pub dc: &'a str,
99    /// Rack name.
100    pub rack: &'a str,
101    /// Peer's token list.
102    pub tokens: &'a [DynToken],
103}
104
105/// Walk a list of [`PeerTokens`] and append continuum entries to
106/// the matching rack inside `dcs`.
107///
108/// Caller is responsible for invoking
109/// [`crate::cluster::datacenter::Rack::sort_continuums`] on each
110/// touched rack once the rebuild is complete.
111///
112/// Returns the count of peers actually applied (a peer whose
113/// `(dc, rack)` is missing from `dcs` is skipped, so dc / rack
114/// tables must be populated before the rebuild runs).
115///
116/// # Examples
117///
118/// ```
119/// use dynomite::cluster::datacenter::Datacenter;
120/// use dynomite::cluster::vnode::{rebuild_continuums, PeerTokens};
121/// use dynomite::hashkit::DynToken;
122///
123/// let mut dc = Datacenter::new("d".into());
124/// dc.upsert_rack("r".into());
125/// let toks = [DynToken::from_u32(7)];
126/// let count = rebuild_continuums(
127///     &mut [dc],
128///     &[PeerTokens { peer_idx: 0, dc: "d", rack: "r", tokens: &toks }],
129/// );
130/// assert_eq!(count, 1);
131/// ```
132pub fn rebuild_continuums(
133    dcs: &mut [crate::cluster::datacenter::Datacenter],
134    peers: &[PeerTokens<'_>],
135) -> usize {
136    // First, clear every rack's continuum so the walk produces a
137    // deterministic result on each call.
138    for dc in dcs.iter_mut() {
139        for rack in dc.racks_mut().iter_mut() {
140            rack.clear_continuums();
141        }
142    }
143    let mut applied = 0usize;
144    let mut touched: Vec<(usize, usize)> = Vec::new();
145    for peer in peers {
146        let Some(dc_idx) = dcs.iter().position(|d| d.name() == peer.dc) else {
147            continue;
148        };
149        let Some(rack_idx) = dcs[dc_idx].rack_idx(peer.rack) else {
150            continue;
151        };
152        let dc = &mut dcs[dc_idx];
153        let rack = &mut dc.racks_mut()[rack_idx];
154        rack.add_peer_tokens(peer.peer_idx, peer.tokens);
155        applied += 1;
156        if !touched.contains(&(dc_idx, rack_idx)) {
157            touched.push((dc_idx, rack_idx));
158        }
159    }
160    for (di, ri) in touched {
161        dcs[di].racks_mut()[ri].sort_continuums();
162    }
163    applied
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::cluster::datacenter::Datacenter;
170
171    fn ring(pairs: &[(u32, u32)]) -> Vec<Continuum> {
172        pairs
173            .iter()
174            .map(|&(idx, tok)| Continuum::new(DynToken::from_u32(tok), idx))
175            .collect()
176    }
177
178    #[test]
179    fn empty_ring_returns_none() {
180        let cs: [Continuum; 0] = [];
181        assert_eq!(dispatch(&cs, &DynToken::from_u32(0)), None);
182    }
183
184    #[test]
185    fn single_token_always_resolves() {
186        let cs = ring(&[(7, 100)]);
187        assert_eq!(dispatch(&cs, &DynToken::from_u32(0)), Some(7));
188        assert_eq!(dispatch(&cs, &DynToken::from_u32(100)), Some(7));
189        assert_eq!(dispatch(&cs, &DynToken::from_u32(101)), Some(7));
190    }
191
192    #[test]
193    fn dispatch_wraps_on_overflow() {
194        let cs = ring(&[(0, 10), (1, 20), (2, 30)]);
195        assert_eq!(dispatch(&cs, &DynToken::from_u32(35)), Some(0));
196        assert_eq!(dispatch(&cs, &DynToken::from_u32(0)), Some(0));
197    }
198
199    #[test]
200    fn dispatch_finds_upper_bound() {
201        let cs = ring(&[(0, 10), (1, 20), (2, 30)]);
202        assert_eq!(dispatch(&cs, &DynToken::from_u32(11)), Some(1));
203        assert_eq!(dispatch(&cs, &DynToken::from_u32(20)), Some(1));
204        assert_eq!(dispatch(&cs, &DynToken::from_u32(21)), Some(2));
205        assert_eq!(dispatch(&cs, &DynToken::from_u32(30)), Some(2));
206    }
207
208    #[test]
209    fn rebuild_skips_unknown_dc() {
210        let mut dc = Datacenter::new("d".into());
211        dc.upsert_rack("r".into());
212        let mut dcs = vec![dc];
213        let toks = [DynToken::from_u32(1)];
214        let known = PeerTokens {
215            peer_idx: 0,
216            dc: "d",
217            rack: "r",
218            tokens: &toks,
219        };
220        let unknown = PeerTokens {
221            peer_idx: 1,
222            dc: "ghost",
223            rack: "r",
224            tokens: &toks,
225        };
226        assert_eq!(rebuild_continuums(&mut dcs, &[known, unknown]), 1);
227    }
228
229    #[test]
230    fn rebuild_clears_before_repopulating() {
231        let mut dc = Datacenter::new("d".into());
232        dc.upsert_rack("r".into());
233        let mut dcs = vec![dc];
234        let toks = [DynToken::from_u32(1)];
235        let p = PeerTokens {
236            peer_idx: 0,
237            dc: "d",
238            rack: "r",
239            tokens: &toks,
240        };
241        rebuild_continuums(&mut dcs, std::slice::from_ref(&p));
242        rebuild_continuums(&mut dcs, &[p]);
243        assert_eq!(dcs[0].racks()[0].ncontinuum(), 1);
244    }
245}