brazier/presence/state
The presence CRDT, ported from Phoenix.Tracker.State.
An observed-remove set of (topic, pid, key) -> meta entries with a
causal context. Every local change bumps this replica’s clock and tags
the entry with #(replica, clock). Removes leave no tombstone: a peer
infers one when a tag it holds is covered by the sender’s context or
clouds but absent from the sender’s values. Clouds hold tags seen out of
order; compact folds contiguous ones into the context.
Every change is also recorded in the delta, which the shard broadcasts
on its next heartbeat and then resets. A peer whose clock falls behind
asks for a transfer and gets a delta generation or a snapshot (see
brazier/presence/delta_generation).
This module is pure. brazier_presence_shard owns one state per shard
and mirrors the visible entries into ETS for reads; the read functions
here scan every value and exist for tests and introspection.
Differences from Phoenix, none of which change the merge results:
- values are keyed by tag, with indices from
(topic, pid, key)to tag and from pid to keys for this replica’s own entries only (the API tracks only processes on the tracking node); - a delta merge finds removes by walking the delta’s clouds instead of every local value, so a heartbeat costs the size of the delta;
- a cloud holds clock values, not whole tags.
Types
The changes since the last reset, covering clocks range_start to
range_end (exclusive start, inclusive end). Phoenix keeps this as a
State in :delta mode.
pub type Delta(r, m) {
Delta(
values: dict.Dict(Tag(r), #(Key, m)),
clouds: dict.Dict(r, set.Set(Int)),
range_start: dict.Dict(r, Int),
range_end: dict.Dict(r, Int),
)
}
Constructors
pub type DeltaError {
NotContiguous
}
Constructors
-
NotContiguousThe two deltas leave a gap between their ranges.
What is tracked. A pid can track several keys per topic.
pub type Key {
Key(topic: String, pid: process.Pid, key: String)
}
Constructors
-
Key(topic: String, pid: process.Pid, key: String)
pub type ReplicaStatus {
Up
Down
}
Constructors
-
Up -
Down
A unique event id: the replica that made the change and its clock value.
pub type Tag(r) {
Tag(replica: r, clock: Int)
}
Constructors
-
Tag(replica: r, clock: Int)
Values
pub fn clocks(state: State(r, m)) -> #(r, dict.Dict(r, Int))
This replica and its context without down replicas. Down replicas are left out because deltas leave them out too: a peer that came up after a replica went down would otherwise ask for a full transfer forever.
pub fn compact(state: State(r, m)) -> State(r, m)
Fold cloud tags that continue the context into it, and drop those it already covers. Empty clouds are removed.
pub fn delta_remove_replica(
delta: Delta(r, m),
replica: r,
) -> Delta(r, m)
Drop a replica from a delta’s range, clouds and values.
pub fn delta_size(delta: Delta(r, m)) -> Int
Values plus cloud tags: what bounds a delta generation.
pub fn extract(
state: State(r, m),
remote_ref: r,
remote_context: dict.Dict(r, Int),
) -> Snapshot(r, m)
The full state a peer with remote_context needs: the replicas it knows,
without its own values.
pub fn extract_delta(
delta: Delta(r, m),
remote_ref: r,
remote_context: dict.Dict(r, Int),
) -> Delta(r, m)
A delta restricted the same way as extract.
pub fn get_by_key(
state: State(r, m),
topic: String,
key: String,
) -> List(Element(r, m))
Visible entries on topic under key, from any pid.
pub fn get_by_pid(
state: State(r, m),
pid: process.Pid,
) -> List(Element(r, m))
This replica’s entries for pid.
pub fn get_by_pid_key(
state: State(r, m),
pid: process.Pid,
topic: String,
key: String,
) -> Result(Element(r, m), Nil)
This replica’s entry for pid under topic and key.
pub fn get_by_topic(
state: State(r, m),
topic: String,
) -> List(Element(r, m))
Visible entries (not from down replicas) on topic.
pub fn join(
state: State(r, m),
pid: process.Pid,
topic: String,
key: String,
meta: m,
) -> Result(#(State(r, m), Element(r, m)), Nil)
Track key for pid on topic. Error(Nil) if already tracked.
pub fn leave(
state: State(r, m),
pid: process.Pid,
topic: String,
key: String,
) -> Result(#(State(r, m), Element(r, m)), Nil)
Stop tracking key for pid on topic. Error(Nil) if not tracked.
pub fn leave_join(
state: State(r, m),
pid: process.Pid,
topic: String,
key: String,
meta: m,
) -> Result(#(State(r, m), Element(r, m), Element(r, m)), Nil)
Replace the meta of a tracked entry: a leave of the old tag and a join
under a new one. Returns the previous and the new element.
Error(Nil) if not tracked.
pub fn leave_pid(
state: State(r, m),
pid: process.Pid,
) -> #(State(r, m), List(Element(r, m)))
Stop tracking everything pid tracks.
pub fn merge(
state: State(r, m),
transfer: Transfer(r, m),
) -> #(State(r, m), List(Element(r, m)), List(Element(r, m)))
Merge a delta or snapshot from a peer. Returns the entries that joined and left. Joins are only taken from replicas this state knows, and leaves only touch other replicas’ entries: a tag the sender has seen (in its context or clouds) but no longer holds was removed.
pub fn merge_deltas(
local: Delta(r, m),
remote: Delta(r, m),
) -> Result(Delta(r, m), DeltaError)
Merge two deltas into one covering both ranges. Phoenix: merge_deltas.
pub fn pid_entry_count(
state: State(r, m),
pid: process.Pid,
) -> Int
The number of this replica’s entries for pid.
pub fn remove_down_replicas(
state: State(r, m),
replica: r,
) -> State(r, m)
Forget a permanently down replica: its values, context and clouds.
pub fn replica_down(
state: State(r, m),
replica: r,
) -> #(State(r, m), List(Element(r, m)), List(Element(r, m)))
Mark replica down. Its values are hidden and returned as leaves; they
stay in the state until remove_down_replicas, so a quick recovery does
not need a transfer.
pub fn replica_status(
state: State(r, m),
replica: r,
) -> Result(ReplicaStatus, Nil)
pub fn replica_up(
state: State(r, m),
replica: r,
) -> #(State(r, m), List(Element(r, m)), List(Element(r, m)))
Mark replica up. Its values become visible and are returned as joins.
pub fn transfer_size(transfer: Transfer(r, m)) -> Int
The number of values a transfer carries: what max_transfer_entries
bounds.