1use crate::distribution::{self, Canonical, Distribution};
9use crate::error::{Error, Result};
10use crate::expr::{minimize, Element, Expr, Node};
11use good_lp::{
12 default_solver, variable, Expression, ProblemVariables, Solution,
13 SolverModel, Variable,
14};
15use hashbrown::{HashMap, HashSet};
16use itertools::Itertools;
17use rand::seq::IndexedRandom;
18use std::collections::BTreeMap;
19use std::time::Duration;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Objective {
24 Load,
26 Network,
28 Latency,
30}
31
32#[derive(Debug, Clone, Copy, Default)]
36pub struct StrategyLimits {
37 pub load: Option<f64>,
39 pub network: Option<f64>,
41 pub latency: Option<Duration>,
43}
44
45impl StrategyLimits {
46 pub(crate) fn check(&self, objective: Objective) -> Result<()> {
48 let conflict = match objective {
49 Objective::Load => self.load.is_some(),
50 Objective::Network => self.network.is_some(),
51 Objective::Latency => self.latency.is_some(),
52 };
53 if conflict {
54 return Err(Error::InvalidQuorumSystem(format!(
55 "a {objective:?} limit cannot be set when optimizing for \
56 {objective:?}"
57 )));
58 }
59 Ok(())
60 }
61}
62
63type Quorums<T> = Vec<HashSet<T>>;
64
65pub type Quorum<T> = Vec<T>;
67
68fn to_quorum<T: Element>(set: &HashSet<T>) -> Quorum<T> {
69 let mut vec: Vec<T> = set.iter().cloned().collect();
70 vec.sort();
71 vec
72}
73
74fn to_set<T: Element>(quorum: &[T]) -> HashSet<T> {
75 quorum.iter().cloned().collect()
76}
77
78#[expect(clippy::cast_precision_loss)]
79fn len_f64(n: usize) -> f64 {
80 n as f64
81}
82
83#[derive(Debug, Clone)]
85pub struct QuorumSystem<T: Element> {
86 reads: Expr<T>,
87 writes: Expr<T>,
88 x_to_node: HashMap<T, Node<T>>,
89}
90
91impl<T: Element> QuorumSystem<T> {
92 pub fn from_reads(reads: Expr<T>) -> Self {
94 let writes = reads.dual();
95 Self::build(reads, writes)
96 }
97
98 pub fn from_writes(writes: Expr<T>) -> Self {
100 let reads = writes.dual();
101 Self::build(reads, writes)
102 }
103
104 pub fn new(reads: Expr<T>, writes: Expr<T>) -> Result<Self> {
111 let optimal_writes = reads.dual();
112 if !writes.quorums().all(|wq| optimal_writes.is_quorum(&wq)) {
113 return Err(Error::NonOverlappingQuorums);
114 }
115 Ok(Self::build(reads, writes))
116 }
117
118 fn build(reads: Expr<T>, writes: Expr<T>) -> Self {
119 let mut x_to_node = HashMap::new();
120 for node in reads.nodes().into_iter().chain(writes.nodes()) {
121 x_to_node.entry(node.x().clone()).or_insert(node);
122 }
123 Self { reads, writes, x_to_node }
124 }
125
126 #[must_use]
128 pub fn reads(&self) -> &Expr<T> {
129 &self.reads
130 }
131
132 #[must_use]
134 pub fn writes(&self) -> &Expr<T> {
135 &self.writes
136 }
137
138 pub fn read_quorums(&self) -> Box<dyn Iterator<Item = HashSet<T>> + '_> {
140 self.reads.quorums()
141 }
142
143 pub fn write_quorums(&self) -> Box<dyn Iterator<Item = HashSet<T>> + '_> {
145 self.writes.quorums()
146 }
147
148 #[must_use]
150 pub fn is_read_quorum(&self, xs: &HashSet<T>) -> bool {
151 self.reads.is_quorum(xs)
152 }
153
154 #[must_use]
156 pub fn is_write_quorum(&self, xs: &HashSet<T>) -> bool {
157 self.writes.is_quorum(xs)
158 }
159
160 pub fn node(&self, x: &T) -> Result<&Node<T>> {
166 self.x_to_node.get(x).ok_or_else(|| {
167 Error::InvalidQuorumSystem(format!(
168 "element {x} not found in quorum system"
169 ))
170 })
171 }
172
173 #[must_use]
175 pub fn nodes(&self) -> HashSet<Node<T>> {
176 self.x_to_node.values().cloned().collect()
177 }
178
179 #[must_use]
181 pub fn elements(&self) -> HashSet<T> {
182 self.x_to_node.keys().cloned().collect()
183 }
184
185 #[must_use]
188 pub fn resilience(&self) -> usize {
189 self.read_resilience().min(self.write_resilience())
190 }
191
192 #[must_use]
194 pub fn read_resilience(&self) -> usize {
195 self.reads.resilience()
196 }
197
198 #[must_use]
200 pub fn write_resilience(&self) -> usize {
201 self.writes.resilience()
202 }
203
204 #[must_use]
206 pub fn dup_free(&self) -> bool {
207 self.reads.dup_free() && self.writes.dup_free()
208 }
209
210 fn candidate_quorums(&self, f: usize) -> Result<(Quorums<T>, Quorums<T>)> {
212 if f == 0 {
213 return Ok((
214 minimize(self.read_quorums().collect()),
215 minimize(self.write_quorums().collect()),
216 ));
217 }
218 let mut xs: Vec<T> = self.elements().into_iter().collect();
219 xs.sort();
220 let rq = f_resilient_quorums(f, &xs, &self.reads);
221 let wq = f_resilient_quorums(f, &xs, &self.writes);
222 if rq.is_empty() || wq.is_empty() {
223 return Err(Error::NoStrategyFound);
224 }
225 Ok((rq, wq))
226 }
227
228 pub fn uniform_strategy(&self, f: usize) -> Result<Strategy<T>> {
236 let (rq, wq) = self.candidate_quorums(f)?;
237 let uniform = |qs: Vec<HashSet<T>>| -> BTreeMap<Quorum<T>, f64> {
238 let p = 1.0 / len_f64(qs.len());
239 qs.iter().map(|q| (to_quorum(q), p)).collect()
240 };
241 Ok(Strategy::new(self, uniform(rq), uniform(wq)))
242 }
243
244 pub fn make_strategy(
256 &self,
257 sigma_r: BTreeMap<Quorum<T>, f64>,
258 sigma_w: BTreeMap<Quorum<T>, f64>,
259 ) -> Result<Strategy<T>> {
260 let r = normalize("sigma_r", sigma_r, |q| self.is_read_quorum(q))?;
261 let w = normalize("sigma_w", sigma_w, |q| self.is_write_quorum(q))?;
262 Ok(Strategy::new(self, r, w))
263 }
264
265 pub fn strategy(
281 &self,
282 objective: Objective,
283 read_fraction: Option<&Distribution>,
284 write_fraction: Option<&Distribution>,
285 limits: &StrategyLimits,
286 f: usize,
287 ) -> Result<Strategy<T>> {
288 limits.check(objective)?;
289 let d = distribution::canonicalize_rw(read_fraction, write_fraction)?;
290 let (rq, wq) = self.candidate_quorums(f)?;
291 self.lp_optimal_strategy(&rq, &wq, &d, objective, limits)
292 }
293
294 fn quorum_latency(&self, quorum: &HashSet<T>, expr: &Expr<T>) -> Duration {
297 quorum_latency(&self.x_to_node, quorum, expr)
298 }
299
300 fn lp_optimal_strategy(
301 &self,
302 read_quorums: &[HashSet<T>],
303 write_quorums: &[HashSet<T>],
304 read_fraction: &Canonical,
305 objective: Objective,
306 limits: &StrategyLimits,
307 ) -> Result<Strategy<T>> {
308 let mut vars = ProblemVariables::new();
309 let r: Vec<Variable> = read_quorums
310 .iter()
311 .map(|_| vars.add(variable().min(0.0).max(1.0)))
312 .collect();
313 let w: Vec<Variable> = write_quorums
314 .iter()
315 .map(|_| vars.add(variable().min(0.0).max(1.0)))
316 .collect();
317 let loads: Vec<(f64, f64, Variable)> = read_fraction
319 .iter()
320 .map(|(fr, &p)| (fr.0, p, vars.add(variable().min(0.0))))
321 .collect();
322
323 let avg_fr: f64 = read_fraction.iter().map(|(k, &p)| k.0 * p).sum();
324 let weighted = |coeffs: &[f64], vs: &[Variable]| -> Expression {
325 coeffs.iter().zip(vs).map(|(&c, &v)| c * v).sum()
326 };
327 let rq_sizes: Vec<f64> =
328 read_quorums.iter().map(|q| len_f64(q.len())).collect();
329 let wq_sizes: Vec<f64> =
330 write_quorums.iter().map(|q| len_f64(q.len())).collect();
331 let network = avg_fr * weighted(&rq_sizes, &r)
332 + (1.0 - avg_fr) * weighted(&wq_sizes, &w);
333 let latency = {
334 let rl: Vec<f64> = read_quorums
335 .iter()
336 .map(|q| self.quorum_latency(q, &self.reads).as_secs_f64())
337 .collect();
338 let wl: Vec<f64> = write_quorums
339 .iter()
340 .map(|q| self.quorum_latency(q, &self.writes).as_secs_f64())
341 .collect();
342 avg_fr * weighted(&rl, &r) + (1.0 - avg_fr) * weighted(&wl, &w)
343 };
344 let load: Expression = loads.iter().map(|&(_, p, l)| p * l).sum();
345
346 let objective_expr = match objective {
347 Objective::Load => load.clone(),
348 Objective::Network => network.clone(),
349 Objective::Latency => latency.clone(),
350 };
351 let mut problem = vars.minimise(objective_expr).using(default_solver);
352 let r_sum: Expression = r.iter().copied().sum();
353 let w_sum: Expression = w.iter().copied().sum();
354 problem = problem.with(r_sum.eq(1.0)).with(w_sum.eq(1.0));
355
356 let mut x_to_r: HashMap<&T, Vec<Variable>> = HashMap::new();
359 for (q, &v) in read_quorums.iter().zip(&r) {
360 for x in q {
361 x_to_r.entry(x).or_default().push(v);
362 }
363 }
364 let mut x_to_w: HashMap<&T, Vec<Variable>> = HashMap::new();
365 for (q, &v) in write_quorums.iter().zip(&w) {
366 for x in q {
367 x_to_w.entry(x).or_default().push(v);
368 }
369 }
370 for &(fr, _, l) in &loads {
371 for node in self.x_to_node.values() {
372 let sum = |m: &HashMap<&T, Vec<Variable>>| -> Expression {
373 m.get(node.x()).into_iter().flatten().copied().sum()
374 };
375 let node_load = (fr / node.read_capacity()) * sum(&x_to_r)
376 + ((1.0 - fr) / node.write_capacity()) * sum(&x_to_w);
377 problem = problem.with(node_load.leq(l));
378 }
379 }
380
381 if let Some(limit) = limits.load {
382 problem = problem.with(load.leq(limit));
383 }
384 if let Some(limit) = limits.network {
385 problem = problem.with(network.leq(limit));
386 }
387 if let Some(limit) = limits.latency {
388 problem = problem.with(latency.leq(limit.as_secs_f64()));
389 }
390
391 let solution = problem.solve()?;
392 let pick = |qs: &[HashSet<T>], vs: &[Variable]| {
393 qs.iter()
394 .zip(vs)
395 .map(|(q, &v)| (to_quorum(q), solution.value(v)))
396 .filter(|&(_, p)| p > 1e-10)
397 .collect::<BTreeMap<_, _>>()
398 };
399 let sigma_r = pick(read_quorums, &r);
400 let sigma_w = pick(write_quorums, &w);
401 let renorm = |m: BTreeMap<Quorum<T>, f64>| {
403 let total: f64 = m.values().sum();
404 m.into_iter().map(|(q, p)| (q, p / total)).collect()
405 };
406 Ok(Strategy::new(self, renorm(sigma_r), renorm(sigma_w)))
407 }
408}
409
410fn normalize<T: Element>(
412 name: &str,
413 sigma: BTreeMap<Quorum<T>, f64>,
414 is_quorum: impl Fn(&HashSet<T>) -> bool,
415) -> Result<BTreeMap<Quorum<T>, f64>> {
416 let invalid =
417 |msg: &str| Error::InvalidQuorumSystem(format!("{name} {msg}"));
418 let mut merged: BTreeMap<Quorum<T>, f64> = BTreeMap::new();
419 for (q, weight) in sigma {
420 if !weight.is_finite() || weight < 0.0 {
421 return Err(invalid("has negative or non-finite weights"));
422 }
423 let set = to_set(&q);
424 if !is_quorum(&set) {
425 return Err(invalid(&format!(
426 "has non-quorum {:?}",
427 to_quorum(&set)
428 )));
429 }
430 *merged.entry(to_quorum(&set)).or_default() += weight;
431 }
432 let total: f64 = merged.values().sum();
433 if !(total.is_finite() && total > 0.0) {
434 return Err(invalid("must have a positive total weight"));
435 }
436 Ok(merged
437 .into_iter()
438 .filter(|&(_, w)| w > 0.0)
439 .map(|(q, w)| (q, w / total))
440 .collect())
441}
442
443fn f_resilient_quorums<T: Element>(
446 f: usize,
447 xs: &[T],
448 expr: &Expr<T>,
449) -> Vec<HashSet<T>> {
450 let mut results = Vec::new();
451 f_resilient_helper(f, xs, expr, &mut HashSet::new(), 0, &mut results);
452 minimize(results)
453}
454
455fn f_resilient_helper<T: Element>(
456 f: usize,
457 xs: &[T],
458 expr: &Expr<T>,
459 current: &mut HashSet<T>,
460 start: usize,
461 results: &mut Vec<HashSet<T>>,
462) {
463 let resilient =
464 current.iter().combinations(f.min(current.len())).all(|failed| {
465 let alive: HashSet<T> = current
466 .iter()
467 .filter(|x| !failed.contains(x))
468 .cloned()
469 .collect();
470 expr.is_quorum(&alive)
471 });
472 if resilient {
473 results.push(current.clone());
474 return;
475 }
476 for j in start..xs.len() {
477 current.insert(xs[j].clone());
478 f_resilient_helper(f, xs, expr, current, j + 1, results);
479 current.remove(&xs[j]);
480 }
481}
482
483fn quorum_latency<T: Element>(
484 x_to_node: &HashMap<T, Node<T>>,
485 quorum: &HashSet<T>,
486 expr: &Expr<T>,
487) -> Duration {
488 let mut nodes: Vec<&Node<T>> =
489 quorum.iter().filter_map(|x| x_to_node.get(x)).collect();
490 nodes.sort_by_key(|n| n.latency());
491 let mut seen = HashSet::new();
492 for node in nodes {
493 seen.insert(node.x().clone());
494 if expr.is_quorum(&seen) {
495 return node.latency();
496 }
497 }
498 Duration::ZERO
500}
501
502impl<T: Element> std::fmt::Display for QuorumSystem<T> {
503 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
504 write!(f, "QuorumSystem(reads={}, writes={})", self.reads, self.writes)
505 }
506}
507
508#[derive(Debug, Clone)]
513pub struct Strategy<T: Element> {
514 sigma_r: BTreeMap<Quorum<T>, f64>,
515 sigma_w: BTreeMap<Quorum<T>, f64>,
516 node_probs: Vec<(Node<T>, f64, f64)>,
518 qs: QuorumSystem<T>,
519}
520
521impl<T: Element> Strategy<T> {
522 fn new(
523 qs: &QuorumSystem<T>,
524 sigma_r: BTreeMap<Quorum<T>, f64>,
525 sigma_w: BTreeMap<Quorum<T>, f64>,
526 ) -> Self {
527 let mut x_read: HashMap<T, f64> = HashMap::new();
528 for (q, &p) in &sigma_r {
529 for x in q {
530 *x_read.entry_ref(x).or_default() += p;
531 }
532 }
533 let mut x_write: HashMap<T, f64> = HashMap::new();
534 for (q, &p) in &sigma_w {
535 for x in q {
536 *x_write.entry_ref(x).or_default() += p;
537 }
538 }
539 let mut node_probs: Vec<(Node<T>, f64, f64)> = qs
540 .x_to_node
541 .values()
542 .map(|n| {
543 let rp = x_read.get(n.x()).copied().unwrap_or(0.0);
544 let wp = x_write.get(n.x()).copied().unwrap_or(0.0);
545 (n.clone(), rp, wp)
546 })
547 .collect();
548 node_probs.sort_by(|a, b| a.0.cmp(&b.0));
549 Self { sigma_r, sigma_w, node_probs, qs: qs.clone() }
550 }
551
552 #[must_use]
554 pub fn quorum_system(&self) -> &QuorumSystem<T> {
555 &self.qs
556 }
557
558 #[must_use]
560 pub fn sigma_r(&self) -> &BTreeMap<Quorum<T>, f64> {
561 &self.sigma_r
562 }
563
564 #[must_use]
566 pub fn sigma_w(&self) -> &BTreeMap<Quorum<T>, f64> {
567 &self.sigma_w
568 }
569
570 #[must_use]
572 pub fn get_read_quorum(&self) -> HashSet<T> {
573 sample_quorum(&self.sigma_r)
574 }
575
576 #[must_use]
578 pub fn get_write_quorum(&self) -> HashSet<T> {
579 sample_quorum(&self.sigma_w)
580 }
581
582 pub fn load(
589 &self,
590 read_fraction: Option<&Distribution>,
591 write_fraction: Option<&Distribution>,
592 ) -> Result<f64> {
593 let d = distribution::canonicalize_rw(read_fraction, write_fraction)?;
594 Ok(d.iter().map(|(fr, &p)| p * self.load_at(fr.0)).sum())
595 }
596
597 pub fn capacity(
604 &self,
605 read_fraction: Option<&Distribution>,
606 write_fraction: Option<&Distribution>,
607 ) -> Result<f64> {
608 let d = distribution::canonicalize_rw(read_fraction, write_fraction)?;
609 Ok(d.iter().map(|(fr, &p)| p / self.load_at(fr.0)).sum())
610 }
611
612 pub fn network_load(
618 &self,
619 read_fraction: Option<&Distribution>,
620 write_fraction: Option<&Distribution>,
621 ) -> Result<f64> {
622 let fr = avg_read_fraction(read_fraction, write_fraction)?;
623 let expected_size = |sigma: &BTreeMap<Quorum<T>, f64>| -> f64 {
624 sigma.iter().map(|(q, &p)| p * len_f64(q.len())).sum()
625 };
626 Ok(fr * expected_size(&self.sigma_r)
627 + (1.0 - fr) * expected_size(&self.sigma_w))
628 }
629
630 pub fn latency(
637 &self,
638 read_fraction: Option<&Distribution>,
639 write_fraction: Option<&Distribution>,
640 ) -> Result<Duration> {
641 let fr = avg_read_fraction(read_fraction, write_fraction)?;
642 let expected = |sigma: &BTreeMap<Quorum<T>, f64>, e: &Expr<T>| -> f64 {
643 sigma
644 .iter()
645 .map(|(q, &p)| {
646 p * self.qs.quorum_latency(&to_set(q), e).as_secs_f64()
647 })
648 .sum()
649 };
650 let secs = fr * expected(&self.sigma_r, &self.qs.reads)
651 + (1.0 - fr) * expected(&self.sigma_w, &self.qs.writes);
652 Ok(Duration::from_secs_f64(secs))
653 }
654
655 fn probs(&self, node: &Node<T>) -> (f64, f64) {
656 self.node_probs
657 .binary_search_by(|(n, _, _)| n.cmp(node))
658 .map_or((0.0, 0.0), |i| {
659 (self.node_probs[i].1, self.node_probs[i].2)
660 })
661 }
662
663 pub fn node_load(
672 &self,
673 node: &Node<T>,
674 read_fraction: Option<&Distribution>,
675 write_fraction: Option<&Distribution>,
676 ) -> Result<f64> {
677 let d = distribution::canonicalize_rw(read_fraction, write_fraction)?;
678 let node = self.qs.x_to_node.get(node.x()).unwrap_or(node);
679 Ok(d.iter().map(|(fr, &p)| p * self.node_load_at(node, fr.0)).sum())
680 }
681
682 pub fn node_utilization(
688 &self,
689 node: &Node<T>,
690 read_fraction: Option<&Distribution>,
691 write_fraction: Option<&Distribution>,
692 ) -> Result<f64> {
693 let d = distribution::canonicalize_rw(read_fraction, write_fraction)?;
694 let node = self.qs.x_to_node.get(node.x()).unwrap_or(node);
695 Ok(d.iter()
696 .map(|(fr, &p)| {
697 p * self.node_load_at(node, fr.0) / self.load_at(fr.0)
698 })
699 .sum())
700 }
701
702 pub fn node_throughput(
709 &self,
710 node: &Node<T>,
711 read_fraction: Option<&Distribution>,
712 write_fraction: Option<&Distribution>,
713 ) -> Result<f64> {
714 let d = distribution::canonicalize_rw(read_fraction, write_fraction)?;
715 let (rp, wp) = self.probs(node);
716 Ok(d.iter()
717 .map(|(fr, &p)| {
718 let cap = 1.0 / self.load_at(fr.0);
719 p * cap * (fr.0 * rp + (1.0 - fr.0) * wp)
720 })
721 .sum())
722 }
723
724 fn load_at(&self, fr: f64) -> f64 {
725 self.node_probs
726 .iter()
727 .map(|(n, rp, wp)| node_load(n, *rp, *wp, fr))
728 .fold(0.0_f64, f64::max)
729 }
730
731 fn node_load_at(&self, node: &Node<T>, fr: f64) -> f64 {
732 let (rp, wp) = self.probs(node);
733 node_load(node, rp, wp, fr)
734 }
735}
736
737fn node_load<T: Element>(node: &Node<T>, rp: f64, wp: f64, fr: f64) -> f64 {
738 fr * rp / node.read_capacity() + (1.0 - fr) * wp / node.write_capacity()
739}
740
741fn avg_read_fraction(
742 read_fraction: Option<&Distribution>,
743 write_fraction: Option<&Distribution>,
744) -> Result<f64> {
745 let d = distribution::canonicalize_rw(read_fraction, write_fraction)?;
746 Ok(d.iter().map(|(k, &p)| k.0 * p).sum())
747}
748
749impl<T: Element> std::fmt::Display for Strategy<T> {
750 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
751 let show = |sigma: &BTreeMap<Quorum<T>, f64>| -> String {
752 sigma
753 .iter()
754 .map(|(q, p)| format!("{{{}}}: {p:.4}", q.iter().join(", ")))
755 .join(", ")
756 };
757 write!(
758 f,
759 "Strategy(reads=[{}], writes=[{}])",
760 show(&self.sigma_r),
761 show(&self.sigma_w)
762 )
763 }
764}
765
766fn sample_quorum<T: Element>(sigma: &BTreeMap<Quorum<T>, f64>) -> HashSet<T> {
769 let entries: Vec<(&Quorum<T>, &f64)> = sigma.iter().collect();
770 entries
771 .choose_weighted(&mut rand::rng(), |(_, &w)| w)
772 .map(|(q, _)| to_set(q))
773 .unwrap_or_default()
774}
775
776#[cfg(test)]
777#[expect(clippy::expect_used, clippy::unwrap_used)]
778mod tests {
779 use super::*;
780
781 fn n(x: &str) -> Expr<String> {
782 Expr::Node(Node::new(x.to_string()))
783 }
784
785 fn node(x: &str) -> Node<String> {
786 Node::new(x.to_string())
787 }
788
789 fn node_with(x: &str, rc: f64, wc: f64, lat: u64) -> Node<String> {
790 Node::new(x.to_string())
791 .with_read_write_capacity(rc, wc)
792 .expect("valid capacity")
793 .with_latency(Duration::from_secs(lat))
794 }
795
796 fn set(items: &[&str]) -> HashSet<String> {
797 items.iter().map(|s| (*s).to_string()).collect()
798 }
799
800 fn quorum(items: &[&str]) -> Quorum<String> {
801 let mut v: Vec<String> =
802 items.iter().map(|s| (*s).to_string()).collect();
803 v.sort();
804 v
805 }
806
807 fn quorum_set(
808 qs: impl Iterator<Item = HashSet<String>>,
809 ) -> HashSet<Vec<String>> {
810 qs.map(|q| {
811 let mut v: Vec<String> = q.into_iter().collect();
812 v.sort();
813 v
814 })
815 .collect()
816 }
817
818 #[test]
821 fn from_reads_generates_dual_writes() {
822 let qs = QuorumSystem::from_reads(n("a") + n("b"));
823 let r = quorum_set(qs.read_quorums());
824 let w = quorum_set(qs.write_quorums());
825 assert!(r.contains(&vec!["a".to_string()]));
826 assert!(r.contains(&vec!["b".to_string()]));
827 assert!(w.contains(&vec!["a".to_string(), "b".to_string()]));
828 }
829
830 #[test]
831 fn from_writes_generates_dual_reads() {
832 let qs = QuorumSystem::from_writes(n("a") + n("b"));
833 let r = quorum_set(qs.read_quorums());
834 let w = quorum_set(qs.write_quorums());
835 assert!(w.contains(&vec!["a".to_string()]));
836 assert!(w.contains(&vec!["b".to_string()]));
837 assert!(r.contains(&vec!["a".to_string(), "b".to_string()]));
838 }
839
840 #[test]
841 fn new_with_valid_overlap() {
842 let qs = QuorumSystem::new(n("a") + n("b"), n("a") * n("b") * n("c"));
843 assert!(qs.is_ok());
844 }
845
846 #[test]
847 fn new_with_no_overlap_fails() {
848 let qs = QuorumSystem::new(n("a") + n("b"), n("a"));
849 assert_eq!(qs.unwrap_err(), Error::NonOverlappingQuorums);
850 }
851
852 #[test]
855 fn elements_returns_all() {
856 let qs = QuorumSystem::from_reads(n("a") + n("b"));
857 let elems = qs.elements();
858 assert!(elems.contains("a"));
859 assert!(elems.contains("b"));
860 }
861
862 #[test]
863 fn nodes_returns_all() {
864 let qs = QuorumSystem::from_reads(n("a") + n("b"));
865 let nodes = qs.nodes();
866 assert_eq!(nodes.len(), 2);
867 }
868
869 #[test]
870 fn is_read_quorum_and_is_write_quorum() {
871 let qs = QuorumSystem::from_reads(n("a") + n("b"));
872 assert!(qs.is_read_quorum(&set(&["a"])));
873 assert!(qs.is_read_quorum(&set(&["b"])));
874 assert!(!qs.is_read_quorum(&set(&["c"])));
875 assert!(qs.is_write_quorum(&set(&["a", "b"])));
876 assert!(!qs.is_write_quorum(&set(&["a"])));
877 }
878
879 #[test]
882 fn resilience_simple() {
883 let qs = QuorumSystem::from_reads(n("a") + n("b"));
884 assert_eq!(qs.resilience(), 0);
885 assert_eq!(qs.read_resilience(), 1);
886 assert_eq!(qs.write_resilience(), 0);
887 }
888
889 #[test]
890 fn dup_free_check() {
891 let qs = QuorumSystem::from_reads(n("a") + n("b"));
892 assert!(qs.dup_free());
893 }
894
895 #[test]
898 fn uniform_strategy_single_node() {
899 let qs = QuorumSystem::from_reads(n("a"));
900 let sigma = qs.uniform_strategy(0).expect("ok");
901 assert_eq!(sigma.sigma_r().len(), 1);
902 assert!((sigma.sigma_r()[&quorum(&["a"])] - 1.0).abs() < f64::EPSILON);
903 assert_eq!(sigma.sigma_w().len(), 1);
904 assert!((sigma.sigma_w()[&quorum(&["a"])] - 1.0).abs() < f64::EPSILON);
905 }
906
907 #[test]
908 fn uniform_strategy_two_nodes() {
909 let qs = QuorumSystem::from_reads(n("a") + n("b"));
910 let sigma = qs.uniform_strategy(0).expect("ok");
911 assert_eq!(sigma.sigma_r().len(), 2);
912 assert!((sigma.sigma_r()[&quorum(&["a"])] - 0.5).abs() < 1e-10);
913 assert!((sigma.sigma_r()[&quorum(&["b"])] - 0.5).abs() < 1e-10);
914 assert_eq!(sigma.sigma_w().len(), 1);
915 assert!(
916 (sigma.sigma_w()[&quorum(&["a", "b"])] - 1.0).abs() < f64::EPSILON
917 );
918 }
919
920 #[test]
921 fn uniform_strategy_grid() {
922 let qs = QuorumSystem::from_reads(n("a") * n("b") + n("c") * n("d"));
923 let sigma = qs.uniform_strategy(0).expect("ok");
924 assert_eq!(sigma.sigma_r().len(), 2);
925 assert!((sigma.sigma_r()[&quorum(&["a", "b"])] - 0.5).abs() < 1e-10);
926 assert!((sigma.sigma_r()[&quorum(&["c", "d"])] - 0.5).abs() < 1e-10);
927 assert_eq!(sigma.sigma_w().len(), 4);
928 for wq in &[
929 quorum(&["a", "c"]),
930 quorum(&["a", "d"]),
931 quorum(&["b", "c"]),
932 quorum(&["b", "d"]),
933 ] {
934 assert!((sigma.sigma_w()[wq] - 0.25).abs() < 1e-10);
935 }
936 }
937
938 #[test]
939 fn uniform_strategy_minimizes() {
940 let qs = QuorumSystem::from_reads(n("a") + n("a") * n("b"));
942 let sigma = qs.uniform_strategy(0).expect("ok");
943 assert_eq!(sigma.sigma_r().len(), 1);
944 assert!((sigma.sigma_r()[&quorum(&["a"])] - 1.0).abs() < f64::EPSILON);
945 }
946
947 #[test]
950 fn make_strategy_normalizes() {
951 let qs = QuorumSystem::from_reads(n("a") * n("b") + n("c") * n("d"));
952 let mut sigma_r = BTreeMap::new();
953 sigma_r.insert(quorum(&["a", "b"]), 25.0);
954 sigma_r.insert(quorum(&["c", "d"]), 75.0);
955 let mut sigma_w = BTreeMap::new();
956 sigma_w.insert(quorum(&["a", "c"]), 1.0);
957 sigma_w.insert(quorum(&["a", "d"]), 1.0);
958 sigma_w.insert(quorum(&["b", "c"]), 1.0);
959 sigma_w.insert(quorum(&["b", "d"]), 1.0);
960
961 let sigma = qs.make_strategy(sigma_r, sigma_w).expect("ok");
962 assert!((sigma.sigma_r()[&quorum(&["a", "b"])] - 0.25).abs() < 1e-10);
963 assert!((sigma.sigma_r()[&quorum(&["c", "d"])] - 0.75).abs() < 1e-10);
964 for wq in &[
965 quorum(&["a", "c"]),
966 quorum(&["a", "d"]),
967 quorum(&["b", "c"]),
968 quorum(&["b", "d"]),
969 ] {
970 assert!((sigma.sigma_w()[wq] - 0.25).abs() < 1e-10);
971 }
972 }
973
974 #[test]
975 fn make_strategy_negative_weights_fail() {
976 let qs = QuorumSystem::from_reads(n("a") * n("b") + n("c") * n("d"));
977 let mut sigma_r = BTreeMap::new();
978 sigma_r.insert(quorum(&["a", "b"]), -1.0);
979 sigma_r.insert(quorum(&["c", "d"]), 1.0);
980 let mut sigma_w = BTreeMap::new();
981 sigma_w.insert(quorum(&["a", "c"]), 1.0);
982
983 assert!(qs.make_strategy(sigma_r, sigma_w).is_err());
984 }
985
986 #[test]
987 fn make_strategy_non_quorum_fails() {
988 let qs = QuorumSystem::from_reads(n("a") * n("b") + n("c") * n("d"));
989 let mut sigma_r = BTreeMap::new();
990 sigma_r.insert(quorum(&["a"]), 1.0); sigma_r.insert(quorum(&["c", "d"]), 1.0);
992 let mut sigma_w = BTreeMap::new();
993 sigma_w.insert(quorum(&["a", "c"]), 1.0);
994
995 assert!(qs.make_strategy(sigma_r, sigma_w).is_err());
996 }
997
998 #[test]
1001 fn strategy_load_and_capacity() {
1002 let a = node_with("a", 50.0, 10.0, 1);
1003 let b = node_with("b", 60.0, 20.0, 2);
1004 let c = node_with("c", 70.0, 30.0, 3);
1005 let d = node_with("d", 80.0, 40.0, 4);
1006
1007 let reads = Expr::Node(a.clone()) * Expr::Node(b.clone())
1008 + Expr::Node(c.clone()) * Expr::Node(d.clone());
1009 let qs = QuorumSystem::from_reads(reads);
1010
1011 let mut sigma_r = BTreeMap::new();
1012 sigma_r.insert(quorum(&["a", "b"]), 0.75);
1013 sigma_r.insert(quorum(&["c", "d"]), 0.25);
1014
1015 let mut sigma_w = BTreeMap::new();
1016 sigma_w.insert(quorum(&["a", "c"]), 0.1);
1017 sigma_w.insert(quorum(&["a", "d"]), 0.2);
1018 sigma_w.insert(quorum(&["b", "c"]), 0.3);
1019 sigma_w.insert(quorum(&["b", "d"]), 0.4);
1020
1021 let sigma = qs.make_strategy(sigma_r, sigma_w).expect("ok");
1022
1023 let fr08 = Distribution::fixed(0.8).expect("ok");
1024
1025 let la = 0.8 / 50.0 * 0.75 + 0.2 / 10.0 * (0.1 + 0.2);
1027 let lb = 0.8 / 60.0 * 0.75 + 0.2 / 20.0 * (0.3 + 0.4);
1028 let lc = 0.8 / 70.0 * 0.25 + 0.2 / 30.0 * (0.1 + 0.3);
1029 let ld = 0.8 / 80.0 * 0.25 + 0.2 / 40.0 * (0.2 + 0.4);
1030
1031 let load_08 = [la, lb, lc, ld].iter().copied().fold(0.0_f64, f64::max);
1032
1033 let got_load = sigma.load(Some(&fr08), None).expect("ok");
1034 assert!(
1035 (got_load - load_08).abs() < 1e-10,
1036 "load mismatch: {got_load} vs {load_08}"
1037 );
1038
1039 let got_cap = sigma.capacity(Some(&fr08), None).expect("ok");
1040 let expected_cap = 1.0 / load_08;
1041 assert!(
1042 (got_cap - expected_cap).abs() < 1e-10,
1043 "capacity mismatch: {got_cap} vs {expected_cap}"
1044 );
1045
1046 let got_node_load = sigma.node_load(&a, Some(&fr08), None).expect("ok");
1048 assert!(
1049 (got_node_load - la).abs() < 1e-10,
1050 "node load mismatch: {got_node_load} vs {la}"
1051 );
1052 }
1053
1054 #[test]
1055 fn strategy_network_load() {
1056 let a = node("a");
1057 let b = node("b");
1058 let c = node("c");
1059 let d = node("d");
1060 let e_node = node("e");
1061
1062 let reads = Expr::Node(a) * Expr::Node(b)
1063 + Expr::Node(c) * Expr::Node(d) * Expr::Node(e_node);
1064 let qs = QuorumSystem::from_reads(reads);
1065
1066 let mut sigma_r = BTreeMap::new();
1067 sigma_r.insert(quorum(&["a", "b"]), 75.0);
1068 sigma_r.insert(quorum(&["c", "d", "e"]), 25.0);
1069
1070 let mut sigma_w = BTreeMap::new();
1071 sigma_w.insert(quorum(&["a", "c"]), 5.0);
1072 sigma_w.insert(quorum(&["a", "d"]), 10.0);
1073 sigma_w.insert(quorum(&["a", "e"]), 15.0);
1074 sigma_w.insert(quorum(&["b", "c"]), 20.0);
1075 sigma_w.insert(quorum(&["b", "d"]), 25.0);
1076 sigma_w.insert(quorum(&["b", "e"]), 25.0);
1077
1078 let sigma = qs.make_strategy(sigma_r, sigma_w).expect("ok");
1079 let fr08 = Distribution::fixed(0.8).expect("ok");
1080
1081 let expected = 0.8 * 0.75 * 2.0 + 0.8 * 0.25 * 3.0 + 0.2 * 2.0;
1082 let got = sigma.network_load(Some(&fr08), None).expect("ok");
1083 assert!(
1084 (got - expected).abs() < 1e-10,
1085 "network load mismatch: {got} vs {expected}"
1086 );
1087 }
1088
1089 #[test]
1090 #[expect(clippy::many_single_char_names)]
1091 fn strategy_latency() {
1092 let a = node_with("a", 1.0, 1.0, 1);
1093 let b = node_with("b", 1.0, 1.0, 2);
1094 let c = node_with("c", 1.0, 1.0, 3);
1095 let d = node_with("d", 1.0, 1.0, 4);
1096 let e = node_with("e", 1.0, 1.0, 5);
1097
1098 let reads = Expr::Node(a) * Expr::Node(b.clone())
1099 + Expr::Node(c.clone())
1100 * Expr::Node(d.clone())
1101 * Expr::Node(e.clone());
1102 let qs = QuorumSystem::from_reads(reads);
1103
1104 let mut sigma_r = BTreeMap::new();
1105 sigma_r.insert(quorum(&["a", "b"]), 10.0);
1106 sigma_r.insert(quorum(&["a", "b", "c"]), 20.0);
1107 sigma_r.insert(quorum(&["c", "d", "e"]), 30.0);
1108 sigma_r.insert(quorum(&["c", "d", "e", "a"]), 40.0);
1109
1110 let mut sigma_w = BTreeMap::new();
1111 sigma_w.insert(quorum(&["a", "c"]), 5.0);
1112 sigma_w.insert(quorum(&["a", "d"]), 10.0);
1113 sigma_w.insert(quorum(&["a", "e"]), 15.0);
1114 sigma_w.insert(quorum(&["b", "c"]), 20.0);
1115 sigma_w.insert(quorum(&["b", "d"]), 25.0);
1116 sigma_w.insert(quorum(&["b", "e"]), 25.0);
1117
1118 let sigma = qs.make_strategy(sigma_r, sigma_w).expect("ok");
1119 let fr08 = Distribution::fixed(0.8).expect("ok");
1120
1121 let expected_secs = 0.8 * 0.10 * 2.0
1122 + 0.8 * 0.20 * 2.0
1123 + 0.8 * 0.30 * 5.0
1124 + 0.8 * 0.40 * 5.0
1125 + 0.2 * 0.05 * 3.0
1126 + 0.2 * 0.10 * 4.0
1127 + 0.2 * 0.15 * 5.0
1128 + 0.2 * 0.20 * 3.0
1129 + 0.2 * 0.25 * 4.0
1130 + 0.2 * 0.25 * 5.0;
1131
1132 let got = sigma.latency(Some(&fr08), None).expect("ok").as_secs_f64();
1133 assert!(
1134 (got - expected_secs).abs() < 1e-10,
1135 "latency mismatch: {got} vs {expected_secs}"
1136 );
1137 }
1138
1139 #[test]
1142 fn minimize_removes_supersets() {
1143 let sets = vec![
1144 set(&["a"]),
1145 set(&["a", "b"]),
1146 set(&["c"]),
1147 set(&["a", "b", "c"]),
1148 ];
1149 let result = minimize(sets);
1150 assert_eq!(result.len(), 2);
1151 assert!(result.contains(&set(&["a"])));
1152 assert!(result.contains(&set(&["c"])));
1153 }
1154
1155 #[test]
1158 fn quorum_system_display() {
1159 let qs = QuorumSystem::from_reads(n("a") + n("b"));
1160 let s = format!("{qs}");
1161 assert!(s.contains("QuorumSystem"));
1162 }
1163
1164 fn s(secs: u64) -> Duration {
1167 Duration::from_secs(secs)
1168 }
1169
1170 fn fixed(fr: f64) -> Distribution {
1171 Distribution::fixed(fr).expect("valid")
1172 }
1173
1174 fn opt(
1175 qs: &QuorumSystem<String>,
1176 objective: Objective,
1177 fr: f64,
1178 limits: StrategyLimits,
1179 f: usize,
1180 ) -> Result<Strategy<String>> {
1181 qs.strategy(objective, Some(&fixed(fr)), None, &limits, f)
1182 }
1183
1184 fn close(a: f64, b: f64) -> bool {
1185 (a - b).abs() < 1e-6
1186 }
1187
1188 fn python_grid() -> QuorumSystem<String> {
1189 let a = node_with("a", 2.0, 1.0, 1);
1190 let b = node_with("b", 2.0, 1.0, 2);
1191 let c = node_with("c", 2.0, 1.0, 3);
1192 let d = node_with("d", 2.0, 1.0, 4);
1193 QuorumSystem::from_reads(
1194 Expr::Node(a) * Expr::Node(b) + Expr::Node(c) * Expr::Node(d),
1195 )
1196 }
1197
1198 #[test]
1199 fn python_parity_load_optimized() {
1200 let qs = python_grid();
1201 let none = StrategyLimits::default();
1202 let net2 = StrategyLimits { network: Some(2.0), ..none };
1203 let lat4 = StrategyLimits { latency: Some(s(4)), ..none };
1204 for limits in [none, net2, lat4] {
1205 for (fr, load) in [(1.0, 0.25), (0.0, 0.5)] {
1206 let sigma = opt(&qs, Objective::Load, fr, limits, 0).unwrap();
1207 let d = fixed(fr);
1208 assert!(close(sigma.load(Some(&d), None).unwrap(), load));
1209 let cap = sigma.capacity(Some(&d), None).unwrap();
1210 assert!(close(cap, 1.0 / load));
1211 }
1212 }
1213 }
1214
1215 #[test]
1216 fn python_parity_network_and_latency_optimized() {
1217 let qs = python_grid();
1218 let none = StrategyLimits::default();
1219 for (fr, limits) in [
1220 (1.0, none),
1221 (0.0, none),
1222 (1.0, StrategyLimits { load: Some(0.25), ..none }),
1223 (0.0, StrategyLimits { load: Some(0.5), ..none }),
1224 (1.0, StrategyLimits { latency: Some(s(2)), ..none }),
1225 (0.0, StrategyLimits { latency: Some(s(3)), ..none }),
1226 ] {
1227 let sigma = opt(&qs, Objective::Network, fr, limits, 0).unwrap();
1228 let net = sigma.network_load(Some(&fixed(fr)), None).unwrap();
1229 assert!(close(net, 2.0), "fr={fr} {limits:?}: {net}");
1230 }
1231 for (fr, lat, limits) in [
1232 (1.0, 2, none),
1233 (0.0, 3, none),
1234 (1.0, 2, StrategyLimits { load: Some(1.0), ..none }),
1235 (0.0, 3, StrategyLimits { load: Some(1.0), ..none }),
1236 (1.0, 2, StrategyLimits { network: Some(2.0), ..none }),
1237 (0.0, 3, StrategyLimits { network: Some(2.0), ..none }),
1238 ] {
1239 let sigma = opt(&qs, Objective::Latency, fr, limits, 0).unwrap();
1240 let got = sigma.latency(Some(&fixed(fr)), None).unwrap();
1241 assert!(close(got.as_secs_f64(), s(lat).as_secs_f64()));
1242 }
1243 }
1244
1245 #[test]
1246 fn python_parity_one_resilient() {
1247 let qs = python_grid();
1248 let none = StrategyLimits::default();
1249 for (fr, load) in [(1.0, 0.5), (0.0, 1.0)] {
1250 let sigma = opt(&qs, Objective::Load, fr, none, 1).unwrap();
1251 assert!(close(sigma.load(Some(&fixed(fr)), None).unwrap(), load));
1252 }
1253 for fr in [1.0, 0.0] {
1254 let sigma = opt(&qs, Objective::Network, fr, none, 1).unwrap();
1255 let net = sigma.network_load(Some(&fixed(fr)), None).unwrap();
1256 assert!(close(net, 4.0));
1257 }
1258 for (fr, lat) in [(1.0, 2.0), (0.0, 3.0)] {
1259 let sigma = opt(&qs, Objective::Latency, fr, none, 1).unwrap();
1260 let got = sigma.latency(Some(&fixed(fr)), None).unwrap();
1261 assert!(close(got.as_secs_f64(), lat));
1262 }
1263 }
1264
1265 #[test]
1266 fn python_parity_illegal_and_unsatisfiable() {
1267 let qs = python_grid();
1268 let none = StrategyLimits::default();
1269 for (objective, limits) in [
1270 (Objective::Load, StrategyLimits { load: Some(1.0), ..none }),
1271 (Objective::Network, StrategyLimits { network: Some(2.0), ..none }),
1272 (
1273 Objective::Latency,
1274 StrategyLimits { latency: Some(s(5)), ..none },
1275 ),
1276 ] {
1277 assert!(matches!(
1278 opt(&qs, objective, 0.1, limits, 0),
1279 Err(Error::InvalidQuorumSystem(_))
1280 ));
1281 }
1282 for (objective, fr, limits) in [
1283 (
1284 Objective::Load,
1285 0.0,
1286 StrategyLimits { network: Some(1.5), ..none },
1287 ),
1288 (
1289 Objective::Load,
1290 0.0,
1291 StrategyLimits { latency: Some(s(1)), ..none },
1292 ),
1293 (
1294 Objective::Network,
1295 1.0,
1296 StrategyLimits {
1297 load: Some(0.25),
1298 latency: Some(s(2)),
1299 ..none
1300 },
1301 ),
1302 ] {
1303 assert_eq!(
1304 opt(&qs, objective, fr, limits, 0).unwrap_err(),
1305 Error::NoStrategyFound
1306 );
1307 }
1308 }
1309
1310 #[test]
1311 fn python_parity_uniform_one_resilient() {
1312 let qs = QuorumSystem::from_reads(
1314 n("a") * n("b") + n("c") * n("d") + n("e") * n("f"),
1315 );
1316 let sigma = qs.uniform_strategy(1).unwrap();
1317 let want_r = [
1318 quorum(&["a", "b", "c", "d"]),
1319 quorum(&["a", "b", "e", "f"]),
1320 quorum(&["c", "d", "e", "f"]),
1321 ];
1322 assert_eq!(sigma.sigma_r().len(), want_r.len());
1323 for q in &want_r {
1324 assert!(close(sigma.sigma_r()[q], 1.0 / 3.0));
1325 }
1326 assert_eq!(sigma.sigma_w().len(), 1);
1328 assert!(close(
1329 sigma.sigma_w()[&quorum(&["a", "b", "c", "d", "e", "f"])],
1330 1.0
1331 ));
1332 }
1333
1334 #[test]
1337 fn f_resilient_impossible_is_no_strategy() {
1338 let qs = QuorumSystem::from_reads(n("a") * n("b"));
1339 assert_eq!(qs.uniform_strategy(1).unwrap_err(), Error::NoStrategyFound);
1340 assert_eq!(
1341 opt(&qs, Objective::Load, 0.5, StrategyLimits::default(), 1)
1342 .unwrap_err(),
1343 Error::NoStrategyFound
1344 );
1345 }
1346
1347 #[test]
1348 fn make_strategy_merges_unsorted_and_duplicate_keys() {
1349 let qs = QuorumSystem::from_reads(n("a") + n("b"));
1350 let mut r = BTreeMap::new();
1351 r.insert(vec!["a".to_string(), "a".to_string()], 1.0);
1352 r.insert(quorum(&["a"]), 1.0);
1353 r.insert(quorum(&["b"]), 2.0);
1354 r.insert(quorum(&["b", "a"]).into_iter().rev().collect(), 0.0);
1355 let mut w = BTreeMap::new();
1356 w.insert(vec!["b".to_string(), "a".to_string()], 1.0);
1357 let sigma = qs.make_strategy(r, w).unwrap();
1358 assert_eq!(sigma.sigma_r().len(), 2);
1359 assert!(close(sigma.sigma_r()[&quorum(&["a"])], 0.5));
1360 let load_a = sigma.node_load(&node("a"), Some(&fixed(1.0)), None);
1361 assert!(close(load_a.unwrap(), 0.5));
1362 assert_eq!(sigma.sigma_w().keys().next(), Some(&quorum(&["a", "b"])));
1363 }
1364
1365 #[test]
1366 fn make_strategy_rejects_bad_weights() {
1367 let qs = QuorumSystem::from_reads(n("a") + n("b"));
1368 let one = |q: &[&str], w: f64| {
1369 let mut m = BTreeMap::new();
1370 m.insert(quorum(q), w);
1371 m
1372 };
1373 let w = || one(&["a", "b"], 1.0);
1374 assert!(qs.make_strategy(BTreeMap::new(), w()).is_err());
1375 assert!(qs.make_strategy(one(&["a"], 0.0), w()).is_err());
1376 assert!(qs.make_strategy(one(&["a"], f64::NAN), w()).is_err());
1377 assert!(qs.make_strategy(one(&["a"], f64::INFINITY), w()).is_err());
1378 assert!(qs.make_strategy(one(&["a"], 1.0), one(&["a"], 1.0)).is_err());
1379 assert!(qs
1380 .make_strategy(one(&["a"], 1.0), one(&["a", "b"], -1.0))
1381 .is_err());
1382 }
1383
1384 #[test]
1385 fn node_metrics_use_system_capacities() {
1386 let a = node_with("a", 2.0, 2.0, 1);
1387 let qs = QuorumSystem::from_reads(Expr::Node(a) + n("b"));
1388 let sigma = qs.uniform_strategy(0).unwrap();
1389 let fr = fixed(1.0);
1390 let la = sigma.node_load(&node("a"), Some(&fr), None).unwrap();
1392 assert!(close(la, 0.25));
1393 let lb = sigma.node_load(&node("b"), Some(&fr), None).unwrap();
1394 assert!(close(lb, 0.5));
1395 let ua = sigma.node_utilization(&node("a"), Some(&fr), None).unwrap();
1396 assert!(close(ua, 0.5));
1397 let tb = sigma.node_throughput(&node("b"), Some(&fr), None).unwrap();
1398 assert!(close(tb, 1.0));
1399 assert!(close(
1400 sigma.node_load(&node("zz"), Some(&fr), None).unwrap(),
1401 0.0
1402 ));
1403 assert!(sigma.load(None, None).is_err());
1404 assert!(sigma.capacity(Some(&fr), Some(&fr)).is_err());
1405 assert!(sigma.network_load(None, None).is_err());
1406 assert!(sigma.latency(None, None).is_err());
1407 assert!(sigma.node_load(&node("a"), None, None).is_err());
1408 assert!(sigma.node_utilization(&node("a"), None, None).is_err());
1409 assert!(sigma.node_throughput(&node("a"), None, None).is_err());
1410 }
1411
1412 #[test]
1413 fn accessors_and_sampling() {
1414 let qs = QuorumSystem::from_reads(n("a") * n("b") + n("c"));
1415 assert_eq!(qs.reads().to_string(), "((a * b) + c)");
1416 assert_eq!(qs.writes().to_string(), "((a + b) * c)");
1417 assert_eq!(qs.node(&"a".to_string()).unwrap().x(), "a");
1418 assert!(qs.node(&"zz".to_string()).is_err());
1419 let sigma = qs.uniform_strategy(0).unwrap();
1420 assert_eq!(sigma.quorum_system().elements(), qs.elements());
1421 for _ in 0..20 {
1422 assert!(qs.is_read_quorum(&sigma.get_read_quorum()));
1423 assert!(qs.is_write_quorum(&sigma.get_write_quorum()));
1424 }
1425 let shown = sigma.to_string();
1426 assert!(shown.contains("{a, b}: 0.5000"), "{shown}");
1427 }
1428
1429 #[test]
1430 fn strategy_with_write_fraction_and_weighted() {
1431 let qs = QuorumSystem::from_reads(n("a") + n("b") + n("c"));
1432 let none = StrategyLimits::default();
1433 let w = fixed(0.25);
1434 let by_w =
1435 qs.strategy(Objective::Load, None, Some(&w), &none, 0).unwrap();
1436 let by_r = opt(&qs, Objective::Load, 0.75, none, 0).unwrap();
1437 let l1 = by_w.load(None, Some(&w)).unwrap();
1438 let l2 = by_r.load(Some(&fixed(0.75)), None).unwrap();
1439 assert!(close(l1, l2));
1440 let d = Distribution::weighted(&[(0.0, 1.0), (1.0, 1.0)]).unwrap();
1441 let sigma =
1442 qs.strategy(Objective::Load, Some(&d), None, &none, 0).unwrap();
1443 assert!(sigma.load(Some(&d), None).unwrap() > 0.0);
1444 assert!(qs.strategy(Objective::Load, None, None, &none, 0).is_err());
1445 }
1446}