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}