Packages

Vector clocks for distributed systems.

Current section

Files

Jump to
vector_clock lib vector_clock.ex
Raw

lib/vector_clock.ex

defmodule VectorClock do
@moduledoc """
Elixir implementation of vector clocks.
## About
Vector clocks are used in distributed systems as a way of maintaining a
logical ordering of events. A vector clock consists of a list of dots,
which each dot representing a node in a distributed system. A dot consists
of an identifier for the node, it's current count, and a timestamp from
the last time it was incremented. When a node sends an event to another
node it increments the it's dot in it's vector clock and sends the clock
along side the message. A node receiving a message can determine whether
it has seen the effect of that message already by comparing it's vector clock
with the received vector clock.
## Source
Based on the erlang version from
[`:riak_core`](https://github.com/basho/riak_core/blob/develop/src/vclock.erl).
"""
@opaque t :: [dot]
@opaque dot :: Dot.t
@opaque pure_dot :: PureDot.t
@type vclock_node :: term
@type counter :: integer
@type timestamp :: integer
defmodule Dot do
@moduledoc false
@type t :: %__MODULE__{
node: VectorClock.vclock_node,
counter: VectorClock.counter,
timestamp: VectorClock.timestamp
}
defstruct [:node, :counter, :timestamp]
end
defmodule PureDot do
@moduledoc false
@type t :: %__MODULE__{
node: VectorClock.vclock_node,
counter: VectorClock.counter
}
defstruct [:node, :counter]
end
@doc """
Create a new empty vector clock.
"""
@spec fresh() :: t
def fresh, do: []
@doc """
Create a new vectory clock with an initial dot.
"""
@spec fresh(vclock_node, counter) :: t
def fresh(node, count) do
[%Dot{node: node, counter: count, timestamp: timestamp()}]
end
@doc """
Check if vector clock `va` is a descendent of vector clock `vb`.
"""
@spec descends(t, t) :: boolean
def descends(_va, []) do
true # all vectory clocks descend from the empty vector clock
end
def descends(va, vb) do
[%{node: node_b, counter: counter_b}|rest_b] = vb
case find_dot(va, node_b) do
nil ->
false
%{counter: counter_a} ->
(counter_a >= counter_b) and descends(va, rest_b)
end
end
@doc """
Check whether a vector clock decends from a given dot.
"""
@spec descends_dot(t, dot) :: boolean
def descends_dot(vclock, dot) do
descends(vclock, [dot])
end
@doc """
Converts a dot to a pure dot, for when timestamp data is not needed.
"""
@spec pure_dot(dot) :: pure_dot
def pure_dot(dot) do
%PureDot{node: dot.node, counter: dot.counter}
end
@doc """
Checks if vector clock `va` strictly dominates vector clock `vb`.
A vector clock is said to dominate another when it's clock represents a later
logical time than the other.
"""
@spec dominates(t, t) :: boolean
def dominates(va, vb) do
descends(va, vb) and not descends(vb, va)
end
@doc """
Combines a list of vector clocks into their least possible common descendant.
"""
@spec merge([t]) :: t
def merge([]), do: []
def merge([single]), do: single
def merge([head|tail]), do: merge(tail, sort_vclock(head))
defp merge([], vclock), do: vclock
defp merge([va|rest], vclock) do
sorted_vclock = sort_vclock(va)
merge(rest, merge(sorted_vclock, vclock, []))
end
defp merge([], [], acc), do: :lists.reverse(acc)
defp merge([], left, acc), do: :lists.reverse(acc, left)
defp merge(left, [], acc), do: :lists.reverse(acc, left)
defp merge([%{node: node1} = dot|vclock],
n = [%{node: node2}|_], acc) when node1 < node2 do
merge(vclock, n, [dot|acc])
end
defp merge(v = [%{node: node1}|_],
[%{node: node2} = dot|nclock], acc) when node1 > node2 do
merge(v, nclock, [dot|acc])
end
defp merge([vdot|vclock], [ndot|nclock], acc) do
{counter, timestamp} = cond do
vdot.counter > ndot.counter -> {vdot.counter, vdot.timestamp}
vdot.counter < ndot.counter -> {ndot.counter, ndot.timestamp}
true -> {vdot.counter, max(vdot.timestamp, ndot.timestamp)}
end
merge(vclock, nclock, [%{vdot| counter: counter, timestamp: timestamp}|acc])
end
@doc """
Get the counter value from a vector clock for a specific node.
"""
@spec get_counter(t, vclock_node) :: counter
def get_counter(vclock, node) do
case find_dot(vclock, node) do
nil -> 0
dot -> dot.counter
end
end
@doc """
Get the timestamp value from a vector clock for a specific node.
"""
@spec get_timestamp(t, vclock_node) :: timestamp | nil
def get_timestamp(vclock, node) do
case find_dot(vclock, node) do
nil -> nil
dot -> dot.timestamp
end
end
@doc """
Get the dot entry from a vector clock for a specific node.
"""
@spec get_dot(t, vclock_node) :: {:ok, dot} | {:error, :not_found}
def get_dot(vclock, node) do
case find_dot(vclock, node) do
nil -> {:error, :not_found}
dot -> {:ok, dot}
end
end
@doc """
Checks if the given argument is a valid dot.
"""
@spec valid_dot?(term) :: boolean
def valid_dot?(%Dot{counter: cnt, timestamp: ts}) when is_integer(cnt) and is_integer(ts) do
true
end
def valid_dot?(_), do: false
@doc """
Increment the vector clock at node.
"""
@spec increment(t, vclock_node) :: t
def increment(vclock, node) do
increment(vclock, node, timestamp())
end
@doc """
Increment the vector clock at node.
"""
@spec increment(t, vclock_node, timestamp) :: t
def increment(vclock, node, timestamp) do
{new_vclock, new_counter, new_timestamp} = case nodetake(vclock, node) do
false ->
{vclock, 1, timestamp}
{dot, mod_vclock} ->
{mod_vclock, dot.counter + 1, timestamp}
end
[%Dot{node: node, counter: new_counter, timestamp: new_timestamp}|new_vclock]
end
@doc """
Get all nodes in the vector clock.
"""
@spec all_nodes(t) :: [vclock_node]
def all_nodes(vclock) do
for %{node: node} <- vclock, do: node
end
@days_from_gregorian_base_to_epoch (1970*365+478)
@seconds_from_gregorian_base_to_epoch (@days_from_gregorian_base_to_epoch*24*60*60)
@doc """
Current timestamp for a vector clock.
"""
@spec timestamp() :: timestamp
def timestamp do
{mega, sec, _} = :os.timestamp()
@seconds_from_gregorian_base_to_epoch + mega*1000000 + sec
end
@doc """
Compares vector clocks for equality.
"""
@spec equal?(t, t) :: boolean
def equal?(va, vb) do
sort_vclock(va) === sort_vclock(vb)
end
# TODO: what should the default values be?
@doc """
Prunes a vector clock based on various parameters.
Vector clocks get pruned when they are either considered too large
or when the top-most dot is too old. Entries are removed one-by-one
off the top until neither of the two conditions are met.
## Options
* `:small_vclock` - max size for a vector clock to be not pruned.
* `:young_vclock` - max difference between `now` and the timestamp on
the latest dot for a vector clock to not be pruned.
* `:big_vclock` - vector clocks larger than this will be pruned.
* `:old_vclock` - max difference between `now` and the timestamp on
the latest dot for it to get pruned.
"""
@spec prune(t, timestamp, Keyword.t) :: t
def prune(vclock, now, opts \\ []) do
sorted_vclock = Enum.sort(vclock, fn dot_a, dot_b ->
{dot_a.timestamp, dot_a.node} < {dot_b.timestamp, dot_b.node}
end)
prune_small(sorted_vclock, now, opts)
end
defp prune_small(vclock, now, opts) do
case length(vclock) <= Keyword.get(opts, :small_vclock, 100) do
true -> vclock
false -> prune_young(vclock, now, opts)
end
end
defp prune_young(vclock, now, opts) do
%{timestamp: head_time} = hd(vclock)
case (now - head_time) < Keyword.get(opts, :young_vclock, 1_000) do
true -> vclock
false -> prune_big_or_old(vclock, now, head_time, opts)
end
end
defp prune_big_or_old(vclock, now, head_time, opts) do
case (length(vclock) > Keyword.get(opts, :big_vclock, 10_000)) or
((now - head_time) > Keyword.get(opts, :old_vclock, 100_000)) do
true -> prune_small(tl(vclock), now, opts)
false -> vclock
end
end
# private helpers
defp sort_vclock(vclock) do
Enum.sort_by(vclock, &Map.get(&1, :node))
end
defp find_dot(vclock, node) do
Enum.find(vclock, fn dot -> dot.node === node end)
end
defp nodetake(vclock, node, acc \\ [])
defp nodetake([], _node, _acc) do
false
end
defp nodetake([%{node: node} = dot|rest], node, acc) do
{dot, :lists.reverse(acc, rest)}
end
defp nodetake([dot|rest], node, acc) do
nodetake(rest, node, [dot|acc])
end
end