Crate cvrdt_exposition
source ·Expand description
Understanding Convergent Replicated Data Types
I wanted to understand CRDTs more, so I put together this Rust library for state-based CRDTs a.k.a. convergent replicated data types a.k.a. CvRDTs
.
It aims to present an explicit (with strong types, etc.) and unified description of the CvRDTs
presented in the wonderful paper A comprehensive study of Convergent and Commutative Replicated Data Types.
Do not use this!
This code is solely for my own edification and is not meant for production use.
There are already much better options for usable CRDTs in Rust; see the rust-crdt
project.
What makes a CvRDT
?
Quoting the Wikipedia article on CRDTs,
CvRDTs
send their full local state to other replicas, where the states are merged by a function which must be commutative, associative, and idempotent.
So suppose we’ve just written a brand new data type, and we’d like to demonstrate it’s a CvRDT
.
Suppose further that x, y, and z are any arbitrary members of our data type, and that our data type has a merge function called merge; for our type to be a CvRDT
, we need the following three things to be true for any values of x, y, and z:
- merge(x, y) = merge(y, x)
- merge(x, merge(y, z)) = merge(merge(x, y), z)
- merge(merge(x, y), y) = merge(x, y)
Wikipedia continues,
The merge function provides a join for any pair of replica states, so the set of all states forms a semilattice.
Our merge function merge induces a partial order on all the elements of our type, akin to Rust’s PartialOrd
trait, where x ≤ y if merge(x, y) = y.
Since for any two elements x and y, both x ≤ merge(x, y) and y ≤ merge(x, y), and merge(x, y) is the “smallest” such elment for which this occurs, it is the least upper bound (or join) of x and y.
The article goes on
The update function must monotonically increase the internal state, according to the same partial order rules as the semilattice.
Suppose our type has an update function which we’ll call add. For any element x and update value u, we need x ≤ update(x, u); that is, we need merge(x, update(x, u)) = update(x, u).
Many types of CvRDT—in this library especially—are grow-only; they can add new elements/values/etc., but once added, these cannot be removed. Some types allow removal as well; if our type does so, we must ensure that removal maintains the partial order given by our merge function. That is, we need the internal state to increase monotonically (as Wikipedia says in the above quote) even in the face of removals. Suppose our type has a function
Ideally we’d check these requirements for every possible configuration of our new CvRDT
implementation.
This can be impossible to do via brute force, as the number of states to check can quickly get prohibitively large.
See the penultimate section for how cvrdt-exposition
verifies these properties.
Examples
Per the Wikipedia article, consider a one-way boolean flag that, once true, can never revert to false:
use cvrdt_exposition::{Grow, OneWayBoolean};
let x = OneWayBoolean::new(false);
let mut y = OneWayBoolean::new(false);
y.add(());
assert_eq!(y.payload(), true);
assert_eq!(y.merge(&x).payload(), true);
As the internal state of a OneWayBoolean
is only a single boolean value, we could verify the implementation fulfills the CvRDT
requirements by hand (though I’d rather get my computer to do it! see below).
How cvrdt-exposition
verifies properties
In the absence of using formal methods like TLA+ (see also Hillel Wayne’s excellent book!), we resort to property-based testing via the proptest
crate.
This excellent crate is listed as a dev-dependency
in cvrdt-exposition
’s Cargo.tml
file, so if you can just use the stuff in this library (although you shouldn’t!) without pulling in an extra dependency.
That said, I highly recommend learning to use proptest
, quickcheck
, or some other property testing framework for Rust.
In the cfg(test)
-only properties
module, cvrdt-exposition
defines macros for automating checking CvRDT
properties.
For instance, given a function arb_cvrdt2
that yields an arbitrary pair of elements of our CvRDT
type, proptest
’s proptest!
macro allows us to test that our CvRDT
’s merge function is commutative via:
proptest! {
#[test]
fn merge_commutative((x, y) in $arb_cvrdt2()) {
prop_assert_eq!(
Grow::payload(&Grow::merge(&x, &y)),
Grow::payload(&Grow::merge(&y, &x))
);
}
};
Note that the above code uses the cvrdt-exposition
nomenclature Grow
, which we use to distinguish CvRDTs
that are grow-only from those that can also remove elements.
See the traits documentation for more.
References
Re-exports
pub use crate::g_counter::GCounter;
pub use crate::g_set::GSet;
pub use crate::lww_register::LWWRegister;
pub use crate::one_way_boolean::OneWayBoolean;
pub use crate::pn_counter::PNCounter;
pub use crate::traits::Grow;
pub use crate::traits::Shrink;
pub use crate::two_phase_set::TwoPhaseSet;
Modules
- Grow-Only Counter
- Grow-Only Set
- Last-Writer-Wins Register
- The simplest
CvRDT
example: a boolean flag that, once true, can never revert to false - Positive-Negative Counter
- Our two traits defining
CvRDTs
- Two-Phase Set