Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update documentation #3

Merged
merged 3 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 216 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# CRDT

**TODO: Add description**
This is a set of basic, composable and extensible CRDTs.

A CRDT is defined as Conflict-free Replicated Data Type,
see https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type

Please refer to the API Reference for usage documentation and examples.

## Installation

Expand All @@ -19,3 +24,213 @@ Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_do
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/crdt>.

## List of implemented CRDTs

```
+----------------++---------++-----------++------------++---------++----------------+
Data Type | LWW-Register || AWORSet || G-Counter || PN-Counter || AWORMap || Delta-AWORSet |
+----------------++---------++-----------++------------++---------++----------------+
CRDT Type | Commutative | Convergent |
+-----------------+-----------------------------------------------------------------+
```

## Examples

These are simple examples that should give an idea of how to use some of the data types,
and what to expect in each case.

### `CRDT.GCounter`

A `CRDT.GCounter` is a growth-only counter. It can be initialized with positive values on
behalf of actors. The resulting value will always be the sum of the values across actors.
An empty counter will have the value '0'.


#### Initializing

``` elixir
counter = CRDT.GCounter.new
CRDT.value(counter) # => 0

counter = CRDT.GCounter.new(actor1: 5, actor2: 10)
CRDT.value(counter) # => 15
```

#### Incrementing

Incrementing a `CRDT.GCounter` is done via the `CRDT.GCounter.inc/2` function.
If the actor key does not exist yet, it is assumed that the given value is the starting
value.

``` elixir
counter = CRDT.GCounter.new
counter = counter |> CRDT.GCounter.inc(:a, 5) # => %CRDT.GCounter{value: %{a: 5}}
counter = counter |> CRDT.GCounter.inc(:a, 2) # => %CRDT.GCounter{value: %{a: 7}}
CRDT.value(counter) # => 7
```

#### Merging

Merging two GCounters preserves all actors in both, taking the higher value if an actor exists in both GCounters.

``` elixir
counter1 = CRDT.GCounter.new(actor1: 5, actor2: 3)
counter2 = CRDT.GCounter.new(actor2: 1, actor3: 8)
CRDT.merge(counter1, counter2) # => %CRDT.GCounter{value: %{actor1: 5, actor2: 3, actor3: 8}}
```

### `CRDT.PNCounter`

A `CRDT.PNCounter` is used to process events that can increment or decrement the value.

#### Initializing

When initialized without starting values, the `CRDT.PNCounter` initial value is '0'.

``` elixir
counter = CRDT.PNCounter.new #=> %CRDT.PNCounter{pos: %{}, neg: %{}}
CRDT.value(counter) #=> 0
```

Initial values can be supplied as positive and negative actor => value maps.

``` elixir
counter = CRDT.PNCounter.new(pos: %{a: 1, b: 2}, neg: %{a: 8, b: 7})
CRDT.value(counter) #=> -12
```

#### Incrementing
Incrementing a `CRDT.PNCounter` is done via the `CRDT.PNCounter.increment/3` function.
Incrementing a pncounter will update only the `pos` actor => value map.
If no value is given, it will be updated by 1 by default.

``` elixir
pncounter = CRDT.PNCounter.new
pncounter = pncounter |> CRDT.PNCounter.increment(:a, 5) # => %CRDT.PNCounter{pos: %{a: 2}, neg: %{}}
pncounter = pncounter |> CRDT.PNCounter.increment(:a, 2) # => %CRDT.PNCounter{pos: %{a: 4}, neg: %{}}
CRDT.value(pncounter) # => 7
```

#### Decrementing
Decrementing a `CRDT.PNCounter` is done via the `CRDT.PNCounter.decrement/3` function.
Incrementing a pncounter will update only the `neg` actor => value map.
If no value is given, it will be updated by 1 by default.

``` elixir
pncounter = CRDT.PNCounter.new
pncounter = pncounter |> CRDT.PNCounter.decrement(:a, 5) # => %CRDT.PNCounter{pos: %{}, neg: %{a: 5}}
pncounter = pncounter |> CRDT.PNCounter.decrement(:a, 2) # => %CRDT.PNCounter{pos: %{}, neg: %{a: 7}}
CRDT.value(pncounter) # => -7
```

#### Merging
Merging two PNCounters preserves all actors in both, taking the total sum of all positive and negative values.

``` elixir
pncounter1 = CRDT.PNCounter.new |> CRDT.PNCounter.increment(actor1: 5, actor2: 3)
pncounter2 = CRDT.PNCounter.new |> CRDT.PNCounter.decrement(actor1: 5, actor3: 3)
merged = CRDT.merge(pncounter1, pncounter2) # => %CRDT.PNCounter{value: %{actor1: 5, actor2: 3, actor3: 8}}
CRDT.value(merged) # => 0
```

### `CRDT.LWWRegister`

A `CRDT.LWWRegister` is a crdt used when we are interested in having the most recent information available (Least Write Wins).
It contains a value and the corresponding timestamp.

#### Initializing

When initialized without starting values, the `CRDT.LWWRegister` initial value is 'nil'.

``` elixir
register = CRDT.LWWRegister.new #=> %CRDT.LWWRegister{value: nil, timestamp: 1698400752943930708}
CRDT.value(counter) #=> nil
```

#### Updating

Updating a `CRDT.LWWRegister` is done via the `CRDT.LWWRegister.set/2` function.
This will update the value with the current system time.

``` elixir
register = CRDT.LWWRegister.new
register = register |> CRDT.LWWRegister.set("hello register") # => %CRDT.LWWRegister{value: "hello register", timestamp: 1700218890688914751}
CRDT.value(register) # => "hello register"
```

#### Merging

Merging two LWWRegisters will simply take the most recent value.

``` elixir
register1 = CRDT.LWWRegister.new |> CRDT.LWWRegister.set("hello")
register2 = CRDT.LWWRegister.new |> CRDT.LWWRegister.set("latest_hello")
merged = CRDT.merge(register1, register2)
CRDT.value(merged) # => "latest_hello"
```

### `CRDT.AWORMap`

A `CRDT.AWORMap` is used to store events in a key => value map. The values stored in an AWORMap are crdts themselves.
Merging strategy follows those of the crdts contained in the map.

#### Initializing
The value of a new map is an empty map.

``` elixir
map = CRDT.AWORMap.new
CRDT.value(map) #=> %{}
```
When initialized the `CRDT.AWORMap` has this structure:

``` elixir
%CRDT.AWORMap{
keys: %CRDT.AWORSet{
dot_kernel: %CRDT.DotKernel{
dot_context: %CRDT.DotContext{version_vector: %{}, dot_cloud: []},
entries: %{}
}
},
entries: %{}
}
```

Inside `CRDT.DotKernel` are the operations performed on the map: which actor made the change, the number of operation and the value added.
Inside the outer entries there is the map keylist with the current values.

It's possible to put a crdt in the `CRDT.AWORMap` through the `CRDT.AWORMap.put/4` function.
It's necessary to specify the actor who make the change, the key under which the crdt will be stored and the crdt itself.

``` elixir
map = CRDT.AWORMap.new
map = map |> CRDT.AWORMap.put(:a, :key, CRDT.GCounter.new())
CRDT.value(map) #=> %{key: 0}
```

#### Updating

Updating a `CRDT.AWORMap` is done via the `CRDT.AWORMap.update!/4` function.
It's possible to pass a function as an argument that will be applied as the updated value of `key`

``` elixir
map = CRDT.AWORMap.new
map = map |> CRDT.AWORMap.put(:a, :key, CRDT.GCounter.new() |> CRDT.GCounter.inc(:a, 1))
CRDT.value(map) #=> %{key: 1}
map = map |> CRDT.AWORMap.update!(:a, :key, &(CRDT.GCounter.inc(&1, :a, 100)))
CRDT.value(map) #=> %{key: 101}
```

##### Update vs Update!
- `CRDT.AWORMap.update!/4` if the given "key" is not present in the AWORMap, a `KeyError` exception is raised.
- `CRDT.AWORMap.update/5` if the given "key" is not present in the AWORMap, the default value is inserted as the crdt of `key`

#### Merging

Merging two AWORMaps uses the same merge strategies as their crdts stored inside them.

``` elixir
map1 = CRDT.AWORMap.new |> CRDT.AWORMap.put(:a, :key, CRDT.GCounter.new() |> CRDT.GCounter.inc(:a, 1))
map2 = CRDT.AWORMap.new |> CRDT.AWORMap.put(:a, :key2, CRDT.GCounter.new() |> CRDT.GCounter.inc(:a, 100))
merged_map = CRDT.merge(map1, map2)
CRDT.value(merged_map) #=> %{key: 100}
```
1 change: 1 addition & 0 deletions lib/crdt.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defprotocol CRDT do
@moduledoc """
Protocol defining the interface for CRDTs.

"""

@type actor :: term()
Expand Down
14 changes: 14 additions & 0 deletions lib/crdt/access.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
defprotocol CRDT.Access do
@moduledoc """
This protocol defines the access methods for CRDTs.
"""
@doc """
Returns the value at the given path.
"""
@spec get_in(t(), nonempty_list(term())) :: term()
def get_in(t, list)

@doc """
Adds the given value to the given path.
"""
@spec put_in(t(), term(), nonempty_list(term()), CRDT.crdt()) :: CRDT.crdt()
def put_in(t, actor, list, value)

@doc """
Updates the value at the given path.
"""
@spec update_in(t(), term(), nonempty_list(term()), (term() -> term())) :: CRDT.crdt()
def update_in(t, actor, list, fun)
end
9 changes: 7 additions & 2 deletions lib/crdt/awor_map.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
defmodule CRDT.AWORMap do
@moduledoc false
@moduledoc """
An add-wins observed-remove map (AWORMap) is a map of keys to CRDTs based on sets.

An AWORMap is a map of keys to CRDTs. It is a CRDT itself and can be used to
implement other CRDTs.
"""

@type t :: %__MODULE__{
keys: CRDT.AWORSet.t(),
Expand Down Expand Up @@ -102,7 +107,7 @@ defmodule CRDT.AWORMap do
def get(%__MODULE__{entries: entries}, key, default \\ nil), do: Map.get(entries, key, default)

@doc """
Updates the `key` in the AWORMap with the the given function to the crdt on behalf of `actor`.
Updates the `key` in the AWORMap with the given function to the crdt on behalf of `actor`.

If `key` is present in AWORMap then the existing crdt is passed to `fun` and its result is
used as the updated crdt of `key`. If `key` is
Expand Down
7 changes: 6 additions & 1 deletion lib/crdt/awor_set.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
defmodule CRDT.AWORSet do
@moduledoc false
@moduledoc """
An add-wins observed-remove set (AWORSet) is a set that allows adding and
removing elements.

It is a variant of a 2P-Set that uses a DotKernel to track the additions and removals.
"""

@type actor :: term
@type value :: term
Expand Down
9 changes: 8 additions & 1 deletion lib/crdt/delta_awor_set.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
defmodule CRDT.DeltaAWORSet do
@moduledoc false
@moduledoc """
A delta add-wins observed-remove set (DeltaAWORSet) is an opimized set that
allows adding and removing elements.

It is a variant of a 2P-Set that uses a DotKernel to track the additions and removals.
The delta variant optimizes the merge operation by only merging the delta of the
two sets.
"""

@type actor :: term
@type value :: term
Expand Down
7 changes: 6 additions & 1 deletion lib/crdt/dot_context.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
defmodule CRDT.DotContext do
@moduledoc false
@moduledoc """
A DotContext is a data structure that contains a version vector and a dot cloud.

The version vector is a map from actors to the maximum version of a dot that has been added to
the dot context on behalf of that actor.
"""

@type actor :: term
@type version :: pos_integer
Expand Down
11 changes: 9 additions & 2 deletions lib/crdt/dot_kernel.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
defmodule CRDT.DotKernel do
@moduledoc false
@moduledoc """
A DotKernel is a data structure that contains a DotContext and a map of dots to values.

The DotContext is a data structure that contains a version vector and a dot cloud.

The version vector is a map from actors to the maximum version of a dot that has been added to
the dot context on behalf of that actor.
"""

@type actor :: term
@type version :: pos_integer
Expand Down Expand Up @@ -121,7 +128,7 @@ defmodule CRDT.DotKernel do
for {dot, entry_value} <- entries, entry_value == value, reduce: entries do
entries ->
# The corresponding dot says in the dot context and acts as a tombstone.
# So we can avoid readding it when merging with an out of date replica.
# So we can avoid reading it when merging with an out of date replica.
Map.delete(entries, dot)
end

Expand Down
Loading