dynomite/cluster/datacenter.rs
1//! Cluster topology: datacenters, racks, and the per-rack token
2//! continuum.
3//!
4//! A [`Datacenter`] owns a list of [`Rack`]s; a `Rack` owns a vector
5//! of [`Continuum`] points that map a [`DynToken`] to the index of
6//! the [`crate::cluster::peer::Peer`] that owns the token.
7//!
8//! Token ring lookups use an upper-bound search (the search lives in
9//! [`crate::cluster::vnode`]). The data shape stays here so the
10//! lookup can be tested against curated continua without spinning
11//! up a full pool.
12//!
13//! # Examples
14//!
15//! ```
16//! use dynomite::cluster::datacenter::{Datacenter, Rack};
17//! let mut dc = Datacenter::new("dc1".into());
18//! dc.upsert_rack("rack1".into());
19//! assert_eq!(dc.racks().len(), 1);
20//! ```
21
22use crate::hashkit::{random_slicing::RandomSlices, DynToken};
23
24/// Per-rack ring storage. Either the historical token continuum
25/// (a sorted [`Vec<Continuum>`]) or a [`RandomSlices`] table.
26/// The dispatcher consults whichever variant is present without
27/// caring which one it is; the engine produces only one shape
28/// per rack at a time.
29#[derive(Clone, Debug, Default)]
30pub enum RackRing {
31 /// Historical per-peer token continuum.
32 #[default]
33 Continuum,
34 /// Random-slicing partition table.
35 RandomSlicing(RandomSlices),
36}
37
38/// One ring point: a `(token, peer_idx)` mapping.
39#[derive(Clone, Debug)]
40pub struct Continuum {
41 /// Token at this ring position.
42 pub token: DynToken,
43 /// Index into the pool's peer array.
44 pub peer_idx: u32,
45}
46
47impl Continuum {
48 /// Construct a continuum point.
49 ///
50 /// # Examples
51 ///
52 /// ```
53 /// use dynomite::cluster::datacenter::Continuum;
54 /// use dynomite::hashkit::DynToken;
55 /// let c = Continuum::new(DynToken::from_u32(7), 3);
56 /// assert_eq!(c.peer_idx, 3);
57 /// ```
58 #[must_use]
59 pub fn new(token: DynToken, peer_idx: u32) -> Self {
60 Self { token, peer_idx }
61 }
62}
63
64/// One rack within a datacenter.
65///
66/// `continuums` is sorted by token in ascending order to support
67/// `vnode_dispatch`'s binary search; callers append continuum
68/// points and call [`Rack::sort_continuums`] once after a batch of
69/// updates.
70#[derive(Clone, Debug)]
71pub struct Rack {
72 name: String,
73 dc: String,
74 nserver_continuum: u32,
75 ncontinuum: u32,
76 continuums: Vec<Continuum>,
77 /// Optional [`RandomSlices`] table; populated when the
78 /// pool's distribution is
79 /// [`crate::conf::Distribution::RandomSlicing`].
80 /// [`Self::continuums`] stays in sync with the peer set so
81 /// the shadow distribution path can binary-search the same
82 /// rack without a second build.
83 ring: RackRing,
84}
85
86impl Rack {
87 /// Build an empty rack.
88 ///
89 /// # Examples
90 ///
91 /// ```
92 /// use dynomite::cluster::datacenter::Rack;
93 /// let r = Rack::new("rack1".into(), "dc1".into());
94 /// assert_eq!(r.name(), "rack1");
95 /// assert_eq!(r.dc(), "dc1");
96 /// assert!(r.continuums().is_empty());
97 /// ```
98 #[must_use]
99 pub fn new(name: String, dc: String) -> Self {
100 Self {
101 name,
102 dc,
103 nserver_continuum: 0,
104 ncontinuum: 0,
105 continuums: Vec::new(),
106 ring: RackRing::Continuum,
107 }
108 }
109
110 /// Rack name.
111 #[must_use]
112 pub fn name(&self) -> &str {
113 &self.name
114 }
115
116 /// Owning datacenter name.
117 #[must_use]
118 pub fn dc(&self) -> &str {
119 &self.dc
120 }
121
122 /// Borrow the continuum points (sorted by token).
123 #[must_use]
124 pub fn continuums(&self) -> &[Continuum] {
125 &self.continuums
126 }
127
128 /// Number of distinct servers ever added (mirrors the C
129 /// reference's `nserver_continuum`).
130 #[must_use]
131 pub fn nserver_continuum(&self) -> u32 {
132 self.nserver_continuum
133 }
134
135 /// Number of continuum points (mirrors `ncontinuum`).
136 #[must_use]
137 pub fn ncontinuum(&self) -> u32 {
138 self.ncontinuum
139 }
140
141 /// Append continuum points produced from one peer's tokens.
142 ///
143 /// # Examples
144 ///
145 /// ```
146 /// use dynomite::cluster::datacenter::{Continuum, Rack};
147 /// use dynomite::hashkit::DynToken;
148 /// let mut r = Rack::new("rack1".into(), "dc1".into());
149 /// r.add_peer_tokens(0, &[DynToken::from_u32(2), DynToken::from_u32(5)]);
150 /// assert_eq!(r.ncontinuum(), 2);
151 /// assert_eq!(r.nserver_continuum(), 2);
152 /// ```
153 pub fn add_peer_tokens(&mut self, peer_idx: u32, tokens: &[DynToken]) {
154 for tok in tokens {
155 self.continuums.push(Continuum::new(tok.clone(), peer_idx));
156 self.ncontinuum = self.ncontinuum.saturating_add(1);
157 self.nserver_continuum = self.nserver_continuum.saturating_add(1);
158 }
159 }
160
161 /// Sort the continuum by token (ascending). Callers must invoke
162 /// this once after a batch of [`Rack::add_peer_tokens`] calls so
163 /// that [`crate::cluster::vnode::dispatch`] can binary-search
164 /// the ring.
165 ///
166 /// # Examples
167 ///
168 /// ```
169 /// use dynomite::cluster::datacenter::Rack;
170 /// use dynomite::hashkit::DynToken;
171 /// let mut r = Rack::new("r".into(), "d".into());
172 /// r.add_peer_tokens(0, &[DynToken::from_u32(5)]);
173 /// r.add_peer_tokens(1, &[DynToken::from_u32(2)]);
174 /// r.sort_continuums();
175 /// assert_eq!(r.continuums()[0].peer_idx, 1);
176 /// ```
177 pub fn sort_continuums(&mut self) {
178 self.continuums.sort_by(|a, b| a.token.cmp(&b.token));
179 }
180
181 /// Reset all continuum state for a fresh rebuild.
182 pub fn clear_continuums(&mut self) {
183 self.continuums.clear();
184 self.ncontinuum = 0;
185 self.nserver_continuum = 0;
186 self.ring = RackRing::Continuum;
187 }
188
189 /// Borrow the rack's ring representation.
190 #[must_use]
191 pub fn ring(&self) -> &RackRing {
192 &self.ring
193 }
194
195 /// Install a [`RandomSlices`] table on this rack. The
196 /// continuum stays populated so the shadow-distribution
197 /// path (and any operator-side dump) can still walk the
198 /// vnode view.
199 pub fn set_random_slices(&mut self, slices: RandomSlices) {
200 self.ring = RackRing::RandomSlicing(slices);
201 }
202
203 /// True when the rack's live distribution is random
204 /// slicing.
205 #[must_use]
206 pub fn is_random_slicing(&self) -> bool {
207 matches!(self.ring, RackRing::RandomSlicing(_))
208 }
209
210 /// Borrow the rack's [`RandomSlices`] table when one is
211 /// installed.
212 #[must_use]
213 pub fn random_slices(&self) -> Option<&RandomSlices> {
214 match &self.ring {
215 RackRing::RandomSlicing(s) => Some(s),
216 RackRing::Continuum => None,
217 }
218 }
219}
220
221/// One datacenter.
222///
223/// A datacenter and its racks. The
224/// `preselected_rack_for_replication` field is computed by
225/// [`crate::cluster::pool::ServerPool::preselect_remote_racks`]
226/// and selects one rack per remote DC for cross-DC writes.
227#[derive(Clone, Debug)]
228pub struct Datacenter {
229 name: String,
230 racks: Vec<Rack>,
231 preselected_rack_for_replication: Option<usize>,
232}
233
234impl Datacenter {
235 /// Build an empty datacenter.
236 ///
237 /// # Examples
238 ///
239 /// ```
240 /// use dynomite::cluster::datacenter::Datacenter;
241 /// let dc = Datacenter::new("dc1".into());
242 /// assert_eq!(dc.name(), "dc1");
243 /// ```
244 #[must_use]
245 pub fn new(name: String) -> Self {
246 Self {
247 name,
248 racks: Vec::new(),
249 preselected_rack_for_replication: None,
250 }
251 }
252
253 /// Datacenter name.
254 #[must_use]
255 pub fn name(&self) -> &str {
256 &self.name
257 }
258
259 /// Borrow the rack list.
260 #[must_use]
261 pub fn racks(&self) -> &[Rack] {
262 &self.racks
263 }
264
265 /// Mutable rack list.
266 pub fn racks_mut(&mut self) -> &mut [Rack] {
267 &mut self.racks
268 }
269
270 /// Find a rack by name.
271 #[must_use]
272 pub fn rack(&self, name: &str) -> Option<&Rack> {
273 self.racks.iter().find(|r| r.name() == name)
274 }
275
276 /// Mutably find a rack by name.
277 pub fn rack_mut(&mut self, name: &str) -> Option<&mut Rack> {
278 self.racks.iter_mut().find(|r| r.name() == name)
279 }
280
281 /// Find a rack and return its index.
282 #[must_use]
283 pub fn rack_idx(&self, name: &str) -> Option<usize> {
284 self.racks.iter().position(|r| r.name() == name)
285 }
286
287 /// Insert a rack if absent; return a mutable handle to the
288 /// rack regardless. Mirrors `server_get_rack`.
289 pub fn upsert_rack(&mut self, name: String) -> &mut Rack {
290 if let Some(idx) = self.rack_idx(&name) {
291 return &mut self.racks[idx];
292 }
293 let dc = self.name.clone();
294 self.racks.push(Rack::new(name, dc));
295 let last = self.racks.len() - 1;
296 &mut self.racks[last]
297 }
298
299 /// Sort racks by name (ascending). Used by
300 /// [`crate::cluster::pool::ServerPool::preselect_remote_racks`]
301 /// and mirrors the `array_sort(&dc->racks, rack_name_cmp)` call
302 /// in `preselect_remote_rack_for_replication`.
303 pub fn sort_racks(&mut self) {
304 self.racks.sort_by(|a, b| a.name().cmp(b.name()));
305 }
306
307 /// Preselected rack index for replicating writes from another
308 /// DC into this DC.
309 #[must_use]
310 pub fn preselected_rack_idx(&self) -> Option<usize> {
311 self.preselected_rack_for_replication
312 }
313
314 /// Borrow the preselected rack, if any.
315 #[must_use]
316 pub fn preselected_rack(&self) -> Option<&Rack> {
317 self.preselected_rack_for_replication
318 .and_then(|i| self.racks.get(i))
319 }
320
321 /// Set the preselected rack index (used by the pool's
322 /// [`preselect_remote_racks`](crate::cluster::pool::ServerPool::preselect_remote_racks)
323 /// pass).
324 pub fn set_preselected_rack_idx(&mut self, idx: Option<usize>) {
325 self.preselected_rack_for_replication = idx;
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn upsert_is_idempotent() {
335 let mut dc = Datacenter::new("dc1".into());
336 dc.upsert_rack("r1".into());
337 dc.upsert_rack("r1".into());
338 assert_eq!(dc.racks().len(), 1);
339 }
340
341 #[test]
342 fn rack_continuum_sorts_by_token() {
343 let mut r = Rack::new("r".into(), "d".into());
344 r.add_peer_tokens(0, &[DynToken::from_u32(9)]);
345 r.add_peer_tokens(1, &[DynToken::from_u32(3)]);
346 r.add_peer_tokens(2, &[DynToken::from_u32(6)]);
347 r.sort_continuums();
348 let idxs: Vec<u32> = r.continuums().iter().map(|c| c.peer_idx).collect();
349 assert_eq!(idxs, vec![1, 2, 0]);
350 }
351
352 #[test]
353 fn rack_clear_resets_counters() {
354 let mut r = Rack::new("r".into(), "d".into());
355 r.add_peer_tokens(0, &[DynToken::from_u32(1)]);
356 r.clear_continuums();
357 assert_eq!(r.ncontinuum(), 0);
358 assert!(r.continuums().is_empty());
359 }
360
361 #[test]
362 fn sort_racks_alphabetical() {
363 let mut dc = Datacenter::new("dc1".into());
364 dc.upsert_rack("rb".into());
365 dc.upsert_rack("ra".into());
366 dc.sort_racks();
367 assert_eq!(dc.racks()[0].name(), "ra");
368 }
369}