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}