Skip to main content

dynomite/msg/
response_mgr.rs

1//! Per-DC response aggregation and quorum decisions.
2//!
3//! When the engine forwards a request to multiple peer replicas, the
4//! arriving responses are routed through a [`ResponseMgr`] that
5//! tracks how many replies are good, how many errored, and whether
6//! the body checksums agree. The aggregator keeps a fixed-size
7//! array sized for [`MAX_REPLICAS_PER_DC`] replicas because the
8//! consistency model is fundamentally a small, finite-state
9//! decision table.
10//!
11//! The state machine is the union of two observations:
12//!
13//! 1. quorum size is `max_responses / 2 + 1`, matching the C
14//!    formula in `init_response_mgr`;
15//! 2. once at least `quorum_responses` good replies have arrived,
16//!    the manager checks whether their body checksums agree and
17//!    declares the request done.
18
19use crate::core::types::MsgId;
20
21use super::message::Msg;
22use super::msg_type::MsgType;
23
24/// Maximum replicas per datacenter the engine tracks.
25pub const MAX_REPLICAS_PER_DC: usize = 3;
26
27/// One accepted response together with its payload checksum.
28#[derive(Debug)]
29struct GoodResponse {
30    msg: Box<Msg>,
31    checksum: u32,
32}
33
34/// Decision the manager has reached about an outstanding request.
35#[derive(Copy, Clone, Debug, Eq, PartialEq)]
36pub enum QuorumOutcome {
37    /// Still waiting for more responses.
38    Pending,
39    /// Quorum was achieved (a majority agree on the body checksum).
40    Achieved,
41    /// Enough responses arrived but they disagree on the body and no
42    /// further responses are pending; the dispatcher must reconcile.
43    Mismatched,
44    /// Quorum is impossible: too many error responses.
45    Failed,
46}
47
48/// Per-DC response aggregator.
49///
50/// The manager is bound to a single request and caps the reply count
51/// at the rack count of its datacenter.
52#[derive(Debug)]
53pub struct ResponseMgr {
54    is_read: bool,
55    max_responses: u8,
56    quorum_responses: u8,
57    error_responses: u8,
58    good: Vec<GoodResponse>,
59    err_rsp: Option<Box<Msg>>,
60    msg_id: MsgId,
61    msg_type: MsgType,
62    dc_name: Option<String>,
63}
64
65impl ResponseMgr {
66    /// Build a manager bound to `req` that expects at most
67    /// `max_responses` replies.
68    ///
69    /// `max_responses` must be in the range `1..=MAX_REPLICAS_PER_DC`
70    /// (it is the rack count of the target datacenter); an
71    /// out-of-range value trips a debug assertion.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use dynomite::msg::{Msg, MsgType, ResponseMgr};
77    ///
78    /// let req = Msg::new(1, MsgType::ReqRedisGet, true);
79    /// let mgr = ResponseMgr::new(&req, 3, None);
80    /// assert_eq!(mgr.max_responses(), 3);
81    /// assert_eq!(mgr.quorum_responses(), 2);
82    /// ```
83    #[must_use]
84    pub fn new(req: &Msg, max_responses: u8, dc_name: Option<String>) -> Self {
85        debug_assert!(max_responses >= 1);
86        debug_assert!(usize::from(max_responses) <= MAX_REPLICAS_PER_DC);
87        let max_responses = max_responses.max(1);
88        Self {
89            is_read: req.flags().is_read,
90            max_responses,
91            quorum_responses: max_responses / 2 + 1,
92            error_responses: 0,
93            good: Vec::with_capacity(MAX_REPLICAS_PER_DC),
94            err_rsp: None,
95            msg_id: req.id(),
96            msg_type: req.ty(),
97            dc_name,
98        }
99    }
100
101    /// True when the manager is bound to a read request.
102    ///
103    /// # Examples
104    /// ```
105    /// use dynomite::msg::{Msg, MsgType, ResponseMgr};
106    /// let req = Msg::new(1, MsgType::ReqRedisGet, true);
107    /// assert!(ResponseMgr::new(&req, 1, None).is_read());
108    /// ```
109    #[must_use]
110    pub fn is_read(&self) -> bool {
111        self.is_read
112    }
113
114    /// Highest response count this manager will ever accept.
115    #[must_use]
116    pub fn max_responses(&self) -> u8 {
117        self.max_responses
118    }
119
120    /// Number of good responses required before quorum can be
121    /// declared.
122    #[must_use]
123    pub fn quorum_responses(&self) -> u8 {
124        self.quorum_responses
125    }
126
127    /// Number of accepted (non-error) responses received so far.
128    #[must_use]
129    pub fn good_responses(&self) -> u8 {
130        u8::try_from(self.good.len()).unwrap_or(u8::MAX)
131    }
132
133    /// Number of error responses received so far.
134    #[must_use]
135    pub fn error_responses(&self) -> u8 {
136        self.error_responses
137    }
138
139    /// Number of replies still expected before any decision.
140    #[must_use]
141    pub fn pending_responses(&self) -> u8 {
142        self.max_responses
143            .saturating_sub(self.good_responses())
144            .saturating_sub(self.error_responses)
145    }
146
147    /// Datacenter label this manager was created for.
148    #[must_use]
149    pub fn dc_name(&self) -> Option<&str> {
150        self.dc_name.as_deref()
151    }
152
153    /// Id of the request this manager is bound to.
154    #[must_use]
155    pub fn msg_id(&self) -> MsgId {
156        self.msg_id
157    }
158
159    /// Type tag of the request this manager is bound to.
160    #[must_use]
161    pub fn msg_type(&self) -> MsgType {
162        self.msg_type
163    }
164
165    /// Submit `rsp` paired with its body checksum `checksum`.
166    ///
167    /// Errors are tallied separately; the first error response is
168    /// retained as the canonical error to propagate when no quorum
169    /// is possible. Good responses past `MAX_REPLICAS_PER_DC` are
170    /// dropped to mirror the fixed-size array in the reference
171    /// engine.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use dynomite::msg::{Msg, MsgType, ResponseMgr};
177    ///
178    /// let req = Msg::new(1, MsgType::ReqRedisGet, true);
179    /// let mut mgr = ResponseMgr::new(&req, 1, None);
180    /// let rsp = Msg::new(2, MsgType::RspRedisStatus, false);
181    /// mgr.submit_response(rsp, 0xdead_beef);
182    /// assert_eq!(mgr.good_responses(), 1);
183    /// ```
184    pub fn submit_response(&mut self, rsp: Msg, checksum: u32) {
185        if rsp.flags().is_error {
186            self.error_responses = self.error_responses.saturating_add(1);
187            if self.err_rsp.is_none() {
188                self.err_rsp = Some(Box::new(rsp));
189            }
190            return;
191        }
192        if self.good.len() < MAX_REPLICAS_PER_DC {
193            self.good.push(GoodResponse {
194                msg: Box::new(rsp),
195                checksum,
196            });
197        }
198    }
199
200    /// Determine whether the manager has reached a final decision
201    /// and, if so, what kind. Mirrors `rspmgr_check_is_done` plus
202    /// `rspmgr_is_quorum_achieved`.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use dynomite::msg::{
208    ///     Msg, MsgType, QuorumOutcome, ResponseMgr,
209    /// };
210    ///
211    /// let req = Msg::new(1, MsgType::ReqRedisGet, true);
212    /// let mut mgr = ResponseMgr::new(&req, 1, None);
213    /// assert_eq!(mgr.outcome(), QuorumOutcome::Pending);
214    /// mgr.submit_response(Msg::new(2, MsgType::RspRedisStatus, false), 1);
215    /// assert_eq!(mgr.outcome(), QuorumOutcome::Achieved);
216    /// ```
217    #[must_use]
218    pub fn outcome(&self) -> QuorumOutcome {
219        let good = self.good_responses();
220        let pending = self.pending_responses();
221
222        if good < self.quorum_responses {
223            if pending + good < self.quorum_responses {
224                return QuorumOutcome::Failed;
225            }
226            return QuorumOutcome::Pending;
227        }
228
229        if self.is_quorum_achieved() {
230            return QuorumOutcome::Achieved;
231        }
232
233        if pending == 0 {
234            QuorumOutcome::Mismatched
235        } else {
236            QuorumOutcome::Pending
237        }
238    }
239
240    /// Convenience: true when `outcome` reports anything but
241    /// [`QuorumOutcome::Pending`].
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use dynomite::msg::{Msg, MsgType, ResponseMgr};
247    ///
248    /// let req = Msg::new(1, MsgType::ReqRedisGet, true);
249    /// let mgr = ResponseMgr::new(&req, 3, None);
250    /// assert!(!mgr.is_done());
251    /// ```
252    #[must_use]
253    pub fn is_done(&self) -> bool {
254        !matches!(self.outcome(), QuorumOutcome::Pending)
255    }
256
257    fn is_quorum_achieved(&self) -> bool {
258        let good = self.good_responses();
259        if self.quorum_responses == 1 && good == self.quorum_responses {
260            return true;
261        }
262        if good < self.quorum_responses {
263            return false;
264        }
265        let chk0 = self.good[0].checksum;
266        let chk1 = self.good[1].checksum;
267        if chk0 == chk1 {
268            return true;
269        }
270        if good < 3 {
271            return false;
272        }
273        let chk2 = self.good[2].checksum;
274        chk1 == chk2 || chk0 == chk2
275    }
276
277    /// Borrow the first error response, if any.
278    ///
279    /// # Examples
280    /// ```
281    /// use dynomite::msg::{Msg, MsgType, ResponseMgr};
282    /// let req = Msg::new(1, MsgType::ReqRedisGet, true);
283    /// let mgr = ResponseMgr::new(&req, 1, None);
284    /// assert!(mgr.error_response().is_none());
285    /// ```
286    #[must_use]
287    pub fn error_response(&self) -> Option<&Msg> {
288        self.err_rsp.as_deref()
289    }
290
291    /// Iterate over the accepted responses paired with their
292    /// checksums.
293    pub fn good_iter(&self) -> impl Iterator<Item = (&Msg, u32)> {
294        self.good.iter().map(|g| (g.msg.as_ref(), g.checksum))
295    }
296
297    /// Pick a response to forward to the client according to the
298    /// majority-checksum rule.
299    ///
300    /// Returns `None` when quorum has not been achieved (the caller
301    /// should consult [`Self::outcome`] and propagate the error
302    /// response from [`Self::error_response`]).
303    ///
304    /// # Examples
305    ///
306    /// ```
307    /// use dynomite::msg::{
308    ///     Msg, MsgType, QuorumOutcome, ResponseMgr,
309    /// };
310    ///
311    /// let req = Msg::new(1, MsgType::ReqRedisGet, true);
312    /// let mut mgr = ResponseMgr::new(&req, 3, None);
313    /// for id in 2..=4 {
314    ///     mgr.submit_response(Msg::new(id, MsgType::RspRedisStatus, false), 7);
315    /// }
316    /// assert_eq!(mgr.outcome(), QuorumOutcome::Achieved);
317    /// assert!(mgr.pick_response().is_some());
318    /// ```
319    #[must_use]
320    pub fn pick_response(&self) -> Option<&Msg> {
321        if !matches!(self.outcome(), QuorumOutcome::Achieved) {
322            return None;
323        }
324        match self.good.len() {
325            1 | 2 => Some(self.good[0].msg.as_ref()),
326            3 => {
327                let c0 = self.good[0].checksum;
328                let c1 = self.good[1].checksum;
329                let c2 = self.good[2].checksum;
330                if c0 == c1 {
331                    Some(self.good[0].msg.as_ref())
332                } else if c1 == c2 {
333                    Some(self.good[1].msg.as_ref())
334                } else if c0 == c2 {
335                    Some(self.good[0].msg.as_ref())
336                } else {
337                    None
338                }
339            }
340            _ => None,
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use crate::msg::{Msg, MsgType};
349
350    fn req() -> Msg {
351        let mut m = Msg::new(1, MsgType::ReqRedisGet, true);
352        m.flags_mut().is_read = true;
353        m
354    }
355
356    fn rsp(id: u64, is_error: bool) -> Msg {
357        let mut m = Msg::new(id, MsgType::RspRedisStatus, false);
358        m.flags_mut().is_error = is_error;
359        m
360    }
361
362    #[test]
363    fn dc_one_one_good() {
364        let mut mgr = ResponseMgr::new(&req(), 1, Some("dc1".into()));
365        assert_eq!(mgr.outcome(), QuorumOutcome::Pending);
366        mgr.submit_response(rsp(2, false), 1);
367        assert_eq!(mgr.outcome(), QuorumOutcome::Achieved);
368        assert!(mgr.pick_response().is_some());
369    }
370
371    #[test]
372    fn dc_one_single_error() {
373        let mut mgr = ResponseMgr::new(&req(), 1, None);
374        mgr.submit_response(rsp(2, true), 0);
375        assert_eq!(mgr.outcome(), QuorumOutcome::Failed);
376        assert!(mgr.error_response().is_some());
377        assert!(mgr.pick_response().is_none());
378    }
379
380    #[test]
381    fn dc_quorum_two_matching() {
382        let mut mgr = ResponseMgr::new(&req(), 2, None);
383        assert_eq!(mgr.quorum_responses(), 2);
384        mgr.submit_response(rsp(2, false), 7);
385        assert_eq!(mgr.outcome(), QuorumOutcome::Pending);
386        mgr.submit_response(rsp(3, false), 7);
387        assert_eq!(mgr.outcome(), QuorumOutcome::Achieved);
388    }
389
390    #[test]
391    fn dc_quorum_two_mismatched_no_third_response() {
392        let mut mgr = ResponseMgr::new(&req(), 2, None);
393        mgr.submit_response(rsp(2, false), 7);
394        mgr.submit_response(rsp(3, false), 9);
395        assert_eq!(mgr.outcome(), QuorumOutcome::Mismatched);
396    }
397
398    #[test]
399    fn dc_quorum_one_good_one_error_fails() {
400        let mut mgr = ResponseMgr::new(&req(), 2, None);
401        mgr.submit_response(rsp(2, false), 7);
402        mgr.submit_response(rsp(3, true), 0);
403        assert_eq!(mgr.outcome(), QuorumOutcome::Failed);
404    }
405
406    #[test]
407    fn dc_safe_quorum_three_all_match() {
408        let mut mgr = ResponseMgr::new(&req(), 3, None);
409        assert_eq!(mgr.quorum_responses(), 2);
410        for id in 2..=4 {
411            mgr.submit_response(rsp(id, false), 11);
412        }
413        assert_eq!(mgr.outcome(), QuorumOutcome::Achieved);
414        assert_eq!(mgr.pick_response().unwrap().id(), 2);
415    }
416
417    #[test]
418    fn dc_safe_quorum_two_match_one_dissent() {
419        let mut mgr = ResponseMgr::new(&req(), 3, None);
420        mgr.submit_response(rsp(2, false), 5);
421        mgr.submit_response(rsp(3, false), 5);
422        // Quorum already achieved before the third reply lands.
423        assert_eq!(mgr.outcome(), QuorumOutcome::Achieved);
424        mgr.submit_response(rsp(4, false), 9);
425        assert_eq!(mgr.outcome(), QuorumOutcome::Achieved);
426    }
427
428    #[test]
429    fn dc_safe_quorum_three_disagreeing_mismatched() {
430        let mut mgr = ResponseMgr::new(&req(), 3, None);
431        mgr.submit_response(rsp(2, false), 1);
432        mgr.submit_response(rsp(3, false), 2);
433        // First two disagree, but a third reply is still pending.
434        assert_eq!(mgr.outcome(), QuorumOutcome::Pending);
435        mgr.submit_response(rsp(4, false), 3);
436        assert_eq!(mgr.outcome(), QuorumOutcome::Mismatched);
437        assert!(mgr.pick_response().is_none());
438    }
439
440    #[test]
441    fn dc_safe_quorum_two_errors_fail_immediately() {
442        let mut mgr = ResponseMgr::new(&req(), 3, None);
443        mgr.submit_response(rsp(2, true), 0);
444        mgr.submit_response(rsp(3, true), 0);
445        assert_eq!(mgr.outcome(), QuorumOutcome::Failed);
446    }
447
448    #[test]
449    fn dc_safe_quorum_three_errors_fail() {
450        let mut mgr = ResponseMgr::new(&req(), 3, None);
451        for id in 2..=4 {
452            mgr.submit_response(rsp(id, true), 0);
453        }
454        assert_eq!(mgr.outcome(), QuorumOutcome::Failed);
455    }
456
457    #[test]
458    fn dc_safe_quorum_one_dissent_picks_majority() {
459        let mut mgr = ResponseMgr::new(&req(), 3, None);
460        mgr.submit_response(rsp(2, false), 1);
461        mgr.submit_response(rsp(3, false), 2);
462        mgr.submit_response(rsp(4, false), 2);
463        assert_eq!(mgr.outcome(), QuorumOutcome::Achieved);
464        // chk1 == chk2 -> response index 1 wins.
465        assert_eq!(mgr.pick_response().unwrap().id(), 3);
466    }
467
468    #[test]
469    fn excess_good_responses_are_dropped() {
470        let mut mgr = ResponseMgr::new(&req(), 3, None);
471        for id in 2..=10 {
472            mgr.submit_response(rsp(id, false), 1);
473        }
474        assert_eq!(mgr.good_responses() as usize, MAX_REPLICAS_PER_DC);
475    }
476
477    #[test]
478    fn accessors_report_construction_state() {
479        // The plain accessors echo the values fixed at construction.
480        let mgr = ResponseMgr::new(&req(), 3, Some("dc-east".into()));
481        assert!(mgr.is_read());
482        assert_eq!(mgr.max_responses(), 3);
483        assert_eq!(mgr.error_responses(), 0);
484        assert_eq!(mgr.dc_name(), Some("dc-east"));
485        assert_eq!(mgr.msg_id(), 1);
486        assert_eq!(mgr.msg_type(), MsgType::ReqRedisGet);
487        assert!(mgr.error_response().is_none());
488        // A None dc-name reports None.
489        let anon = ResponseMgr::new(&req(), 1, None);
490        assert_eq!(anon.dc_name(), None);
491    }
492
493    #[test]
494    fn good_iter_yields_msg_and_checksum_pairs() {
495        // good_iter walks the accepted responses with their checksums.
496        let mut mgr = ResponseMgr::new(&req(), 3, None);
497        mgr.submit_response(rsp(2, false), 7);
498        mgr.submit_response(rsp(3, false), 9);
499        let pairs: Vec<(u64, u32)> = mgr.good_iter().map(|(m, c)| (m.id(), c)).collect();
500        assert_eq!(pairs, vec![(2, 7), (3, 9)]);
501    }
502
503    #[test]
504    fn pick_response_three_way_first_and_third_match() {
505        // The c0 == c2 arm: replies 0 and 2 agree, reply 1 dissents.
506        // pick_response returns the index-0 response.
507        let mut mgr = ResponseMgr::new(&req(), 3, None);
508        mgr.submit_response(rsp(2, false), 5);
509        mgr.submit_response(rsp(3, false), 9);
510        mgr.submit_response(rsp(4, false), 5);
511        assert_eq!(mgr.outcome(), QuorumOutcome::Achieved);
512        assert_eq!(mgr.pick_response().unwrap().id(), 2);
513    }
514}