# Guide
This follows the tutorial of the original Python Quoracle.
## Quorum systems
Given nodes `X`, a read-write quorum system is a pair `(R, W)` of sets of
subsets of `X` (read quorums and write quorums) such that every read quorum
intersects every write quorum. Quoracle describes quorums with
expressions: `a + b` is "a or b", `a * b` is "a and b", and
`choose(k, [...])` is "any k of".
```rust
use quoracle::{choose, majority, Expr, Node, QuorumSystem};
# fn main() -> Result<(), quoracle::Error> {
let [a, b, c, d, e, f] =
["a", "b", "c", "d", "e", "f"].map(|x| Expr::Node(Node::new(x)));
// Reads need a full row; writes (the dual) need one node from each row.
let grid = QuorumSystem::from_reads(a.clone() * b.clone() * c.clone()
+ d.clone() * e.clone() * f.clone());
assert_eq!(grid.read_quorums().count(), 2);
assert_eq!(grid.write_quorums().count(), 9);
// Any 2 of 3 for both reads and writes.
let maj = QuorumSystem::from_reads(majority(vec![a.clone(), b.clone(), c.clone()])?);
assert_eq!(maj.resilience(), 1);
// Explicit reads and writes are checked for safety.
assert!(QuorumSystem::new(a.clone() + b.clone(), a.clone()).is_err());
let _ = choose(2, vec![d, e, f])?;
# Ok(())
# }
```
`from_reads` sets the writes to the *dual* of the reads, the smallest
write rule that intersects every read quorum. `from_writes` does the
reverse.
## Resilience
The resilience is the number of nodes that can fail while some quorum is
still alive. `QuorumSystem::resilience` is the minimum of the read and
write resilience.
## Strategies, load, and capacity
A strategy assigns a probability to each read quorum and each write
quorum. A node's load is the fraction of operations it serves, divided by
its capacity. The system's load is the load of its busiest node, and its
capacity is `1 / load`: how much throughput it sustains relative to a
single unit-capacity node.
```rust
use quoracle::{Distribution, Expr, Node, Objective, QuorumSystem, StrategyLimits};
# fn main() -> Result<(), quoracle::Error> {
let none = StrategyLimits::default();
// Optimal for 100% reads: alternate rows, each node serves half of reads.
let fr = Distribution::fixed(1.0)?;
let s = grid.strategy(Objective::Load, Some(&fr), None, &none, 0)?;
assert!((s.load(Some(&fr), None)? - 0.5).abs() < 1e-6);
// Optimize for a mix: 25% reads 1/3 of the time, 75% reads 2/3.
let mix = Distribution::weighted(&[(0.25, 1.0), (0.75, 2.0)])?;
let s = grid.strategy(Objective::Load, Some(&mix), None, &none, 0)?;
println!("{s}");
# Ok(())
# }
```
## Heterogeneous nodes
Capacities are requests per unit time and can differ between reads and
writes.
```rust
use quoracle::{Distribution, Expr, Node, Objective, QuorumSystem, StrategyLimits};
# fn main() -> Result<(), quoracle::Error> {
let [a, c] = ["a", "c"].map(|x| big(x).map(Expr::Node));
let [b, d] = ["b", "d"].map(|x| small(x).map(Expr::Node));
let grid = QuorumSystem::from_reads(a? * b? + c? * d?);
let fr = Distribution::fixed(0.9)?;
let s = grid.strategy(Objective::Load, Some(&fr), None, &StrategyLimits::default(), 0)?;
println!("capacity: {:.0} ops/s", s.capacity(Some(&fr), None)?);
# Ok(())
# }
```
## Network load, latency, and limits
`Objective::Network` minimizes the expected number of nodes contacted;
`Objective::Latency` minimizes expected latency (each node's latency is set
with `Node::with_latency`; a quorum's latency is when its fastest nodes form
a quorum). `StrategyLimits` bounds the metrics you are not optimizing:
```rust
use quoracle::{Distribution, Error, Expr, Node, Objective, QuorumSystem, StrategyLimits};
# fn main() -> Result<(), quoracle::Error> {
let fr = Distribution::fixed(0.5)?;
let limits = StrategyLimits { load: Some(0.75), ..Default::default() };
let s = grid.strategy(Objective::Network, Some(&fr), None, &limits, 0)?;
assert!(s.load(Some(&fr), None)? <= 0.75 + 1e-9);
// Impossible limits are reported, not ignored.
let tight = StrategyLimits { load: Some(0.1), ..Default::default() };
assert_eq!(
grid.strategy(Objective::Network, Some(&fr), None, &tight, 0).unwrap_err(),
Error::NoStrategyFound
);
# Ok(())
# }
```
## f-resilient strategies
With `f > 0`, strategies only use quorums that remain quorums after any
`f` of their nodes fail, so an operation can tolerate `f` slow or failed
nodes without picking a new quorum. This costs capacity.
## Search
`search` tries every quorum system over the given nodes and returns the
best one. It is exhaustive, so use a timeout above about 6 nodes.
```rust
use quoracle::{search, Distribution, Node, SearchConfig};
use std::time::Duration;
# fn main() -> Result<(), quoracle::Error> {
let nodes: Vec<Node<u32>> = (0..4).map(Node::new).collect();
let config = SearchConfig {
resilience: 1,
read_fraction: Some(Distribution::fixed(0.5)?),
timeout: Duration::from_secs(5),
..Default::default()
};
let best = search(&nodes, &config)?;
println!("{}", best.quorum_system);
# Ok(())
# }
```