Skip to content

Instantly share code, notes, and snippets.

@asonge
Created February 22, 2014 21:20
Show Gist options
  • Save asonge/9162583 to your computer and use it in GitHub Desktop.
Save asonge/9162583 to your computer and use it in GitHub Desktop.

Ravel

Ravel provides a set of CRDT's (Cumulative Replicated Data Types) for Elixir, with a little more than inspiration from Basho's https://github.com/basho/riak_dt

About the name

CRDT's seem to be a great way to overcomplicated simple datastructures, and some people ...

defmodule Ravel.Mixfile do
use Mix.Project
def project do
[ app: :ravel,
version: "0.0.1",
elixir: "~> 0.12.4",
deps: deps ]
end
# Configuration for the OTP application
def application do
[mod: { Ravel, [] }]
end
# Returns the list of dependencies in the format:
# { :foobar, git: "https://github.com/elixir-lang/foobar.git", tag: "0.1" }
#
# To specify particular versions, regardless of the tag, do:
# { :barbat, "~> 0.1", github: "elixir-lang/barbat" }
defp deps do
[
{ :ex_doc, github: "elixir-lang/ex_doc" },
{ :excheck, github: "parroty/excheck" }
]
end
end
defmodule Ravel do
@moduledoc """
A native CRDT library for Elixir based on `riak_dt` from the great engineers at Basho.
"""
#use Application.Behaviour
# See http://elixir-lang.org/docs/stable/Application.Behaviour.html
# for more information on OTP Applications
def start(_type, _args) do
Ravel.Supervisor.start_link
end
end
defmodule RavelGCounterTest do
use ExUnit.Case , async: true
alias Ravel.CRDT
alias Ravel.GCounter
setup do
{ :ok, [counter: GCounter.new()] }
end
test "empty", context do
assert 0 == GCounter.value(context[:counter])
end
test "increments", context do
counter = context[:counter]
counter = GCounter.increment(counter, :actor1)
assert 1 == CRDT.value(counter)
counter = GCounter.increment(counter, :actor1, 5)
assert 6 == CRDT.value(counter)
counter = GCounter.increment(counter, :actor2, 5)
assert 11 == CRDT.value(counter)
end
test "simple merge", context do
counter = context[:counter]
counter = GCounter.increment(counter, :actor1, 5)
counter2 = GCounter.increment(counter, :actor2, 5)
counter0 = CRDT.merge(counter, counter2)
assert 10 == CRDT.value(counter0)
end
test "merge history", context do
counter = context[:counter]
counter1 = GCounter.increment(counter, :actor1, 5)
counter1 = GCounter.increment(counter1, :actor2, 5)
counter2 = GCounter.increment(counter, :actor2, 5)
counter0 = CRDT.merge(counter1, counter2)
assert 10 == CRDT.value(counter0)
end
test "equal test", context do
counter1 = context[:counter]
counter2 = context[:counter]
# Place these updates out-of-order, to make sure equals still works.
counter1 = GCounter.increment(counter1, :actor1, 5)
counter1 = GCounter.increment(counter1, :actor2, 5)
counter2 = GCounter.increment(counter2, :actor2, 5)
counter2 = GCounter.increment(counter2, :actor1, 5)
assert true == CRDT.equal?(counter1, counter2)
assert false == CRDT.equal?(context[:counter], counter1)
end
test "actors", context do
counter = context[:counter]
counter = GCounter.increment(counter, :actor1, 5)
counter = GCounter.increment(counter, :actor2, 5)
assert [:actor1, :actor2] == CRDT.actors(counter)
end
end
defmodule RavelORDictTest do
use ExUnit.Case, async: true
use ExCheck
alias Ravel.ORDict
alias Ravel.GCounter
alias Ravel.PNCounter
setup do
gc = GCounter.new()
a = GCounter.increment(gc, :a)
b = GCounter.increment(gc, :a, 2)
c = GCounter.increment(gc, :a, 3)
b1 = GCounter.increment(gc, :b, 2)
c1 = GCounter.increment(gc, :b, 3)
d = GCounter.increment(gc, :b)
e = GCounter.increment(gc, :b)
f = GCounter.increment(gc, :b)
{:ok, [
a: a, b: b, c: c, b1: b1, c1: c1, d: d, e: e, f: f,
empty: ORDict.new,
a_abc: ORDict.new([a: a, b: b, c: c], [actor: :a]),
b_abc: ORDict.new([a: a, b: b, c: c], [actor: :b]),
b_bcd: ORDict.new([b: b1, c: c1, d: d], [actor: :b]),
b_def: ORDict.new([d: d, e: e, f: f], [actor: :b]),
b_456: ORDict.new([d: d, e: e, f: f], [actor: :b])
]}
end
test "basic values", ctx do
dictlist = Keyword.take(ctx, [:a, :b, :c])
assert [] == ORDict.value(ctx[:empty])
assert dictlist == ORDict.value(ctx[:a_abc]) |> Enum.sort
assert ORDict.equal?(ctx[:a_abc], ctx[:b_abc])
assert ctx[:empty] == ORDict.empty(ctx[:a_abc])
end
test "new equivalence", ctx do
dictlist = Keyword.take(ctx, [:a, :b, :c])
assert dictlist == ORDict.new(Enum.map(dictlist, &{:a, &1})) |> sort_value
assert dictlist == ORDict.new(dictlist, &{:a, &1}) |> sort_value
assert dictlist == ORDict.new(dictlist, &{:a, &1}) |> ORDict.to_list |> Enum.sort
end
test "actors", ctx do
assert [:a] == ORDict.actors(ctx[:a_abc])
assert [:b] == ORDict.actors(ctx[:b_bcd])
assert [:a,:b] == ORDict.actors(ORDict.merge(ctx[:a_abc], ctx[:b_bcd])) |> Enum.sort
end
test "delete", ctx do
dictlist = Keyword.take(ctx, [:a, :c])
assert dictlist == ORDict.delete(ctx[:a_abc], :b) |> sort_value
assert ctx[:a_abc] == ORDict.delete(ctx[:a_abc], :d)
assert {:error, {:precondition, {:not_present, :d}}} == ORDict.remove(ctx[:a_abc], :d)
end
test "drop", ctx do
dictlist = Keyword.take(ctx, [:a])
assert dictlist == ORDict.drop(ctx[:a_abc], [:b,:c]) |> sort_value
end
test "fetch, fetch!, get", ctx do
assert {:ok, ctx[:a]} == ORDict.fetch(ctx[:a_abc], :a)
assert :error == ORDict.fetch(ctx[:a_abc], :d)
assert ctx[:a] == ORDict.fetch!(ctx[:a_abc], :a)
assert ctx[:a] == ORDict.get(ctx[:a_abc], :a)
assert nil == ORDict.get(ctx[:a_abc], :d)
assert ctx[:a] == ORDict.get(ctx[:a_abc], :d, ctx[:a])
end
test "has_key?", ctx do
assert ORDict.has_key?(ctx[:a_abc], :a)
assert !ORDict.has_key?(ctx[:a_abc], :d)
end
test "keys", ctx do
assert [:a,:b,:c] == ORDict.keys(ctx[:a_abc]) |> Enum.sort
end
test "merge", ctx do
abcdef = Keyword.take(ctx, [:a,:b,:c,:d,:e,:f])
assert ctx[:a_abc]===ORDict.merge(ctx[:a_abc], ctx[:a_abc])
assert abcdef == ORDict.merge(ctx[:a_abc], ctx[:b_def]) |> sort_value
end
test "merge disjoint", ctx do
[a: a,b: b,b1: b1,c: c,c1: c1,d: d] = Keyword.take(ctx, [:a,:b,:b1,:c,:c1,:d]) |> Enum.sort
abcd = [a: a,b: GCounter.merge(b,b1),c: GCounter.merge(c,c1),d: d]
assert abcd == ORDict.merge(ctx[:a_abc], ctx[:b_bcd]) |> sort_value
end
test "merge disjoint w/ remove", ctx do
[a: a,b: b,b1: b1,d: d] = Keyword.take(ctx, [:a,:b,:b1,:d]) |> Enum.sort
abd = [a: a,b: GCounter.merge(b,b1), d: d]
b_abd = ORDict.merge(ctx[:a_abc], ctx[:b_bcd]) |> ORDict.remove!(:c)
assert abd == ORDict.merge(ctx[:a_abc], b_abd) |> sort_value
end
# NOTE: the semantics of this may change soon
test "merge w/ mismatch crdt", ctx do
mismatch = ORDict.new() |> ORDict.add!(:b, :c, PNCounter.new)
# ORDict.merge(mismatch, ctx[:a_abc])
assert :crdt_mismatch == catch_throw(ORDict.merge(mismatch, ctx[:a_abc]))
end
test "pop", ctx do
# dict_values = Keyword.take(ctx, [:b,:c]) |> Enum.sort
new_dict = ORDict.remove!(ctx[:a_abc], :a)
assert {ctx[:a], new_dict} == ORDict.pop(ctx[:a_abc], :a)
end
test "put", ctx do
dictlist = [ {:d, ctx[:a]} | Keyword.take(ctx, [:a,:b,:c])] |> Enum.sort
assert dictlist == ORDict.put(ctx[:a_abc], {:d, :a}, ctx[:a]) |> sort_value
end
test "put_new", ctx do
assert :not_supported == catch_throw(ORDict.put_new(ctx[:a_abc], {:d, :a}, ctx[:a]))
end
test "reduce", ctx do
assert 6 == Enum.reduce(ctx[:a_abc], 0, fn({_,v}, acc) -> GCounter.value(v)+acc end)
#assert [0,1] == Enum.map(a, &(&1-1)) |> Enum.sort
assert [{0,{:a, 1}},{1,{:b, 2}},{2,{:c, 3}}] == Stream.zip(0..2, ctx[:a_abc]) |> Enum.map(fn({n,{k,v}}) -> {n,{k,GCounter.value(v)}} end)
end
test "size", ctx do
assert 0 == ORDict.size(ctx[:empty])
assert 3 == ORDict.size(ctx[:a_abc])
end
test "update", ctx do
dictlist1 = [{:a,ctx[:a]}, {:b,GCounter.merge(ctx[:b],ctx[:b1])}, {:c,ctx[:c]}]
dictlist2 = [{:a,ctx[:a]}, {:b, ctx[:b]}, {:c,ctx[:c]}, {:d,ctx[:d]}]
#IO.inspect ORDict.update(ctx[:a_abc], {:a,:b}, ctx[:b1], &GCounter.merge(&1,ctx[:b1]))
assert dictlist1 == ORDict.update!(ctx[:a_abc], {:a,:b}, &GCounter.merge(&1,ctx[:b1])) |> sort_value
assert dictlist2 == ORDict.update(ctx[:a_abc], {:a,:d}, ctx[:d], &GCounter.merge(&1,ctx[:d])) |> sort_value
assert dictlist1 == ORDict.update(ctx[:a_abc], {:a,:b}, ctx[:b1], &GCounter.merge(&1,ctx[:b1])) |> sort_value
assert :key_not_present == catch_throw(ORDict.update!(ctx[:a_abc], {:a,:d}, &GCounter.merge(&1,ctx[:b1])))
end
test "values", ctx do
assert [ctx[:a],ctx[:b],ctx[:c]] == ORDict.values(ctx[:a_abc]) |> Enum.sort
end
defp sort_value(ordict) do
ordict |> ORDict.value |> Enum.sort
end
end
defmodule RavelORSetTest do
use ExUnit.Case, async: true
use ExCheck
alias Ravel.ORSet
setup do
{:ok, [
empty: ORSet.new,
a_123: ORSet.new |> ORSet.add!(:a, 1) |> ORSet.add!(:a, 2) |> ORSet.add!(:a, 3),
a_123_m: ORSet.new(Enum.map 1..3, &{:a, &1}),
b_234: ORSet.new |> ORSet.add!(:b, 2) |> ORSet.add!(:b, 3) |> ORSet.add!(:b, 4),
b_456: ORSet.new(4..6, :b)
]}
end
test "simple check", context do
assert [] == ORSet.to_list(context[:empty])
assert [1,2,3] == ORSet.to_list(context[:a_123]) |> Enum.sort
assert [1,2,3] == ORSet.to_list(context[:a_123_m]) |> Enum.sort
assert {:error, :need_actor_tuple} == ORSet.new([1,2,3])
assert [1,2,3] == ORSet.to_list(ORSet.new(1..3, :a)) |> Enum.sort
end
test "merge", context do
assert [1,2,3,4,5,6] == ORSet.merge(context[:a_123], context[:b_456]) |> ORSet.value |> Enum.sort
assert [1,2,3,4,5,6] == ORSet.union(context[:a_123], context[:b_456]) |> ORSet.value |> Enum.sort
end
test "complex merge", context do
a = context[:a_123]
b = context[:b_234]
c1 = ORSet.merge(a, b)
c2 = ORSet.merge(b, a)
assert [1,2,3,4] == ORSet.value(c1) |> Enum.sort
assert [1,2,3,4] == ORSet.value(c2) |> Enum.sort
a1 = ORSet.add!(a, :a, 5)
assert [1,2,3,4,5] == ORSet.merge(a1,c1) |> ORSet.value |> Enum.sort
a2 = ORSet.remove!(a1, 1)
assert [2,3,4,5] == ORSet.merge(a2,c1) |> ORSet.value |> Enum.sort
end
test "member?, delete", context do
a = context[:a_123]
assert true == ORSet.member?(a, 2)
assert false == ORSet.member?(a, 4)
b = context[:b_456]
c = ORSet.merge(a,b)
assert true == ORSet.member?(c, 2)
assert true == ORSet.member?(c, 4)
c = ORSet.delete(c, 2)
assert false == ORSet.member?(c, 2)
end
test "size", context do
a = context[:a_123]
assert 0 == ORSet.size(ORSet.new())
assert 3 == ORSet.size(a)
a = ORSet.remove!(a, 1)
assert 2 == ORSet.size(a)
end
test "double add" do
a = ORSet.new
a = ORSet.add!(a, :a, 1)
a = ORSet.add!(a, :b, 1)
assert [1] == ORSet.value(a) |> Enum.to_list
end
test "double add merge" do
a = ORSet.new
b = ORSet.add!(a, :a, 1)
c = ORSet.add!(a, :b, 1)
d = ORSet.merge(b, c)
assert [1] == ORSet.value(d) |> Enum.to_list
end
test "disjoint merge drop" do
a = ORSet.new([a: 1, a: -2])
b = ORSet.new([b: 4])
c = ORSet.merge(a, b)
a = ORSet.remove!(a, -2)
assert false == ORSet.merge(a, c) |> ORSet.member?(-2)
c = ORSet.add!(c, :c, -2)
assert true == ORSet.merge(a, c) |> ORSet.member?(-2)
end
test "disjoint?" do
a = ORSet.new([a: 1, a: -2])
b = ORSet.new([b: 4])
assert ORSet.disjoint?(a, b)
c = ORSet.merge(a, b)
a = ORSet.remove!(a, -2)
assert false == ORSet.disjoint?(a, c)
c = ORSet.add!(c, :c, -2) |> ORSet.remove!(1)
assert false == ORSet.disjoint?(a, c)
a = ORSet.add!(a, :a, 1)
assert ORSet.disjoint?(a, c)
end
test "empty", context do
assert ORSet.new() === ORSet.empty(context[:a_123])
end
test "equal?", context do
c = ORSet.merge(context[:a_123], context[:b_234])
assert ORSet.equal?(ORSet.new(1..4, :c), c)
end
test "difference" do
a = ORSet.new |> ORSet.add!(:a, 1) |> ORSet.add!(:a, 2)
b = ORSet.new |> ORSet.add!(:b, 2)
assert [1] == ORSet.difference(a,b) |> ORSet.to_list
end
test "intersection", context do
assert [2,3] === ORSet.intersection(context[:a_123], context[:b_234]) |> ORSet.value |> Enum.sort
assert [] === ORSet.intersection(context[:a_123], context[:b_456]) |> ORSet.value |> Enum.sort
end
test "put" do
assert [1] === ORSet.new() |> ORSet.put({:a, 1}) |> ORSet.to_list
end
test "subset" do
assert ORSet.subset?(ORSet.new(1..5, :a), ORSet.new(1..8, :b))
assert false === ORSet.subset?(ORSet.new(1..5, :a), ORSet.new(2..8, :b))
end
test "disjoint drop using difference instead of merge" do
a = ORSet.new |> ORSet.add!(:a, 1) |> ORSet.add!(:a, -2)
b = ORSet.new |> ORSet.add!(:b, 4)
c = ORSet.merge(a, b)
a = ORSet.remove!(a, -2)# |> ORSet.add!(:a, 5)
assert false == ORSet.difference(a, c) |> ORSet.member?(-2)
c = ORSet.add!(c, :c, -2)
assert false == ORSet.difference(a, c) |> ORSet.member?(-2)
end
test "Enumerable count/member? support" do
a = ORSet.new |> ORSet.add!(:a, 1) |> ORSet.add!(:a, 2)
assert Enum.count(a) == 2
assert Enum.member?(a, 1)
end
test "Enumerable reduce support" do
a = ORSet.new |> ORSet.add!(:a, 1) |> ORSet.add!(:a, 2)
assert 3 == Enum.reduce(a, 0, &(&1+&2))
assert [0,1] == Enum.map(a, &(&1-1)) |> Enum.sort
assert [{0,1},{1,2}] == Stream.zip(0..1, a) |> Enum.to_list
end
property :adds do
for_all {x, y} in {list(int), list(int)} do
values = Enum.sort(x ++ y) |> Enum.uniq
a = Enum.reduce(x, ORSet.new, fn(v, acc) -> ORSet.add!(acc, :a, v) end)
b = Enum.reduce(y, ORSet.new, fn(v, acc) -> ORSet.add!(acc, :b, v) end)
values == ORSet.merge(a, b) |> ORSet.value |> Enum.sort
end
end
property :add_subtract_add do
for_all {adds, subtracts, readds} in {list(int), list(int), list(int)} do
adds = Enum.uniq adds
subtracts = Enum.uniq subtracts
readds = Enum.uniq readds
values = Enum.sort((adds -- subtracts) ++ readds) |> Enum.uniq
a = Enum.reduce(adds, ORSet.new, fn(v, acc) -> ORSet.add!(acc, :a, v) end)
b = Enum.reduce(subtracts, a, fn(v, acc) ->
case ORSet.member?(a, v) do
true -> ORSet.remove!(acc, v)
false -> acc
end
end)
c = Enum.reduce(readds, ORSet.new, fn(v, acc) -> ORSet.add!(acc, :b, v) end)
values == ORSet.merge(b, c) |> ORSet.value |> Enum.sort
end
end
end
defmodule RavelCrdtPNCounterTest do
use ExUnit.Case, async: true
alias Ravel.CRDT
alias Ravel.PNCounter
setup do
{ :ok, [counter: PNCounter.new()] }
end
test "empty", context do
assert 0 == PNCounter.value(context[:counter])
end
test "increments", context do
counter = context[:counter]
counter = PNCounter.increment(counter, :actor1)
assert 1 == CRDT.value(counter)
counter = PNCounter.increment(counter, :actor1, 5)
assert 6 == CRDT.value(counter)
counter = PNCounter.increment(counter, :actor2, 5)
assert 11 == CRDT.value(counter)
end
test "decrements", context do
counter = context[:counter]
counter = PNCounter.decrement(counter, :actor1)
assert -1 == CRDT.value(counter)
counter = PNCounter.decrement(counter, :actor1, 5)
assert -6 == CRDT.value(counter)
counter = PNCounter.decrement(counter, :actor2, 5)
assert -11 == CRDT.value(counter)
counter = PNCounter.increment(counter, :actor2, 11)
assert 0 == CRDT.value(counter)
end
test "simple merge", context do
counter = context[:counter]
counter = PNCounter.increment(counter, :actor1, 5)
counter2 = PNCounter.decrement(counter, :actor2, 5)
counter0 = CRDT.merge(counter, counter2)
assert 0 == CRDT.value(counter0)
end
test "merge history", context do
counter = context[:counter]
counter1 = PNCounter.increment(counter, :actor1, 5)
counter1 = PNCounter.decrement(counter1, :actor1, 5)
counter2 = PNCounter.decrement(counter, :actor2, 5)
counter0 = CRDT.merge(counter1, counter2)
assert -5 == CRDT.value(counter0)
end
test "equal test", context do
counter1 = context[:counter]
counter2 = context[:counter]
# Place these updates out-of-order, to make sure equals still works.
counter1 = PNCounter.increment(counter1, :actor1, 5)
counter1 = PNCounter.decrement(counter1, :actor2, 5)
counter2 = PNCounter.decrement(counter2, :actor2, 5)
counter2 = PNCounter.increment(counter2, :actor1, 5)
assert true == PNCounter.equal(counter1, counter2)
assert false == PNCounter.equal(context[:counter], counter1)
end
test "actors", context do
counter = context[:counter]
counter = PNCounter.increment(counter, :actor1, 5)
counter = PNCounter.decrement(counter, :actor2, 5)
assert [:actor1, :actor2] == CRDT.actors(counter)
end
end
defmodule RavelTest do
use ExUnit.Case
test "the truth" do
assert(true)
end
end
defmodule RavelVClockTest do
use ExUnit.Case, async: true
alias Ravel.VClock
# setup do
# { :ok, [a: Ravel.GCounter.new()] }
# end
test "simple" do
a = VClock.new()
b = VClock.new()
a1 = VClock.increment(a, :actor1)
b1 = VClock.increment(b, :actor2)
assert true == VClock.descends(a1, a)
assert true == VClock.descends(b1, b)
assert false == VClock.descends(a1, b1)
a2 = VClock.increment(a1, :actor1)
c = VClock.merge([a2, b1])
c1 = VClock.increment(c, :actor3)
assert true == VClock.descends(c1, a2)
assert true == VClock.descends(c1, b1)
assert false == VClock.descends(b1, c1)
assert false == VClock.descends(b1, a1)
end
test "accessor" do
vc = VClock.new() |> VClock.increment(:actor1) |> VClock.increment(:actor2) |> VClock.increment(:actor2)
assert 1 == VClock.get_counter(vc, :actor1)
assert 2 == VClock.get_counter(vc, :actor2)
assert 0 == VClock.get_counter(vc, :actor3)
assert [:actor1, :actor2] == VClock.nodes(vc)
end
test "merge_test" do
vc1 = VClock.new([actor1: 1, actor2: 2, actor4: 4])
vc2 = VClock.new([actor3: 3, actor4: 3])
vc3 = VClock.new([actor1: 1, actor2: 2, actor3: 3, actor4: 4])
assert vc3 == VClock.merge([vc1, vc2])
end
test "merge_less_left_right_test" do
vc1 = VClock.new([actor5: 5])
vc2 = VClock.new([actor6: 6, actor7: 7])
vc3 = VClock.new([actor5: 5, actor6: 6, actor7: 7])
assert vc3 == VClock.merge([vc1, vc2])
assert vc3 == VClock.merge([vc2, vc1])
end
test "merge_same_id_test" do
vc1 = VClock.new([actor1: 1, actor2: 1])
vc2 = VClock.new([actor1: 1, actor3: 1])
vc3 = VClock.new([actor1: 1, actor2: 1, actor3: 1])
assert vc3 == VClock.merge([vc1, vc2])
assert vc3 == VClock.merge([vc2, vc1])
end
test "dominates" do
a = VClock.new([a: 1, b: 2])
b = VClock.new([b: 2])
c = VClock.new([c: 2])
d = VClock.new([b: 1])
e = VClock.new([a: 1])
assert VClock.dominates(a,b)
assert false == VClock.dominates(a,a)
assert VClock.dominates(a,e)
assert false == VClock.dominates(a,c)
assert VClock.dominates(a,d)
end
test "equal" do
assert VClock.equal(VClock.new([a: 1, b: 2]), VClock.new([b: 2, a: 1]))
assert false == VClock.equal(VClock.new([a: 1, b: 2]), VClock.new([b: 2, a: 1, c: 2]))
end
test "replace_actors" do
a = VClock.new([a: 1, b: 2, c: 3, d: 4])
b = VClock.new([e: 1, f: 2, c: 3, d: 4])
assert VClock.equal(b, VClock.replace_actors(a, [e: :a, f: :b]))
assert VClock.equal(b, VClock.replace_actors(a, [e: :a, f: :b, h: :g]))
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment