Skip to main content

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}