Skip to main content

quorum_set/quorum/
hierarchical_quorum_set.rs

1use std::collections::BTreeMap;
2use std::collections::BTreeSet;
3use std::collections::btree_set;
4
5use crate::quorum::HierarchicalQuorumSetError;
6use crate::quorum::QuorumIntersection;
7use crate::quorum::QuorumSet;
8
9/// One group of a [`HierarchicalQuorumSet`]: its members, which are IDs and child groups, and
10/// how many of them a set must reach to reach the group.
11///
12/// A set reaches an ID by holding it, and a child group by reaching enough members of that group
13/// in turn. So the groups form a tree whose leaves are IDs, and every level applies the same
14/// test.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct Group<ID> {
17    /// The number of members of the group that an accepted set must reach to reach the group.
18    pub quorum_size: u64,
19
20    /// The IDs that the group holds directly.
21    pub ids: BTreeSet<ID>,
22
23    /// The child groups, by name.
24    pub groups: BTreeMap<String, Group<ID>>,
25}
26
27impl<ID> Group<ID>
28where ID: Ord + Clone
29{
30    /// Returns a group of `ids`, which a set reaches by holding `quorum_size` of them.
31    pub fn of_ids<I>(quorum_size: u64, ids: I) -> Self
32    where I: IntoIterator<Item = ID> {
33        Self {
34            quorum_size,
35            ids: ids.into_iter().collect(),
36            groups: BTreeMap::new(),
37        }
38    }
39
40    /// Returns a group of the child groups `groups`, each given with its name, which a set reaches
41    /// by reaching `quorum_size` of them. A later group replaces an earlier one with the same name.
42    pub fn of_groups<I, K>(quorum_size: u64, groups: I) -> Self
43    where
44        I: IntoIterator<Item = (K, Group<ID>)>,
45        K: Into<String>,
46    {
47        let mut by_name = BTreeMap::new();
48        for (name, group) in groups {
49            by_name.insert(name.into(), group);
50        }
51        Self {
52            quorum_size,
53            ids: BTreeSet::new(),
54            groups: by_name,
55        }
56    }
57
58    /// Returns the number of members: the IDs and the child groups.
59    fn member_count(&self) -> u64 {
60        let member_count = self.ids.len() + self.groups.len();
61        member_count as u64
62    }
63
64    /// Returns whether `candidates` reach this group.
65    fn is_reached_by(&self, candidates: &BTreeSet<&ID>) -> bool {
66        let held_ids = self.ids.iter().filter(|id| candidates.contains(id));
67        let held = held_ids.count() as u64;
68        let reached_groups = self.groups.values().filter(|group| group.is_reached_by(candidates));
69        let reached = reached_groups.count() as u64;
70        held + reached >= self.quorum_size
71    }
72
73    /// Adds the IDs of this group and of every group below it to `ids`.
74    fn collect_ids(&self, ids: &mut BTreeSet<ID>) {
75        ids.extend(self.ids.iter().cloned());
76        for group in self.groups.values() {
77            group.collect_ids(ids);
78        }
79    }
80
81    /// Returns the group with the same members whose quorum size, here and in every group below,
82    /// is `member_count - quorum_size + 1`.
83    fn dual(&self) -> Self {
84        let quorum_size = self.member_count() - self.quorum_size + 1;
85        let mut groups = BTreeMap::new();
86        for (name, group) in self.groups.iter() {
87            groups.insert(name.clone(), group.dual());
88        }
89        Self {
90            quorum_size,
91            ids: self.ids.clone(),
92            groups,
93        }
94    }
95
96    /// Returns whether `self` and `other` hold the same IDs and the same child group names, here
97    /// and in every group below, with two quorum sizes that sum to more than the member count of
98    /// each group.
99    fn proves_intersection(&self, other: &Self) -> bool {
100        let same_ids = self.ids == other.ids;
101        let same_names = self.groups.keys().eq(other.groups.keys());
102        if !same_ids || !same_names {
103            return false;
104        }
105
106        let quorum_sizes = self.quorum_size + other.quorum_size;
107        if quorum_sizes <= self.member_count() {
108            return false;
109        }
110
111        let children = self.groups.values().zip(other.groups.values());
112        for (group, other_group) in children {
113            let proved = group.proves_intersection(other_group);
114            if !proved {
115                return false;
116            }
117        }
118        true
119    }
120}
121
122/// A quorum rule over a tree of groups: it accepts an ID set that reaches the root group.
123///
124/// A [`Group`] holds IDs, child groups, or both: its members. A set reaches a group when the
125/// IDs of the group that it holds, plus the child groups that it reaches, number at least the
126/// quorum size of the group.
127///
128/// Each group has a name that is stable among its siblings, such as the name of a zone, and each
129/// ID belongs to exactly one group. [`new()`](Self::new) checks that every group of the tree
130/// holds a member and has `1 <= quorum_size <= member_count`, and that no ID appears twice in
131/// the tree. So every group can be reached, and the set of all IDs is a quorum.
132///
133/// The intersection check proves `Some(true)` for two rules over the same tree when their quorum
134/// sizes sum to more than the member count of every group, as a rule and its
135/// [`dual()`](Self::dual) do. It answers `None` for every other pair.
136///
137/// # Examples
138///
139/// ```
140/// use std::collections::BTreeMap;
141/// use std::collections::BTreeSet;
142///
143/// use quorum_set::{Group, HierarchicalQuorumSet, QuorumIntersection, QuorumSet};
144///
145/// // Zone a has racks r1 = {1, 2} and r2 = {3, 4}, zone b holds IDs 5 and 6, and ID 7
146/// // sits at the root. A quorum reaches 2 of zone a, zone b and ID 7. It reaches zone a
147/// // through an ID in each rack, and zone b through both of its IDs.
148/// let a = Group::of_groups(2, [
149///     ("r1", Group::of_ids(1, [1, 2])),
150///     ("r2", Group::of_ids(1, [3, 4])),
151/// ]);
152/// let b = Group::of_ids(2, [5, 6]);
153/// let root = Group {
154///     quorum_size: 2,
155///     ids: BTreeSet::from([7]),
156///     groups: BTreeMap::from([("a".to_string(), a), ("b".to_string(), b)]),
157/// };
158/// let rule = HierarchicalQuorumSet::new(root).unwrap();
159///
160/// let accepts = rule.is_quorum([1, 3, 7].iter());
161/// assert!(accepts);
162/// // Rack r2 is lost, so only zone b is reached.
163/// let accepts = rule.is_quorum([1, 2, 5, 6].iter());
164/// assert!(!accepts);
165///
166/// // The quorums {1, 3, 7} and {2, 4, 5, 6} both reach zone a, through other IDs of each
167/// // rack. The check proves nothing, because the two quorum sizes of a rack sum to only its
168/// // member count.
169/// let intersects = rule.intersects_with(&rule);
170/// assert_eq!(None, intersects);
171/// ```
172#[derive(Clone, Debug, PartialEq, Eq)]
173pub struct HierarchicalQuorumSet<ID> {
174    /// The root group, which holds every ID of the rule in its tree.
175    root: Group<ID>,
176}
177
178impl<ID> HierarchicalQuorumSet<ID>
179where ID: Ord + Clone
180{
181    /// Builds a rule that accepts an ID set reaching `root`.
182    ///
183    /// Returns [`HierarchicalQuorumSetError`] for the first problem in a walk of the tree that
184    /// visits a group before the groups below it, and sibling groups in name order: a group that
185    /// holds no member, a group without `1 <= quorum_size <= member_count`, or an ID that a
186    /// visited group already holds.
187    pub fn new(root: Group<ID>) -> Result<Self, HierarchicalQuorumSetError<ID>> {
188        let mut group_of = BTreeMap::new();
189        check_group(&root, &[], &mut group_of)?;
190
191        let rule = Self { root };
192        Ok(rule)
193    }
194
195    /// Returns the root group, which holds every ID of the rule in its tree.
196    pub fn root(&self) -> &Group<ID> {
197        &self.root
198    }
199
200    /// Returns the rule over the same tree that accepts exactly the sets that meet every quorum of
201    /// `self`.
202    ///
203    /// A quorum of `self` that misses a set `S` takes its IDs from outside `S`. Because no ID
204    /// appears twice in the tree, it can reach the members of a group independently: each ID
205    /// outside `S`, and each child group that `S` does not block. `S` blocks a group when it leaves
206    /// fewer than `quorum_size` members reachable, so when it holds or blocks
207    /// `member_count - quorum_size + 1` members: the IDs that it holds and the child groups that
208    /// it blocks. `S` meets every quorum exactly when it blocks the root group. So the result keeps
209    /// the tree and gives every group that number as its quorum size.
210    pub fn dual(&self) -> Self {
211        let root = self.root.dual();
212        Self { root }
213    }
214}
215
216impl<ID> QuorumSet for HierarchicalQuorumSet<ID>
217where ID: Ord + Clone
218{
219    type Id = ID;
220    type Iter = btree_set::IntoIter<ID>;
221
222    fn is_quorum<'a, I>(&self, ids: I) -> bool
223    where
224        ID: 'a,
225        I: Iterator<Item = &'a ID> + Clone,
226    {
227        let candidates: BTreeSet<&ID> = ids.collect();
228        self.root.is_reached_by(&candidates)
229    }
230
231    fn ids(&self) -> Self::Iter {
232        let mut ids = BTreeSet::new();
233        self.root.collect_ids(&mut ids);
234        ids.into_iter()
235    }
236}
237
238/// Sufficient quorum intersection for two rules over the same tree.
239///
240/// A quorum of each rule reaches at least `quorum_size` members of the root group. When the two
241/// quorum sizes sum to more than the member count, two quorums reach a common member. A common
242/// ID is shared by both quorums, and a common child group repeats the argument one level down. So
243/// two quorums share an ID when the two quorum sizes of every group sum to more than its member
244/// count. The [`dual()`](HierarchicalQuorumSet::dual) of a rule gives every group the quorum size
245/// that makes this sum exceed its member count by exactly 1, so the check proves every rule against
246/// its dual. The check runs in O(n).
247impl<ID> QuorumIntersection<HierarchicalQuorumSet<ID>> for HierarchicalQuorumSet<ID>
248where ID: Ord + Clone
249{
250    /// Return `Some(true)` when both rules hold the same IDs and the same child group names in
251    /// every group, and the two quorum sizes of every group sum to more than its member count.
252    ///
253    /// Return `None` otherwise, including for rules over different trees, such as after an ID
254    /// moves to another group: the check proves nothing for them. This check never returns
255    /// `Some(false)`.
256    fn intersects_with(&self, other: &HierarchicalQuorumSet<ID>) -> Option<bool> {
257        let proved = self.root.proves_intersection(&other.root);
258        if proved { Some(true) } else { None }
259    }
260}
261
262/// Returns an error unless `group`, at `path`, and every group below it hold a member and have
263/// `1 <= quorum_size <= member_count`, and no ID appears twice. `group_of` maps each ID of
264/// the groups visited before to the path of its group.
265fn check_group<ID>(
266    group: &Group<ID>,
267    path: &[String],
268    group_of: &mut BTreeMap<ID, Vec<String>>,
269) -> Result<(), HierarchicalQuorumSetError<ID>>
270where
271    ID: Ord + Clone,
272{
273    let member_count = group.member_count();
274    if member_count == 0 {
275        let empty = HierarchicalQuorumSetError::EmptyGroup {
276            group: path.to_vec(),
277        };
278        return Err(empty);
279    }
280
281    let size_fits = (1..=member_count).contains(&group.quorum_size);
282    if !size_fits {
283        let invalid = HierarchicalQuorumSetError::InvalidQuorumSize {
284            group: path.to_vec(),
285            quorum_size: group.quorum_size,
286            member_count,
287        };
288        return Err(invalid);
289    }
290
291    for id in &group.ids {
292        let Some(first) = group_of.insert(id.clone(), path.to_vec()) else {
293            continue;
294        };
295        let shared = HierarchicalQuorumSetError::SharedId {
296            id: id.clone(),
297            first,
298            second: path.to_vec(),
299        };
300        return Err(shared);
301    }
302
303    for (name, child) in group.groups.iter() {
304        let mut child_path = path.to_vec();
305        child_path.push(name.clone());
306        check_group(child, &child_path, group_of)?;
307    }
308    Ok(())
309}