├── project
├── build.properties
└── plugins.sbt
├── .gitignore
├── doc
└── images
│ └── state_based_crdt_flow.png
├── src
├── main
│ └── scala
│ │ └── com
│ │ └── machinomy
│ │ └── crdt
│ │ ├── state
│ │ ├── package.scala
│ │ ├── Convergent.scala
│ │ ├── PartialOrder.scala
│ │ ├── TombStone.scala
│ │ ├── Bias.scala
│ │ ├── GSet.scala
│ │ ├── TPSet.scala
│ │ ├── MCSet.scala
│ │ ├── PNCounter.scala
│ │ ├── GCounter.scala
│ │ ├── ORSet.scala
│ │ └── LWWElementSet.scala
│ │ └── op
│ │ ├── Counter.scala
│ │ ├── GraphLikeA.scala
│ │ ├── ORSet.scala
│ │ ├── MonotonicDag.scala
│ │ ├── PartialOrderDag.scala
│ │ ├── DiGraphLike.scala
│ │ └── TPTPGraph.scala
└── test
│ └── scala
│ └── com
│ └── machinomy
│ └── crdt
│ ├── state
│ ├── TPSetSuite.scala
│ ├── ORSetSuite.scala
│ ├── GSetSuite.scala
│ ├── MCSetSuite.scala
│ ├── PNCounterSuite.scala
│ ├── LWWElementSetSuite.scala
│ └── GCounterSuite.scala
│ └── op
│ ├── MonotonicDagSuite.scala
│ ├── CounterSuite.scala
│ ├── PartialOrderDagSuite.scala
│ ├── ORSetSuite.scala
│ └── TPTPGraphSuite.scala
├── README.md
└── LICENSE
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version = 0.13.8
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | project/target/
3 | credentials.properties
4 |
--------------------------------------------------------------------------------
/doc/images/state_based_crdt_flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinomy/crdt/HEAD/doc/images/state_based_crdt_flow.png
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | logLevel := Level.Warn
2 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.3")
3 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "1.6.0")
4 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/package.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt
2 |
3 | import cats._
4 | import com.github.nscala_time.time.Imports._
5 |
6 | package object state {
7 | implicit object DateTimeOrder extends Order[DateTime] {
8 | override def compare(x: DateTime, y: DateTime): Int = {
9 | x.compare(y)
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/state/TPSetSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.state
2 |
3 | import org.scalatest.FunSuite
4 | import cats.syntax.all._
5 |
6 | class TPSetSuite extends FunSuite {
7 | test("Fresh TPSet is empty") {
8 | val fresh = TPSet[Int]()
9 | assert(fresh.value.isEmpty)
10 | }
11 |
12 | test("TPSet could be updated") {
13 | val a = TPSet[Int]() + 3 - 3
14 | assert(a.value === Set.empty[Int])
15 | val b = TPSet[Int]() + 3 - 1
16 | assert(b.value === Set(3))
17 | }
18 |
19 | test("TPSet could be merged") {
20 | val a = TPSet[Int]() + 3 - 3
21 | val b = TPSet[Int]() + 1 - 1 + 2
22 | val c = a |+| b
23 | assert(c.value === Set(2))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/state/ORSetSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.state
2 |
3 | import com.github.nscala_time.time.Imports._
4 | import org.scalatest.FunSuite
5 |
6 | class ORSetSuite extends FunSuite {
7 | test("Fresh ORSet is empty") {
8 | val fresh = ORSet[Int, DateTime]()
9 | assert(fresh.value.isEmpty)
10 | }
11 |
12 | test("ORSet could be updated") {
13 | val a = ORSet[Int, DateTime]() + 3
14 | assert(a.value === Set(3))
15 |
16 | val b = ORSet[Int, DateTime]() + 3 - 3
17 | assert(b.value === Set.empty)
18 |
19 | val now = DateTime.now()
20 | val c = ORSet[Int, DateTime]() + 3 - 3
21 | assert(c.value === Set.empty)
22 |
23 | val d = c + (3, now + 10.minutes)
24 | assert(d.value === Set(3))
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/Convergent.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.state
18 |
19 | trait Convergent[Element, Value] {
20 | def value: Value
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/PartialOrder.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.state
2 |
3 | import cats.{PartialOrder => CatPartialOrder}
4 |
5 | /** Constructor of [[cats.PartialOrder]] instances based on `lteqv` relation.
6 | *
7 | * @see [[PartialOrder.byLteqv]]
8 | */
9 | object PartialOrder {
10 | /** [[cats.PartialOrder]] based on `lteqv` relation.
11 | *
12 | * @param f lteqv relation
13 | */
14 | def byLteqv[A](f: (A,A) => Boolean) = new CatPartialOrder[A] {
15 | override def partialCompare(x: A, y: A): Double =
16 | (lteqv(x, y), lteqv(y, x)) match {
17 | case (true, true) => 0
18 | case (false, true) => 1
19 | case (true, false) => -1
20 | case (false, false) => Double.NaN
21 | }
22 |
23 | override def lteqv(x: A, y: A): Boolean = f(x, y)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/op/MonotonicDagSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.op
2 |
3 | import org.scalatest.{FunSuite, Matchers}
4 |
5 | import scalax.collection.Graph
6 | import scalax.collection.GraphPredef._
7 | import scalax.collection.GraphEdge._
8 |
9 | class MonotonicDagSuite extends FunSuite with Matchers {
10 | test("add, edge") {
11 | val g: Graph[Int, DiEdge] = Graph[Int, DiEdge]() + 1 + 2
12 | val dag = MonotonicDag[Int, DiEdge, Graph[Int, DiEdge]](g)
13 | val edge = 1 ~> 2
14 | val (dag2, op) = dag.add(edge)
15 | dag2.value.edges shouldNot be(empty)
16 | }
17 |
18 | test("add, vertex") {
19 | val g: Graph[Int, DiEdge] = Graph[Int, DiEdge]() + 1 + 100
20 | val (dag, _)= MonotonicDag[Int, DiEdge, Graph[Int, DiEdge]](g).add(1 ~> 100)
21 | val (dag2, op) = dag.add(2, 1, 100)
22 | dag2.value.edges shouldNot be(empty)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/op/CounterSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.op
2 |
3 | import org.scalatest.{FunSuite, Matchers}
4 | import org.scalatest.prop.PropertyChecks
5 | import org.scalacheck.Gen
6 |
7 | class CounterSuite extends FunSuite with PropertyChecks with Matchers {
8 | test("fresh value is zero") {
9 | val counter = Counter[Int]()
10 | assert(counter.value == 0)
11 | }
12 |
13 | test("increment") {
14 | forAll(Gen.posNum[Int]) { (i: Int) =>
15 | val increment = Counter.Increment(i)
16 | val counter = Counter.update(Counter[Int](), increment)
17 | counter.value should be(i)
18 | }
19 | }
20 |
21 | test("decrement") {
22 | forAll(Gen.posNum[Int]) { (i: Int) =>
23 | val decrement = Counter.Decrement(i)
24 | val counter = Counter.update(Counter[Int](), decrement)
25 | counter.value should be(-1 * i)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/state/GSetSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.state
2 |
3 | import cats.kernel.Eq
4 | import org.scalatest.FunSuite
5 | import cats.syntax.all._
6 |
7 | class GSetSuite extends FunSuite {
8 | val eq = implicitly[Eq[GSet[Int]]]
9 |
10 | test("Just created GSet is empty") {
11 | val gSet = GSet[Int]()
12 | assert(gSet.value.isEmpty)
13 | }
14 |
15 | test("GSet calculates value") {
16 | val a = GSet[Int]()
17 | val b = a + 3
18 | val c = b + 1
19 | val d = c + 3
20 | assert(d.value === Set(1, 3))
21 | }
22 |
23 | test("GSets can be merged") {
24 | val a = GSet[Int](Set(1, 2, 3))
25 | val b = GSet[Int](Set(2, 3, 4))
26 | val result = a |+| b
27 | assert(result.value === Set(1, 2, 3, 4))
28 | }
29 |
30 | test("equality") {
31 | val a = GSet[Int]()
32 | val b = GSet[Int]()
33 | assert(eq.eqv(a, b))
34 |
35 | val a1 = a + 1
36 | assert(a1 !== b)
37 |
38 | val b1 = b + 1
39 | assert(eq.eqv(a1, b1))
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/state/MCSetSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.state
2 |
3 | import cats._
4 | import cats.syntax.all._
5 | import org.scalatest.FunSuite
6 |
7 | class MCSetSuite extends FunSuite {
8 | test("Just created MCSet is empty") {
9 | val gSet = Monoid[MCSet[Int, Int]].empty
10 | assert(gSet.value.isEmpty)
11 | }
12 |
13 | test("MCSet calculates value") {
14 | val a = Monoid[MCSet[Int, Int]].empty + 3 + 1 + 3
15 | assert(a.value === Set(1, 3))
16 | }
17 |
18 | test("MCSet additions can be merged") {
19 | val a = Monoid[MCSet[Int, Int]].empty + 1 + 2 + 3
20 | val b = Monoid[MCSet[Int, Int]].empty + 2 + 3 + 4
21 | val result = a |+| b
22 | assert(result.value === Set(1, 2, 3, 4))
23 | }
24 |
25 | test("MCSet additions and removals can be merged") {
26 | val a = Monoid[MCSet[Int, Int]].empty + 1 + 2 + 3
27 | val b = Monoid[MCSet[Int, Int]].empty - 2 - 3 - 4
28 | val c = a |+| b
29 | assert(c.value === Set(1, 2, 3))
30 |
31 | val d = a |+| (a - 2 - 3)
32 | assert(d.value === Set(1))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/TombStone.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.state
18 |
19 | import java.util.UUID
20 |
21 | import com.github.nscala_time.time.Imports._
22 |
23 | trait TombStone[A] {
24 | def next: A
25 | }
26 |
27 | object TombStone {
28 | implicit object DateTimeTombStone extends TombStone[DateTime] {
29 | override def next = DateTime.now
30 | }
31 |
32 | implicit object UuidTombStone extends TombStone[UUID] {
33 | override def next = UUID.randomUUID
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/op/PartialOrderDagSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.op
2 |
3 | import com.machinomy.crdt.state.TPSet
4 | import org.scalatest.{FunSuite, Matchers}
5 |
6 | import scalax.collection.Graph
7 | import scalax.collection.GraphPredef._
8 | import scalax.collection.GraphEdge._
9 |
10 | class PartialOrderDagSuite extends FunSuite with Matchers {
11 | test("add vertex") {
12 | val g: Graph[Int, DiEdge] = Graph[Int, DiEdge]() + 1 ~> 100
13 | val edges: Set[DiEdge[Int]] = g.edges.toOuter
14 | val vertices = TPSet(g.nodes.toOuter)
15 | val dag = PartialOrderDag[Int, DiEdge](vertices, edges)
16 | val (dag2, op) = dag.add(2, 1, 100)
17 | dag2.value.edges shouldNot be(empty)
18 | }
19 |
20 | // @todo Actually, remove the vertex as a payload, but leave it as a chain link
21 | test("remove vertex - does nothing") {
22 | val g: Graph[Int, DiEdge] = Graph[Int, DiEdge]() + 1 ~> 100 + 1 ~> 2 + 2 ~> 100
23 | val edges = g.edges.toOuter
24 | val vertices = TPSet(g.nodes.toOuter)
25 | val dag = PartialOrderDag[Int, DiEdge](vertices, edges)
26 | val (dag2, op) = dag.remove(2)
27 | dag2.value.edges shouldNot be(empty)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/state/PNCounterSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.state
2 |
3 | import cats._
4 | import org.scalatest.FunSuite
5 | import cats.syntax.all._
6 |
7 | class PNCounterSuite extends FunSuite {
8 | test("Empty value is zero") {
9 | val fresh = Monoid[PNCounter[Int, Int]].empty
10 | assert(fresh.value === 0)
11 | }
12 |
13 | test("Could be calculated") {
14 | val counter = Monoid[PNCounter[Int, Int]].empty + (1 -> 1) + (2 -> 3)
15 | assert(counter.value === 1 + 3)
16 | }
17 |
18 | test("Could be merged") {
19 | val a = Monoid[PNCounter[Int, Int]].empty + (1 -> 1) + (2 -> 3)
20 | val b = Monoid[PNCounter[Int, Int]].empty + (1 -> 2) + (2 -> -3)
21 | val c = a |+| b
22 | assert(c.value === 2)
23 | }
24 |
25 | test("Could get replica value") {
26 | val a = Monoid[PNCounter[Int, Int]].empty + (1 -> 2) + (2 -> 3) + (1 -> -1)
27 | assert(a.get(1) === 1)
28 | }
29 |
30 | test("Could get table") {
31 | val a = Monoid[PNCounter[Int, Int]].empty + (1 -> 2) + (2 -> 3) + (1 -> -1)
32 | assert(a.table === Map(1 -> 1, 2 -> 3))
33 | }
34 |
35 | test("Can update table") {
36 | val a = Monoid[PNCounter[Int, Int]].empty + (1 -> 2) + (2 -> 3) + (1 -> -1)
37 | val b = a + (2 -> 5)
38 | assert(b.table === Map(1 -> 1, 2 -> 8))
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/state/LWWElementSetSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.state
2 |
3 | import cats._
4 | import cats.syntax.all._
5 | import com.github.nscala_time.time.Imports._
6 | import org.scalatest.FunSuite
7 |
8 | class LWWElementSetSuite extends FunSuite {
9 | test("fresh is empty") {
10 | val set = Monoid[LWWElementSet[Int, DateTime, Bias.AdditionWins]].empty
11 | assert(set.value.isEmpty)
12 | }
13 |
14 | test("calculates value") {
15 | val a = Monoid[LWWElementSet[Int, DateTime, Bias.AdditionWins]].empty + 3 + 1 + 3
16 | assert(a.value === Set(1, 3))
17 | }
18 |
19 | test("can be combined, addition bias") {
20 | val now = DateTime.now
21 | val a = Monoid[LWWElementSet[Int, DateTime, Bias.AdditionWins]].empty + (1, now) + (2, now) + (3, now)
22 | val b = Monoid[LWWElementSet[Int, DateTime, Bias.AdditionWins]].empty - (2, now) - (3, now) - (4, now)
23 | val result = a |+| b
24 | assert(result.value === Set(1, 2, 3))
25 | }
26 |
27 | test("can be combined, removal bias") {
28 | val now = DateTime.now
29 | val a = Monoid[LWWElementSet[Int, DateTime, Bias.RemovalWins]].empty + (1, now) + (2, now) + (3, now)
30 | val b = Monoid[LWWElementSet[Int, DateTime, Bias.RemovalWins]].empty - (2, now) - (3, now) - (4, now)
31 | val result = a |+| b
32 | assert(result.value === Set(1))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/op/Counter.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.op
18 |
19 | case class Counter[E: Numeric](value: E)
20 |
21 | object Counter {
22 | sealed trait Update[E]
23 |
24 | private def num[E: Numeric] = implicitly[Numeric[E]]
25 |
26 | case class Increment[E: Numeric](i: E) extends Update[E] {
27 | assert(num.gteq(i, num.zero))
28 | }
29 |
30 | case class Decrement[E: Numeric](i: E) extends Update[E] {
31 | assert(num.gteq(i, num.zero))
32 | }
33 |
34 | def apply[E: Numeric]() = new Counter[E](implicitly[Numeric[E]].zero)
35 |
36 | def update[E: Numeric](counter: Counter[E], update: Update[E]): Counter[E] = update match {
37 | case Counter.Increment(i) => Counter(num.plus(counter.value, i))
38 | case Counter.Decrement(i) => Counter(num.minus(counter.value, i))
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/op/GraphLikeA.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.op
18 |
19 | import scala.language.higherKinds
20 |
21 | object GraphLikeA {
22 | trait VertexLike[A]
23 | trait SentinelLike[A] extends VertexLike[A]
24 | trait EdgeLike[A, V <: VertexLike[A]] {
25 | def u: V
26 | def v: V
27 | }
28 | trait GraphLike[A, V <: VertexLike[A], E <: EdgeLike[A, V]] {
29 | def vertices: Set[V]
30 | def edges: Set[E]
31 | def existsPath(u: V, v: V): Boolean
32 | def add(e: E): GraphLike[A, V, E]
33 | def add(v: V): GraphLike[A, V, E]
34 | }
35 | trait CanBuildEdge[A, V <: VertexLike[A], E <: EdgeLike[A, V]] {
36 | def buildEdge(u: V, v: V): E
37 | }
38 | trait CanBuildGraph[A, V <: VertexLike[A], E <: EdgeLike[A, V]] {
39 | def buildGraph(vertices: Set[V], edges: Set[E]): GraphLike[A, V, E]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/Bias.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.state
18 |
19 | import cats._
20 |
21 | trait Bias[B] {
22 | def apply[E, T: TombStone : PartialOrder](element: E, addition: T, removal: T): Option[E]
23 | }
24 |
25 | object Bias {
26 | sealed trait Direction
27 | case class AdditionWins() extends Direction
28 | case class RemovalWins() extends Direction
29 |
30 | implicit object AdditionWinsBias extends Bias[AdditionWins] {
31 | override def apply[E, T: TombStone : PartialOrder](element: E, add: T, remove: T): Option[E] = {
32 | val order = implicitly[PartialOrder[T]]
33 | if (order.gteqv(add, remove)) {
34 | Some(element)
35 | } else {
36 | None
37 | }
38 | }
39 | }
40 |
41 | implicit object RemovalWinsObject extends Bias[RemovalWins] {
42 | override def apply[E, T: TombStone : PartialOrder](element: E, add: T, remove: T): Option[E] = {
43 | val order = implicitly[PartialOrder[T]]
44 | if (order.gteqv(remove, add)) {
45 | None
46 | } else {
47 | Some(element)
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/GSet.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.state
18 |
19 | import cats._
20 |
21 | /** Grow-only set. `Combine` operation is a union of the sets.
22 | *
23 | * @tparam E Contained element
24 | * @see [[com.machinomy.crdt.state.GSet.monoid]] Behaves like a [[cats.Monoid]]
25 | * @see [[com.machinomy.crdt.state.GSet.partialOrder]] Behaves like a [[cats.PartialOrder]]
26 | * @see Shapiro, M., Preguiça, N., Baquero, C., & Zawirski, M. (2011).
27 | * Conflict-free replicated data types.
28 | * In Proceedings of the 13th international conference on Stabilization, safety, and security of distributed systems (pp. 386–400).
29 | * Grenoble, France: Springer-Verlag.
30 | * Retrieved from [[http://dl.acm.org/citation.cfm?id=2050642]]
31 | */
32 | class GSet[E](val state: Set[E]) extends Convergent[E, Set[E]] {
33 | type Self = GSet[E]
34 |
35 | /** Add `element` to the set.
36 | *
37 | * @return Updated GSet
38 | */
39 | def +(element: E) = new GSet[E](state + element)
40 |
41 | /** @return Value of the set.
42 | */
43 | override def value: Set[E] = state
44 | }
45 |
46 | object GSet {
47 | /** Implements [[cats.Monoid]] type class for [[GSet]].
48 | *
49 | * @tparam E Contained element
50 | */
51 | implicit def monoid[E] = new Monoid[GSet[E]] {
52 | override def empty: GSet[E] = apply[E]()
53 |
54 | override def combine(x: GSet[E], y: GSet[E]): GSet[E] =
55 | new GSet[E](x.value ++ y.value)
56 | }
57 |
58 | /** Implements [[cats.PartialOrder]] type class for [[GSet]].
59 | *
60 | * @tparam E Contained element
61 | */
62 | implicit def partialOrder[E] = PartialOrder.byLteqv[GSet[E]] { (x, y) =>
63 | x.state subsetOf y.state
64 | }
65 |
66 | def apply[E]() = new GSet[E](Set.empty[E])
67 |
68 | def apply[E](state: Set[E]) = new GSet[E](state)
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/state/GCounterSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.state
2 |
3 | import cats.kernel.{Eq, Monoid}
4 | import cats.syntax.all._
5 | import org.scalacheck.Gen
6 | import org.scalatest.FunSuite
7 | import org.scalatest.prop.PropertyChecks
8 |
9 | class GCounterSuite extends FunSuite with PropertyChecks {
10 | val replicaGen = Gen.posNum[Int]
11 | val valueGen = Gen.posNum[Int]
12 | val counterGen = for {
13 | id <- replicaGen
14 | value <- valueGen
15 | } yield GCounter[Int, Int]() + (id -> value)
16 | val eq = implicitly[Eq[GCounter[Int, Int]]]
17 |
18 | test("Fresh GCounter is empty") {
19 | val fresh = GCounter[Int, Int]()
20 | assert(Monoid[GCounter[Int, Int]].isEmpty(fresh))
21 | assert(fresh.value === 0)
22 | }
23 |
24 | test("could be calculated") {
25 | val counter = GCounter[Int, Int]().increment(1, 2).increment(2, 3)
26 | assert(counter.value === 5)
27 | }
28 |
29 | test("could be combined") {
30 | val a = GCounter[Int, Int]() + (1, 2) + (2, 3)
31 | val b = GCounter[Int, Int]() + (1, 2) + (3, 4)
32 | val c = a |+| b
33 | assert(c.value === 2 + 3 + 4)
34 | }
35 |
36 | test("could present replica value") {
37 | val a = GCounter[Int, Int]()
38 | assert(a.get(0) === 0)
39 | val b = a.increment(1, 2).increment(2, 3)
40 | assert(b.get(1) === 2)
41 | assert(b.get(2) === 3)
42 | }
43 |
44 | test("equality") {
45 | val a = GCounter[Int, Int]()
46 | val b = GCounter[Int, Int]()
47 | assert(eq.eqv(a, b))
48 |
49 | val a1 = a + (1 -> 1)
50 | assert(eq.neqv(a1, b))
51 | val b1 = b + (1 -> 2)
52 | assert(a1 !== b1)
53 | assert(eq.neqv(a1, b1))
54 | val a2 = a1 + (1 -> 1)
55 | assert(eq.eqv(a2, b1))
56 | }
57 |
58 | test("associativity") {
59 | forAll(Gen.listOfN(3, counterGen)) {
60 | case x :: y :: z :: Nil =>
61 | val left = x |+| (y |+| z)
62 | val right = (x |+| y) |+| z
63 | assert(eq.eqv(left, right))
64 | case _ => throw new RuntimeException("This is unexpected, really")
65 | }
66 | }
67 |
68 | test("commutativity") {
69 | forAll(Gen.listOfN(2, counterGen)) {
70 | case x :: y :: Nil =>
71 | val left = x |+| y
72 | val right = y |+| x
73 | assert(eq.eqv(left, right))
74 | case _ => throw new RuntimeException("This is unexpected, really")
75 | }
76 | }
77 |
78 | test("idempotency") {
79 | forAll(Gen.listOf(counterGen)) { list =>
80 | whenever(list.nonEmpty) {
81 | val counter = list.reduce(_ |+| _)
82 | assert(eq.eqv(counter, counter |+| counter))
83 | }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/op/ORSet.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.op
18 |
19 | import com.machinomy.crdt.state.TombStone
20 |
21 | case class ORSet[E, T: TombStone](state: Map[E, Set[T]]) {
22 | lazy val value: Set[E] = state.keySet
23 |
24 | def add(e: E): ORSet.UpdateResult[E, T] = {
25 | val t: T = implicitly[TombStone[T]].next
26 | val nextTombstones = state.getOrElse(e, Set.empty[T]) + t
27 | val next = ORSet(state + (e -> nextTombstones))
28 | (next, Some(ORSet.Add(e, t)))
29 | }
30 |
31 | def contains(e: E): Boolean = value.contains(e)
32 |
33 | def remove(e: E): ORSet.UpdateResult[E, T] = {
34 | val tombstones = state.getOrElse(e, Set.empty[T])
35 | val next = ORSet(state - e)
36 | (next, Some(ORSet.Remove(e, tombstones)))
37 | }
38 |
39 | def run(operation: ORSet.Add[E, T]): ORSet.UpdateResult[E, T] =
40 | if (contains(operation.e)) {
41 | (this, None)
42 | } else {
43 | val tombstones = state.getOrElse(operation.e, Set.empty[T])
44 | val nextState = state.updated(operation.e, tombstones)
45 | val next = ORSet(nextState)
46 | (next, Some(operation))
47 | }
48 |
49 | def run(operation: ORSet.Remove[E, T]): ORSet.UpdateResult[E, T] =
50 | if (contains(operation.e)) {
51 | val existingTombstones = state.getOrElse(operation.e, Set.empty[T])
52 | val nextTombstones = existingTombstones -- operation.tombstones
53 | val nextState =
54 | if (nextTombstones.nonEmpty) {
55 | state.updated(operation.e, nextTombstones)
56 | } else {
57 | state - operation.e
58 | }
59 | (new ORSet(nextState), Some(operation))
60 | } else {
61 | (this, None)
62 | }
63 |
64 | }
65 |
66 | object ORSet {
67 | type UpdateResult[E, T] = (ORSet[E, T], Option[ORSet.Update[E]])
68 |
69 | def apply[E, T: TombStone](): ORSet[E, T] = ORSet(Map.empty[E, Set[T]])
70 |
71 | sealed trait Update[E]
72 | case class Add[E, T: TombStone](e: E, t: T) extends Update[E]
73 | case class Remove[E, T: TombStone](e: E, tombstones: Set[T]) extends Update[E]
74 | }
75 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/op/ORSetSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.op
2 |
3 | import org.scalatest.{FunSuite, Matchers}
4 | import org.scalatest.prop.PropertyChecks
5 | import org.scalacheck.Gen
6 | import com.github.nscala_time.time.Imports._
7 | import com.machinomy.crdt.state.TombStone
8 |
9 | class ORSetSuite extends FunSuite with PropertyChecks with Matchers {
10 | test("fresh is empty") {
11 | val set = ORSet[Int, DateTime]()
12 | assert(set.value.isEmpty)
13 | }
14 |
15 | test("add") {
16 | forAll(Gen.posNum[Int]) { (i: Int) =>
17 | val set = ORSet[Int, DateTime]()
18 | val (nextSet, operation) = set.add(i)
19 | nextSet.value should be(Set(i))
20 | operation.isDefined should be(true)
21 | operation.get.isInstanceOf[ORSet.Add[_, _]] should be(true)
22 | }
23 | }
24 |
25 | test("remove if present") {
26 | forAll(Gen.posNum[Int]) { (i: Int) =>
27 | val initial = ORSet[Int, DateTime]()
28 | val (set, _) = initial.add(i)
29 | val (finalSet, operation) = set.remove(i)
30 | finalSet.value should be(empty)
31 | operation.isDefined should be(true)
32 | operation.get.isInstanceOf[ORSet.Remove[_, _]] should be(true)
33 | }
34 | }
35 |
36 | test("remove if absent") {
37 | forAll(Gen.posNum[Int]) { (i: Int) =>
38 | val initial = ORSet[Int, DateTime]()
39 | val (set, operation) = initial.remove(i)
40 | set.value should be(empty)
41 | operation shouldNot be(empty)
42 | operation.get.isInstanceOf[ORSet.Remove[_, _]] should be(true)
43 | }
44 | }
45 |
46 | test("add operation") {
47 | forAll(Gen.posNum[Int]) { (i: Int) =>
48 | val set = ORSet[Int, DateTime]()
49 | val addOp = ORSet.Add(i, implicitly[TombStone[DateTime]].next)
50 | val (nextSet, operation) = set.run(addOp)
51 | nextSet.value should be(Set(i))
52 | operation shouldNot be(empty)
53 | operation.get.isInstanceOf[ORSet.Add[_, _]] should be(true)
54 | }
55 | }
56 |
57 | test("remove operation if present") {
58 | forAll(Gen.posNum[Int]) { (i: Int) =>
59 | val initial = ORSet[Int, DateTime]()
60 | val (set, _) = initial.add(i)
61 | val removeOp = ORSet.Remove(i, set.state(i))
62 | val (finalSet, operation) = set.run(removeOp)
63 | finalSet.value should be(empty)
64 | operation.isDefined should be(true)
65 | operation.get.isInstanceOf[ORSet.Remove[_, _]] should be(true)
66 | }
67 | }
68 |
69 | test("remove operation if absent") {
70 | forAll(Gen.posNum[Int]) { (i: Int) =>
71 | val initial = ORSet[Int, DateTime]()
72 | val removeOp = ORSet.Remove(i, Set(implicitly[TombStone[DateTime]].next))
73 | val (set, operation) = initial.run(removeOp)
74 | set.value should be(empty)
75 | operation should be(empty)
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/op/MonotonicDag.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.op
18 |
19 | import scala.language.higherKinds
20 | import scalax.collection.Graph
21 | import scalax.collection.GraphEdge._
22 | import scalax.collection.GraphPredef._
23 |
24 | case class MonotonicDag[V, E[X] <: DiEdgeLikeIn[X], G <: Graph[V, E]](graph: G)(implicit graphLike: DiGraphLike[V, E, G]) {
25 | def value = graph
26 |
27 | def add(e: E[V] with OuterEdge[V, E]): MonotonicDag.UpdateResult[V, E, G] = {
28 | val from = graphLike.fromVertex(graph, e)
29 | val to = graphLike.toVertex(value, e)
30 | val containsSource = graphLike.contains(value, from)
31 | val containsDestination = graphLike.contains(value, to)
32 | val effective = graphLike.addEdge(value, e)
33 | if (containsSource && containsDestination && graphLike.existsPath(effective, from, to)) {
34 | (MonotonicDag[V, E, G](effective), Some(MonotonicDag.AddEdge[V, E, G](e)))
35 | } else {
36 | (this, None)
37 | }
38 | }
39 |
40 | def add(v: V, left: V, right: V): MonotonicDag.UpdateResult[V, E, G] = {
41 | val absentV = !graphLike.contains(graph, v)
42 | val presentLeft = graphLike.contains(graph, left)
43 | val presentRight = graphLike.contains(graph, right)
44 | val existsPath = graphLike.existsPath(graph, left, right)
45 | if (absentV && presentLeft && presentRight && existsPath) {
46 | val g1 = graphLike.addVertex(graph, v)
47 | val g2 = graphLike.addEdge(g1, graphLike.buildEdge(left, v))
48 | val g3 = graphLike.addEdge(g2, graphLike.buildEdge(v, right))
49 | val next = MonotonicDag[V, E, G](g3)
50 | (next, Some(MonotonicDag.AddVertex[V, E, G](v, left, right)))
51 | } else {
52 | (this, None)
53 | }
54 | }
55 | }
56 |
57 | object MonotonicDag {
58 | type UpdateResult[V, E[X] <: DiEdgeLikeIn[X], G <: Graph[V, E]] = (MonotonicDag[V, E, G], Option[Update[V, E, G]])
59 |
60 | sealed trait Update[V, E[X] <: DiEdgeLikeIn[X], G <: Graph[V, E]]
61 | case class AddEdge[V, E[X] <: DiEdgeLikeIn[X], G <: Graph[V, E]](e: E[V]) extends Update[V, E, G]
62 | case class AddVertex[V, E[X] <: DiEdgeLikeIn[X], G <: Graph[V, E]](v: V, left: V, right: V) extends Update[V, E, G]
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/op/PartialOrderDag.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.op
18 |
19 | import com.machinomy.crdt.op.PartialOrderDag.Update
20 | import com.machinomy.crdt.state.TPSet
21 |
22 | import scala.language.higherKinds
23 | import scalax.collection.Graph
24 | import scalax.collection.GraphPredef._
25 |
26 | /**
27 | * Add-Remove Partial Order
28 | */
29 | case class PartialOrderDag[V, E[X] <: DiEdgeLikeIn[X]](vertices: TPSet[V], edges: Set[E[V]])(implicit graphLike: DiGraphLike[V, E, Graph[V, E]]) {
30 | lazy val value: Graph[V, E] = graphLike.buildGraph(vertices.value, edges)
31 |
32 | def add(v: V, left: V, right: V): PartialOrderDag.UpdateResult[V, E] = {
33 | if (!value.contains(v) && graphLike.existsPath(value, left, right)) {
34 | val nextVertices = vertices + v
35 | val leftEdge = graphLike.buildEdge(left, v).edge
36 | val rightEdge = graphLike.buildEdge(v, right).edge
37 | val nextEdges = edges + leftEdge + rightEdge
38 | val next = new PartialOrderDag[V, E](nextVertices, nextEdges)
39 | (next, Some(PartialOrderDag.AddVertex[V, E](v, left, right)))
40 | } else {
41 | (this, None)
42 | }
43 | }
44 |
45 | // @todo This does not work.
46 | def remove(v: V): PartialOrderDag.UpdateResult[V, E] =
47 | if (value.contains(v) && !graphLike.isSentinel(v)) {
48 | val nextVertices = vertices - v
49 | val next = new PartialOrderDag[V, E](nextVertices, edges)
50 | (next, Some(PartialOrderDag.RemoveVertex[V, E](v)))
51 | } else {
52 | (this, None)
53 | }
54 |
55 | def run(operation: PartialOrderDag.AddVertex[V, E]): (PartialOrderDag[V, E], Option[Update[V, E]]) =
56 | add(operation.v, operation.left, operation.right)
57 |
58 | def run(operation: PartialOrderDag.RemoveVertex[V, E]): (PartialOrderDag[V, E], Option[Update[V, E]]) =
59 | remove(operation.v)
60 | }
61 |
62 | object PartialOrderDag {
63 | type UpdateResult[V, E[X] <: DiEdgeLikeIn[X]] = (PartialOrderDag[V, E], Option[Update[V, E]])
64 |
65 | sealed trait Update[V, E[X] <: DiEdgeLikeIn[X]]
66 |
67 | case class AddVertex[V, E[X] <: DiEdgeLikeIn[X]](v: V, left: V, right: V) extends Update[V, E]
68 |
69 | case class RemoveVertex[V, E[X] <: DiEdgeLikeIn[X]](v: V) extends Update[V, E]
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/op/DiGraphLike.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.op
18 |
19 | import scala.language.higherKinds
20 | import scalax.collection.Graph
21 | import scalax.collection.GraphEdge._
22 | import scalax.collection.GraphPredef._
23 |
24 | trait DiGraphLike[V, E[X] <: DiEdgeLikeIn[X], G <: Graph[V, E]] {
25 | def contains(graph: G, vertex: V): Boolean
26 | def addEdge(graph: G, edge: E[V] with OuterEdge[V, E]): G
27 | def addVertex(graph: G, vertex: V): G
28 | def fromVertex(graph: G, edge: E[V] with OuterEdge[V, E]): V
29 | def toVertex(graph: G, edge: E[V] with OuterEdge[V, E]): V
30 | def path(graph: G, from: V, to: V): Option[G#Path]
31 | def existsPath(graph: G, from: V, to: V): Boolean
32 | def buildEdge(from: V, to: V): E[V] with OuterEdge[V, E] // @todo CanBuildEdge
33 | def buildGraph(vertices: Set[V], edges: Set[E[V]]): G // @todo CanBuildGraph
34 | def isSentinel(v: V): Boolean // @todo CanDetectSentinel
35 | }
36 |
37 | object DiGraphLike {
38 | implicit object IntDiGraphLike extends DiGraphLike[Int, DiEdge, Graph[Int, DiEdge]] {
39 | override def contains(graph: Graph[Int, DiEdge], vertex: Int): Boolean =
40 | graph.contains(vertex)
41 | override def addEdge(graph: Graph[Int, DiEdge], edge: DiEdge[Int] with OuterEdge[Int, DiEdge]): Graph[Int, DiEdge] =
42 | graph + edge
43 | override def addVertex(graph: Graph[Int, DiEdge], vertex: Int): Graph[Int, DiEdge] =
44 | graph + vertex
45 | override def fromVertex(graph: Graph[Int, DiEdge], edge: DiEdge[Int] with OuterEdge[Int, DiEdge]): Int =
46 | edge.from
47 | override def toVertex(graph: Graph[Int, DiEdge], edge: DiEdge[Int] with OuterEdge[Int, DiEdge]): Int =
48 | edge.to
49 | override def path(graph: Graph[Int, DiEdge], from: Int, to: Int): Option[graph.Path] =
50 | for {
51 | fromVertex <- graph.find(from)
52 | toVertex <- graph.find(to)
53 | path <- fromVertex.pathTo(toVertex)
54 | } yield path
55 | override def existsPath(graph: Graph[Int, DiEdge], from: Int, to: Int): Boolean =
56 | path(graph, from, to).isDefined
57 | override def buildEdge(from: Int, to: Int): DiEdge[Int] =
58 | from ~> to
59 | override def buildGraph(vertices: Set[Int], edges: Set[DiEdge[Int]]): Graph[Int, DiEdge] = Graph.from(vertices, edges)
60 | override def isSentinel(v: Int): Boolean = v == 1 || v == 100
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/TPSet.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.state
18 |
19 | import cats._
20 | import cats.syntax.all._
21 |
22 | /**
23 | * 2P-Set, or two-phase set. Contains one [[GSet]] for additions, and one for removals
24 | * Removing of an element is allowed, only if it is present in the set of additions.
25 | * `Combine` operation combines additions and removals as a GSet. Element can only be added or removed once.
26 | *
27 | * @tparam E Contained element
28 | * @see [[com.machinomy.crdt.state.TPSet.monoid]] Behaves like a [[cats.Monoid]]
29 | * @see [[com.machinomy.crdt.state.TPSet.partialOrder]] Behaves like a [[cats.PartialOrder]]
30 | * @see Shapiro, M., Preguiça, N., Baquero, C., & Zawirski, M. (2011).
31 | * Conflict-free replicated data types.
32 | * In Proceedings of the 13th international conference on Stabilization, safety, and security of distributed systems (pp. 386–400).
33 | * Grenoble, France: Springer-Verlag.
34 | * Retrieved from [[http://dl.acm.org/citation.cfm?id=2050642]]
35 | */
36 | case class TPSet[E](additions: GSet[E] = GSet[E](), removals: GSet[E] = GSet[E]()) extends Convergent[E, Set[E]] {
37 | type Self = TPSet[E]
38 |
39 | /** Add element to the set.
40 | *
41 | * @return Updated TPSet.
42 | */
43 | def +(element: E): TPSet[E] = copy(additions = additions + element)
44 |
45 | /** Remove element from the set.
46 | *
47 | * @return Updated TPSet.
48 | */
49 | def -(element: E): TPSet[E] = if (additions.value.contains(element)) {
50 | copy(removals = removals + element)
51 | } else {
52 | this
53 | }
54 |
55 | /** @return Value of the set.
56 | */
57 | override def value: Set[E] = additions.value -- removals.value
58 | }
59 |
60 | object TPSet {
61 | /** Implements [[cats.Monoid]] type class for [[TPSet]].
62 | *
63 | * @tparam E Contained element
64 | */
65 | implicit def monoid[E](implicit gSetMonoid: Monoid[GSet[E]]) = new Monoid[TPSet[E]] {
66 | override def empty: TPSet[E] = new TPSet[E](gSetMonoid.empty, gSetMonoid.empty)
67 |
68 | override def combine(x: TPSet[E], y: TPSet[E]): TPSet[E] = {
69 | val additions = x.additions |+| y.additions
70 | val removals = x.removals |+| y.removals
71 | new TPSet[E](additions, removals)
72 | }
73 | }
74 |
75 | /** Implements [[cats.PartialOrder]] type class for [[TPSet]].
76 | *
77 | * @tparam E Contained element
78 | */
79 | implicit def partialOrder[E](implicit gSetPartialOrder: PartialOrder[GSet[E]]) = PartialOrder.byLteqv[TPSet[E]] { (x, y) =>
80 | val additions = gSetPartialOrder.lteqv(x.additions, y.additions)
81 | val removals = gSetPartialOrder.lteqv(x.removals, y.removals)
82 | additions && removals
83 | }
84 |
85 | def apply[E](elements: Set[E]): TPSet[E] = TPSet[E](GSet[E](elements))
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/op/TPTPGraph.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.op
18 |
19 | import com.machinomy.crdt.op.GraphLikeA._
20 |
21 | /**
22 | * 2P2P-Graph
23 | */
24 | case class TPTPGraph[A, V <: VertexLike[A], E <: EdgeLike[A, V]](va: Set[V], vr: Set[V], ea: Set[E], er: Set[E]) {
25 | type Self = TPTPGraph[A, V, E]
26 |
27 | def contains(v: V): Boolean = vertices.contains(v)
28 |
29 | def contains(e: E): Boolean = contains(e.u) && contains(e.v) && (ea -- er).contains(e)
30 |
31 | def add(v: V): TPTPGraph.UpdateResult[A, V, E] = {
32 | val nextVa = va + v
33 | val next: Self = copy(va = nextVa)
34 | (next, Some(TPTPGraph.AddVertex(v)))
35 | }
36 |
37 | def add(e: E): TPTPGraph.UpdateResult[A, V, E] = {
38 | if (contains(e.u) && contains(e.v)) {
39 | val nextEa = ea + e
40 | val next: Self = copy(ea = nextEa)
41 | (next, Some(TPTPGraph.AddEdge[A, V, E](e)))
42 | } else {
43 | (this, None)
44 | }
45 | }
46 |
47 | def isSingle(v: V): Boolean = (ea -- er).forall(e => e.u != v && e.v != v)
48 |
49 | def remove(v: V): TPTPGraph.UpdateResult[A, V, E] = {
50 | if (contains(v) && isSingle(v)) {
51 | val nextVr = vr + v
52 | val next: Self = copy(vr = nextVr)
53 | (next, Some(TPTPGraph.RemoveVertex[A, V](v)))
54 | } else {
55 | (this, None)
56 | }
57 | }
58 |
59 | def remove(e: E): TPTPGraph.UpdateResult[A, V, E] = {
60 | if (contains(e)) {
61 | val nextEr = er + e
62 | val next: Self = copy(er = nextEr)
63 | (next, Some(TPTPGraph.RemoveEdge[A, V, E](e)))
64 | } else {
65 | (this, None)
66 | }
67 | }
68 |
69 | def vertices: Set[V] = va -- vr
70 |
71 | def edges: Set[E] = ea -- er
72 |
73 | def run(operation: TPTPGraph.AddVertex[A, V]): TPTPGraph.UpdateResult[A, V, E] = add(operation.v)
74 |
75 | def run(operation: TPTPGraph.AddEdge[A, V, E]): TPTPGraph.UpdateResult[A, V, E] = add(operation.e)
76 |
77 | def run(operation: TPTPGraph.RemoveVertex[A, V]): TPTPGraph.UpdateResult[A, V, E] = remove(operation.v)
78 |
79 | def run(operation: TPTPGraph.RemoveEdge[A, V, E]): TPTPGraph.UpdateResult[A, V, E] = remove(operation.e)
80 | }
81 |
82 | object TPTPGraph {
83 | type UpdateResult[A, V <: VertexLike[A], E <: EdgeLike[A, V]] = (TPTPGraph[A, V, E], Option[TPTPGraph.Update[A]])
84 |
85 | def apply[A, V <: VertexLike[A], E <: EdgeLike[A, V]](): TPTPGraph[A, V, E] = {
86 | val va = Set.empty[V]
87 | val vr = Set.empty[V]
88 | val ea = Set.empty[E]
89 | val er = Set.empty[E]
90 | new TPTPGraph(va, vr, ea, er)
91 | }
92 |
93 | sealed trait Update[A]
94 | case class AddVertex[A, V <: VertexLike[A]](v: V) extends Update[A]
95 | case class AddEdge[A, V <: VertexLike[A], E <: EdgeLike[A, V]](e: E) extends Update[A]
96 | case class RemoveVertex[A, V <: VertexLike[A]](v: V) extends Update[A]
97 | case class RemoveEdge[A, V <: VertexLike[A], E <: EdgeLike[A, V]](e: E) extends Update[A]
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/MCSet.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.state
18 |
19 | import cats._
20 |
21 | /** Max-Change Set. Assigns each element an integer. If the number is odd, the element is considered present.
22 | * Odd number means the element is absent. To add an element you increment __odd__ number. To remove the element,
23 | * __even__ number is decremented.
24 | *
25 | * @tparam E Element contained.
26 | * @tparam T Number assigned to an element, must implement [[scala.math.Integral]] type class.
27 | * @todo Find a paper to cite.
28 | */
29 | case class MCSet[E, T](state: Map[E, T])(implicit integral: Integral[T]) extends Convergent[E, Set[E]] {
30 | type Self = MCSet[E, T]
31 |
32 | /** Add an element to the set.
33 | *
34 | * @return Updated MCSet.
35 | */
36 | def +(element: E): Self = {
37 | val tag = state.getOrElse(element, integral.zero)
38 | if (isPresent(tag)) {
39 | this
40 | } else {
41 | increment(element, tag)
42 | }
43 | }
44 |
45 | /** Remove an element from the set.
46 | *
47 | * @return Updated MCSet.
48 | */
49 | def -(element: E): Self = {
50 | val tag = state.getOrElse(element, integral.zero)
51 | if (isPresent(tag)) {
52 | increment(element, tag)
53 | } else {
54 | this
55 | }
56 | }
57 |
58 | /** Contained [[scala.collection.immutable.Set]].
59 | *
60 | * @return
61 | */
62 | override def value: Set[E] = state.keySet.filter { element =>
63 | val change = state.getOrElse(element, integral.zero)
64 | isPresent(change)
65 | }
66 |
67 | /** Odd means present
68 | *
69 | * @param tag Integer assigned for the element
70 | * @return
71 | */
72 | protected def isPresent(tag: T): Boolean = integral.toInt(tag) % 2 != 0
73 |
74 | private def increment(element: E, tag: T): Self = {
75 | val nextTag = integral.plus(tag, integral.one)
76 | val nextState = state.updated(element, nextTag)
77 | copy(state = nextState)
78 | }
79 | }
80 |
81 | object MCSet {
82 | /** Implements [[cats.Monoid]] type class for [[MCSet]].
83 | *
84 | * @tparam E Contained element
85 | */
86 | implicit def monoid[E, T](implicit integral: Integral[T]) = new Monoid[MCSet[E, T]] {
87 | override def empty: MCSet[E, T] = new MCSet[E, T](Map.empty[E, T])
88 |
89 | override def combine(x: MCSet[E, T], y: MCSet[E, T]): MCSet[E, T] = {
90 | val keys = x.state.keySet ++ y.state.keySet
91 | val pairs =
92 | for (key <- keys) yield (x.state.get(key), y.state.get(key)) match {
93 | case (Some(a), Some(b)) =>
94 | key -> integral.max(a, b)
95 | case (Some(a), None) =>
96 | key -> a
97 | case (None, Some(b)) =>
98 | key -> b
99 | case (None, None) =>
100 | throw new IllegalArgumentException(s"Expected to retrieve value for key $key")
101 | }
102 | MCSet(pairs.toMap)
103 | }
104 | }
105 |
106 | /** Implements [[cats.PartialOrder]] type class for [[MCSet]].
107 | *
108 | * @tparam E Contained element
109 | */
110 | implicit def partialOrder[E, T](implicit integral: Integral[T]) = PartialOrder.byLteqv[MCSet[E, T]] { (x, y) =>
111 | val ids = x.state.keySet
112 | ids.forall { id =>
113 | val yTag = y.state.getOrElse(id, integral.zero)
114 | val xTag = x.state.getOrElse(id, integral.zero)
115 | integral.lteq(xTag, yTag)
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/test/scala/com/machinomy/crdt/op/TPTPGraphSuite.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.op
2 |
3 | import com.machinomy.crdt.op.GraphLikeA._
4 | import org.scalatest.{FunSuite, Matchers}
5 | import org.scalatest.prop.PropertyChecks
6 |
7 | class TPTPGraphSuite extends FunSuite with PropertyChecks with Matchers {
8 | case class Vertex(id: Int) extends VertexLike[Int]
9 | case class Edge(u: Vertex, v: Vertex) extends EdgeLike[Int, Vertex]
10 |
11 | test("initial") {
12 | val g = TPTPGraph[Int, Vertex, Edge]()
13 | g.edges should be(empty)
14 | g.vertices should be(empty)
15 | }
16 |
17 | test("add vertex") {
18 | val g = TPTPGraph[Int, Vertex, Edge]()
19 | val vertex = Vertex(3)
20 | val (g2, op) = g.add(vertex)
21 | g2.edges should be(empty)
22 | g2.vertices should contain(vertex)
23 | op shouldNot be(empty)
24 | op.get.isInstanceOf[TPTPGraph.AddVertex[_, _]] should be(true)
25 | }
26 |
27 | test("add vertex operation") {
28 | val g = TPTPGraph[Int, Vertex, Edge]()
29 | val vertex = Vertex(3)
30 | val addOp = TPTPGraph.AddVertex[Int, Vertex](vertex)
31 | val (g2, op) = g.run(addOp)
32 | g2.edges should be(empty)
33 | g2.vertices should contain(vertex)
34 | op shouldNot be(empty)
35 | op.get.isInstanceOf[TPTPGraph.AddVertex[_, _]] should be(true)
36 | }
37 |
38 | test("add edge") {
39 | val g = TPTPGraph[Int, Vertex, Edge]()
40 | val u = Vertex(3)
41 | val v = Vertex(4)
42 | val (g2, op2) = g.add(u)
43 | val (g3, op3) = g2.add(v)
44 | val e = Edge(u, v)
45 | val (g4, op4) = g3.add(e)
46 |
47 | g4.edges should contain(e)
48 | g4.vertices should contain(u)
49 | g4.vertices should contain(v)
50 | op4 shouldNot be(empty)
51 | op4.get.isInstanceOf[TPTPGraph.AddEdge[_, _, _]] should be(true)
52 | }
53 |
54 | test("add edge operation") {
55 | val g = TPTPGraph[Int, Vertex, Edge]()
56 | val u = Vertex(3)
57 | val v = Vertex(4)
58 | val addOp1 = TPTPGraph.AddVertex[Int, Vertex](u)
59 | val (g2, op2) = g.add(u)
60 | val (g3, op3) = g2.add(v)
61 | val e = Edge(u, v)
62 | val (g4, op4) = g3.add(e)
63 |
64 | g4.edges should contain(e)
65 | g4.vertices should contain(u)
66 | g4.vertices should contain(v)
67 | op4 shouldNot be(empty)
68 | op4.get.isInstanceOf[TPTPGraph.AddEdge[_, _, _]] should be(true)
69 | }
70 |
71 | test("remove vertex") {
72 | val g = TPTPGraph[Int, Vertex, Edge]()
73 | val v = Vertex(3)
74 | val (g2, op2) = g.add(v)
75 | val (g3, op3) = g2.remove(v)
76 |
77 | g3.edges should be(empty)
78 | g3.vertices should be(empty)
79 | }
80 |
81 | test("remove vertex operation") {
82 | val g = TPTPGraph[Int, Vertex, Edge]()
83 | val v = Vertex(3)
84 | val (g2, op2) = g.add(v)
85 | val removeOp = TPTPGraph.RemoveVertex[Int, Vertex](v)
86 | val (g3, op3) = g2.remove(v)
87 |
88 | g3.edges should be(empty)
89 | g3.vertices should be(empty)
90 | }
91 |
92 | test("remove edge") {
93 | val g = TPTPGraph[Int, Vertex, Edge]()
94 | val u = Vertex(3)
95 | val v = Vertex(4)
96 | val (g2, op2) = g.add(u)
97 | val (g3, op3) = g2.add(v)
98 | val e = Edge(u, v)
99 | val (g4, op4) = g3.add(e)
100 | val (g5, op5) = g4.remove(e)
101 |
102 | g5.edges shouldNot contain(e)
103 | g4.vertices should contain(u)
104 | g4.vertices should contain(v)
105 | op5 shouldNot be(empty)
106 | op5.get.isInstanceOf[TPTPGraph.RemoveEdge[_, _, _]] should be(true)
107 | }
108 |
109 | test("remove edge operation") {
110 | val g = TPTPGraph[Int, Vertex, Edge]()
111 | val u = Vertex(3)
112 | val v = Vertex(4)
113 | val (g2, op2) = g.add(u)
114 | val (g3, op3) = g2.add(v)
115 | val e = Edge(u, v)
116 | val (g4, op4) = g3.add(e)
117 | val removeOp = TPTPGraph.RemoveEdge[Int, Vertex, Edge](e)
118 | val (g5, op5) = g4.run(removeOp)
119 |
120 | g5.edges shouldNot contain(e)
121 | g4.vertices should contain(u)
122 | g4.vertices should contain(v)
123 | op5 shouldNot be(empty)
124 | op5.get.isInstanceOf[TPTPGraph.RemoveEdge[_, _, _]] should be(true)
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/PNCounter.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.state
18 |
19 | import cats._
20 | import cats.syntax.all._
21 |
22 | /** Positive-Negative counter. Allows increments and decrements both.
23 | * `combine` operation takes the maximum count for each replica, for each of increments and decrements sets.
24 | * Value is the sum of all replicas.
25 | *
26 | * @tparam R Replica identifier
27 | * @tparam E Counter element, must behave like [[scala.math.Numeric]]
28 | * @see [[com.machinomy.crdt.state.PNCounter.monoid]] Behaves like a [[cats.Monoid]]
29 | * @see [[com.machinomy.crdt.state.PNCounter.partialOrder]] Behaves like a [[cats.PartialOrder]]
30 | * @example
31 | * {{{
32 | * import com.machinomy.crdt.state._
33 | * import cats.syntax.all._
34 | * import cats._
35 | *
36 | * val counter = Monoid[PNCounter[Int, Int]].empty // fresh PN-Counter
37 | * val firstReplica = counter + (1 -> 1) // increment replica 1
38 | * val secondReplica = counter + (2 -> -2) // decrement replica 2
39 | * val firstReplicacombined = firstReplica |+| secondReplica // combine
40 | * val secondReplicacombined = secondReplica |+| firstReplica // combine
41 | * firstReplicacombined == secondReplicacombined // the result is independent of combine order
42 | * firstReplicacombined.value == -1
43 | * }}}
44 | */
45 | case class PNCounter[R, E](increments: GCounter[R, E], decrements: GCounter[R, E])(implicit num: Numeric[E]) extends Convergent[E, E] {
46 | type Self = PNCounter[R, E]
47 |
48 | /** Increment or decrement value for replica `pair._1` by `pair._2`.
49 | *
50 | * @param pair Replica identifier
51 | * @return Updated PNCounter
52 | */
53 | def +(pair: (R, E)): Self = {
54 | val delta = pair._2
55 | if (num.gteq(delta, num.zero)) {
56 | copy(increments = increments + pair)
57 | } else {
58 | val positive = (pair._1, num.minus(num.zero, pair._2))
59 | copy(decrements = decrements + positive)
60 | }
61 | }
62 |
63 | /** Value for `replicaId`, or zero if absent.
64 | *
65 | * @param replicaId Replica identifier
66 | * @return
67 | */
68 | def get(replicaId: R): E = num.minus(increments.get(replicaId), decrements.get(replicaId))
69 |
70 | /** Values per replica, increments minus decrements.
71 | */
72 | def table: Map[R, E] = {
73 | def fill(keys: Set[R], incs: Map[R, E], decs: Map[R, E], table: Map[R, E] = Map.empty): Map[R, E] =
74 | if (keys.isEmpty) {
75 | table
76 | } else {
77 | val key = keys.head
78 | val inc = incs.getOrElse(key, num.zero)
79 | val dec = decs.getOrElse(key, num.zero)
80 | fill(keys.tail, incs, decs, table.updated(key, num.minus(inc, dec)))
81 | }
82 |
83 | val keys: Set[R] = increments.state.keySet ++ decrements.state.keySet
84 | fill(keys, increments.state, decrements.state)
85 | }
86 |
87 | /** @return Value of the counter.
88 | */
89 | override def value: E = num.minus(increments.value, decrements.value)
90 | }
91 |
92 | object PNCounter {
93 | /** Implements [[cats.Monoid]] type class for [[PNCounter]].
94 | *
95 | * @tparam R Replica identifier
96 | * @tparam E Counter element, must behave like [[scala.math.Numeric]]
97 | */
98 | implicit def monoid[R, E: Numeric] = new Monoid[PNCounter[R, E]] {
99 | override def empty: PNCounter[R, E] = new PNCounter(Monoid[GCounter[R, E]].empty, Monoid[GCounter[R, E]].empty)
100 |
101 | override def combine(x: PNCounter[R, E], y: PNCounter[R, E]): PNCounter[R, E] = {
102 | val increments = x.increments |+| y.increments
103 | val decrements = x.decrements |+| y.decrements
104 | new PNCounter[R, E](increments, decrements)
105 | }
106 | }
107 |
108 | /** Implements [[cats.PartialOrder]] type class for [[PNCounter]].
109 | *
110 | * @tparam R Replica identifier
111 | * @tparam E Counter element, must behave like [[scala.math.Numeric]]
112 | */
113 | implicit def partialOrder[R, E](implicit num: Numeric[E]) = PartialOrder.byLteqv[PNCounter[R, E]] { (x, y) =>
114 | val ids = x.increments.state.keySet ++
115 | y.increments.state.keySet ++
116 | x.decrements.state.keySet ++
117 | y.decrements.state.keySet
118 | ids.forall { id =>
119 | val xInc = x.increments.state.getOrElse(id, num.zero)
120 | val yInc = y.increments.state.getOrElse(id, num.zero)
121 | val xDec = x.decrements.state.getOrElse(id, num.zero)
122 | val yDec = y.decrements.state.getOrElse(id, num.zero)
123 | num.lteq(xInc, yInc) && num.lteq(xDec, yDec)
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/GCounter.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.state
18 |
19 | import cats._
20 |
21 | /** Grow-only counter. Could be incremented only.
22 | * `combine` operation takes the maximum count for each replica.
23 | * Value is the sum of all replicas.
24 | *
25 | * @tparam R Replica identifier
26 | * @tparam E Counter element, must behave like [[scala.math.Numeric]]
27 | * @example
28 | * {{{
29 | * import com.machinomy.crdt.state.GCounter
30 | * import cats.syntax.all._
31 | * import cats._
32 | *
33 | * val counter = Monoid[GCounter[Int, Int]].empty
34 | * val firstReplica = counter + (1 -> 1)
35 | * val secondReplica = counter + (2 -> 2)
36 | * val firstReplicaCombined = firstReplica |+| secondReplica
37 | * val secondReplicaCombined = secondReplica |+| firstReplica
38 | *
39 | * firstReplicaCombined == secondReplicaCombined
40 | * }}}
41 | * @see [[com.machinomy.crdt.state.GCounter.monoid]] Behaves like a [[cats.kernel.Monoid]]
42 | * @see [[com.machinomy.crdt.state.GCounter.partialOrder]] Behaves like a [[cats.kernel.PartialOrder]]
43 | * @see Shapiro, M., Preguiça, N., Baquero, C., & Zawirski, M. (2011).
44 | * Conflict-free replicated data types.
45 | * In Proceedings of the 13th international conference on Stabilization, safety, and security of distributed systems (pp. 386–400).
46 | * Grenoble, France: Springer-Verlag.
47 | * Retrieved from [[http://dl.acm.org/citation.cfm?id=2050642]]
48 | */
49 | case class GCounter[R, E](state: Map[R, E])(implicit num: Numeric[E]) extends Convergent[E, E] {
50 | type Self = GCounter[R, E]
51 |
52 | /** Increment value for replica `pair._1` by `pair._2`. Only positive values are allowed.
53 | *
54 | * @see [[increment]]
55 | * @param pair Replica identifier
56 | * @return Updated GCounter
57 | */
58 | def +(pair: (R, E)): Self = increment(pair._1, pair._2)
59 |
60 | /** Increment value for replica `replicaId` by `delta`. Only positive values are allowed.
61 | *
62 | * @see [[+]]
63 | * @param replicaId Replica identifier.
64 | * @param delta Increment of a counter
65 | * @return Updated GCounter
66 | */
67 | def increment(replicaId: R, delta: E): Self = {
68 | require(num.gteq(delta, num.zero), "Can only increment GCounter")
69 | if (num.equiv(delta, num.zero)) {
70 | this
71 | } else {
72 | state.get(replicaId) match {
73 | case Some(value) => new GCounter[R, E](state.updated(replicaId, num.plus(value, delta)))
74 | case None => new GCounter[R, E](state.updated(replicaId, delta))
75 | }
76 | }
77 | }
78 |
79 | /** Value for `replicaId`, or zero if absent.
80 | *
81 | * @param replicaId Replica identifier
82 | * @return
83 | */
84 | def get(replicaId: R): E = state.getOrElse(replicaId, num.zero)
85 |
86 | /** @return Value of the counter.
87 | */
88 | override def value: E = state.values.sum
89 | }
90 |
91 | object GCounter {
92 | /** Implements [[cats.kernel.Monoid]] type class for [[GCounter]].
93 | *
94 | * @tparam R Replica identifier
95 | * @tparam E Counter element, must behave like [[scala.math.Numeric]]
96 | */
97 | implicit def monoid[R, E](implicit num: Numeric[E]) = new Monoid[GCounter[R, E]] {
98 | override def empty: GCounter[R, E] = new GCounter[R, E](Map.empty[R, E])
99 |
100 | override def combine(x: GCounter[R, E], y: GCounter[R, E]): GCounter[R, E] = {
101 | def fill(ids: Set[R], a: Map[R, E], b: Map[R, E], result: Map[R, E] = Map.empty): Map[R, E] =
102 | if (ids.isEmpty) {
103 | result
104 | } else {
105 | val key = ids.head
106 | val valueA = a.getOrElse(key, num.zero)
107 | val valueB = b.getOrElse(key, num.zero)
108 | fill(ids.tail, a, b, result.updated(key, num.max(valueA, valueB)))
109 | }
110 | val ids = x.state.keySet ++ y.state.keySet
111 | GCounter(fill(ids, x.state, y.state))
112 | }
113 | }
114 |
115 | /** Implements [[cats.PartialOrder]] type class for [[GCounter]].
116 | *
117 | * @tparam R Replica identifier
118 | * @tparam E Counter element, must behave like [[scala.math.Numeric]]
119 | */
120 | implicit def partialOrder[R, E](implicit num: Numeric[E]) = PartialOrder.byLteqv[GCounter[R, E]] { (x, y) =>
121 | val ids = x.state.keySet ++ y.state.keySet
122 | ids.forall { id =>
123 | val xValue = x.state.getOrElse(id, num.zero)
124 | val yValue = y.state.getOrElse(id, num.zero)
125 | num.lteq(xValue, yValue)
126 | }
127 | }
128 |
129 | def apply[R, E: Numeric](): GCounter[R, E] = new GCounter[R, E](Map.empty[R, E])
130 | }
131 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/ORSet.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Machinomy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.machinomy.crdt.state
18 |
19 | import cats._
20 |
21 | /** Observed-Removed Set. An element is assigned sets of `additions` and `removals`.
22 | * Every addition results in adding a unique tag to the `additions` set. Removal leads to removing
23 | * the tags observed in a source replica for the element (see [[com.machinomy.crdt.state.ORSet.Moves]]). Actually,
24 | * it moves the tags to `removals` set. When combining, both sets are united for every element.
25 | *
26 | * @tparam E Element of the set
27 | * @tparam T Unique tag
28 | * @see [[com.machinomy.crdt.state.ORSet.monoid]] Behaves like a [[cats.Monoid]]
29 | * @see [[com.machinomy.crdt.state.ORSet.partialOrder]] Behaves like a [[cats.PartialOrder]]
30 | * @see Shapiro, M., Preguiça, N., Baquero, C., & Zawirski, M. (2011).
31 | * Conflict-free replicated data types.
32 | * In Proceedings of the 13th international conference on Stabilization, safety, and security of distributed systems (pp. 386–400).
33 | * Grenoble, France: Springer-Verlag.
34 | * Retrieved from [[http://dl.acm.org/citation.cfm?id=2050642]]
35 | */
36 | case class ORSet[E, T: TombStone](state: Map[E, ORSet.Moves[T]]) extends Convergent[E, Set[E]] {
37 | type Self = ORSet[E, T]
38 |
39 | /** Add `element` to the set, using `stone` as a unique tag.
40 | *
41 | * @return Updated ORSet
42 | */
43 | def +(element: E, stone: T): Self = {
44 | val moves: ORSet.Moves[T] = state.getOrElse(element, ORSet.Moves[T]())
45 | val nextAdditions = moves.additions + stone
46 | val nextMoves = moves.copy(additions = nextAdditions)
47 | copy(state = state.updated(element, nextMoves))
48 | }
49 |
50 | /** Add `pair._1` to the set, using `pair._2` as a unique tag.
51 | *
52 | * @return Updated ORSet
53 | */
54 | def +(pair: (E, T)): Self = this + (pair._1, pair._2)
55 |
56 | /** Add `element` to the set, and generate `stone` on the fly.
57 | *
58 | * @return Updated ORSet
59 | */
60 | def +(element: E): Self = this + (element, implicitly[TombStone[T]].next)
61 |
62 | /** Remove `element` from the set.
63 | *
64 | * @return Updated ORSet
65 | */
66 | def -(element: E): Self = {
67 | val moves: ORSet.Moves[T] = state.getOrElse(element, ORSet.Moves[T]())
68 | val nextRemovals = moves.removals ++ moves.additions
69 | val nextMoves = moves.copy(removals = nextRemovals)
70 | copy(state = state.updated(element, nextMoves))
71 | }
72 |
73 | /** @return Value of the set.
74 | */
75 | override def value: Set[E] = state.keySet.filter { element =>
76 | val moves: ORSet.Moves[T] = state.getOrElse(element, ORSet.Moves[T]())
77 | (moves.additions -- moves.removals).nonEmpty
78 | }
79 | }
80 |
81 | object ORSet {
82 |
83 | /** Record addition and removal tags for [[ORSet]].
84 | *
85 | * @tparam T Addition and removal tag
86 | */
87 | case class Moves[T](additions: Set[T] = Set.empty[T], removals: Set[T] = Set.empty[T])
88 |
89 | def apply[E, T: TombStone](): ORSet[E, T] = ORSet[E, T](Map.empty[E, Moves[T]])
90 |
91 | /** Implements [[cats.Monoid]] type class for [[ORSet]].
92 | *
93 | * @tparam E Contained element
94 | * @tparam T Unique tag
95 | */
96 | implicit def monoid[E, T: TombStone] = new Monoid[ORSet[E, T]] {
97 | override def empty: ORSet[E, T] = new ORSet[E, T](Map.empty[E, Moves[T]])
98 |
99 | override def combine(x: ORSet[E, T], y: ORSet[E, T]): ORSet[E, T] = {
100 | val keys = x.state.keySet ++ y.state.keySet
101 | val nextStateSet =
102 | for {
103 | k <- keys
104 | } yield {
105 | val xMoves = x.state.getOrElse(k, Moves[T]())
106 | val yMoves = y.state.getOrElse(k, Moves[T]())
107 | val additions = xMoves.additions ++ yMoves.additions
108 | val removals = xMoves.removals ++ yMoves.removals
109 | k -> Moves(additions, removals)
110 | }
111 | new ORSet[E, T](nextStateSet.toMap)
112 | }
113 | }
114 |
115 | /** Implements [[cats.PartialOrder]] type class for [[ORSet]].
116 | *
117 | * @tparam E Contained element
118 | * @tparam T Unique tag
119 | */
120 | implicit def partialOrder[E, T: TombStone] = PartialOrder.byLteqv[ORSet[E, T]] { (x, y) =>
121 | val elements = x.state.keySet
122 | elements.forall { element =>
123 | val xMoves = x.state.getOrElse(element, Moves[T]())
124 | val yMoves = y.state.getOrElse(element, Moves[T]())
125 | val xAdditions = xMoves.additions
126 | val yAdditions = yMoves.additions
127 | val xRemovals = xMoves.removals
128 | val yRemovals = yMoves.removals
129 | (xAdditions subsetOf yAdditions) && (xRemovals subsetOf yRemovals)
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/main/scala/com/machinomy/crdt/state/LWWElementSet.scala:
--------------------------------------------------------------------------------
1 | package com.machinomy.crdt.state
2 |
3 | import cats._
4 | import cats.syntax.all._
5 | import com.machinomy.crdt.state.LWWElementSet.Moves
6 |
7 | /** Last-Writer-Wins Set attaches a timestamp to an element addition or removal. An element is present iff addition timestamp is bigger than that of removal.
8 | * Concurrent addition and removal is resolved using `Bias`: either addition or removal always wins.
9 | *
10 | * @tparam E Element contained
11 | * @tparam T Timestamp
12 | * @tparam B Bias
13 | * @see [[com.machinomy.crdt.state.LWWElementSet.monoid]] Behaves like a [[cats.Monoid]]
14 | * @see [[com.machinomy.crdt.state.LWWElementSet.partialOrder]] Behaves like a [[cats.PartialOrder]]
15 | * @see [[com.machinomy.crdt.state.Bias]] Either addition or removal always wins
16 | * @see [[com.machinomy.crdt.state.LWWElementSet.Moves]] Container for removal/addition timestamps
17 | * @see Shapiro, M., Preguiça, N., Baquero, C., & Zawirski, M. (2011).
18 | * Conflict-free replicated data types.
19 | * In Proceedings of the 13th international conference on Stabilization, safety, and security of distributed systems (pp. 386–400).
20 | * Grenoble, France: Springer-Verlag.
21 | * Retrieved from [[http://dl.acm.org/citation.cfm?id=2050642]]
22 | */
23 | case class LWWElementSet[E, T: PartialOrder, B: Bias](state: Map[E, LWWElementSet.Moves[T]])(implicit tombStone: TombStone[T], movesMonoid: Monoid[Moves[T]]) extends Convergent[E, Set[E]] {
24 | type Self = LWWElementSet[E, T, B]
25 |
26 | /** Add `element` to the set.
27 | *
28 | * @return Updated LWWElementSet
29 | */
30 | def +(element: E): Self = {
31 | val stone = tombStone.next
32 | this + (element, stone)
33 | }
34 |
35 | /** Add `element` to the set stamped by `stone`.
36 | *
37 | * @return Updated LWWElementSet
38 | */
39 | def +(element: E, stone: T): Self = {
40 | val moves = state.getOrElse(element, movesMonoid.empty).copy(addition = Some(stone))
41 | new LWWElementSet(state.updated(element, moves))
42 | }
43 |
44 | /** Add `pair._1` to the set stamped by `pair._2`.
45 | *
46 | * @return Updated LWWElementSet
47 | */
48 | def +(pair: (E, T)): Self = this + (pair._1, pair._2)
49 |
50 | /** Remove `element` from the set.
51 | *
52 | * @return Updated LWWElementSet
53 | */
54 | def -(element: E): Self = {
55 | val stone = tombStone.next
56 | this - (element, stone)
57 | }
58 |
59 | /** Remove `element` from the set. Removal is stamped by `stone`.
60 | *
61 | * @return Updated LWWElementSet
62 | */
63 | def -(element: E, stone: T): Self = {
64 | val moves = state.getOrElse(element, movesMonoid.empty).copy(removal = Some(stone))
65 | new LWWElementSet(state.updated(element, moves))
66 | }
67 |
68 | /** Remove `pair._1` from the set. Removal is stamped by `pair._2`.
69 | *
70 | * @return Updated LWWElementSet
71 | */
72 | def -(pair: (E, T)): Self = this - (pair._1, pair._2)
73 |
74 | /** @return Value of the set.
75 | */
76 | override def value: Set[E] = state.foldLeft(Set.empty[E]) { case (memo, (element, moves)) =>
77 | elementByMoves(element, moves) match {
78 | case Some(e) => memo + e
79 | case None => memo
80 | }
81 | }
82 |
83 | /** Decide if element is worth keeping in a `value` set.
84 | */
85 | protected def elementByMoves(element: E, moves: Moves[T])(implicit bias: Bias[B]): Option[E] =
86 | moves match {
87 | case Moves(Some(addition), Some(removal)) =>
88 | bias.apply(element, addition, removal)
89 | case Moves(Some(addition), None) =>
90 | Some(element)
91 | case _ =>
92 | None
93 | }
94 | }
95 |
96 | object LWWElementSet {
97 |
98 | /** Record addition and removal tags for [[ORSet]].
99 | *
100 | * @tparam T Addition and removal tag
101 | * @see [[com.machinomy.crdt.state.LWWElementSet.Moves.monoid]] Behaves like a [[cats.Monoid]]
102 | * @see [[com.machinomy.crdt.state.LWWElementSet.Moves.partialOrder]] Behaves like a [[cats.PartialOrder]]
103 | */
104 | case class Moves[T](addition: Option[T] = None, removal: Option[T] = None)
105 |
106 | object Moves {
107 | /** Implements [[cats.Monoid]] type class for [[Moves]].
108 | *
109 | * @tparam T Unique tag
110 | */
111 | implicit def monoid[T](implicit order: PartialOrder[T]) = new Monoid[Moves[T]] {
112 | override def empty: Moves[T] = new Moves[T](None, None)
113 |
114 | override def combine(x: Moves[T], y: Moves[T]): Moves[T] = {
115 | def next(optA: Option[T], optB: Option[T]) = (optA, optB) match {
116 | case (Some(a), Some(b)) if order.gteqv(a, b) => Some(a)
117 | case (Some(a), Some(b)) => Some(b)
118 | case (Some(a), None) => Some(a)
119 | case (None, Some(b)) => Some(b)
120 | case _ => None
121 | }
122 |
123 | val addition = next(x.addition, y.addition)
124 | val removal = next(x.removal, y.removal)
125 | new Moves(addition, removal)
126 | }
127 | }
128 |
129 | /** Implements [[cats.PartialOrder]] type class for [[Moves]].
130 | *
131 | * @tparam T Unique tag
132 | */
133 | implicit def partialOrder[T](implicit order: PartialOrder[T]) = PartialOrder.byLteqv[Moves[T]] { (x, y) =>
134 | def lteqv(optA: Option[T], optB: Option[T]): Boolean = (optA, optB) match {
135 | case (Some(a), Some(b)) if order.lteqv(a, b) => true
136 | case (Some(a), None) => true
137 | case (None, Some(b)) => false
138 | case (_, _) => true
139 | }
140 | lteqv(x.addition, y.addition) && lteqv(x.removal, y.removal)
141 | }
142 | }
143 |
144 | /** Implements [[cats.Monoid]] type class for [[LWWElementSet]].
145 | *
146 | * @tparam E Contained element
147 | * @tparam T Unique tag
148 | * @tparam B Bias
149 | */
150 | implicit def monoid[E, T: PartialOrder, B: Bias](implicit tombStone: TombStone[T]) = new Monoid[LWWElementSet[E, T, B]] {
151 | override def empty: LWWElementSet[E, T, B] = new LWWElementSet(Map.empty[E, LWWElementSet.Moves[T]])
152 | override def combine(x: LWWElementSet[E, T, B], y: LWWElementSet[E, T, B]): LWWElementSet[E, T, B] = {
153 | val elements = x.state.keySet ++ y.state.keySet
154 | val state = elements.foldLeft(Map.empty[E, Moves[T]]) { case (memo, element) =>
155 | val xMoves = x.state.getOrElse(element, Moves[T]())
156 | val yMoves = y.state.getOrElse(element, Moves[T]())
157 | val moves = xMoves |+| yMoves
158 | memo.updated(element, moves)
159 | }
160 | new LWWElementSet(state)
161 | }
162 | }
163 |
164 | /** Implements [[cats.PartialOrder]] type class for [[LWWElementSet]].
165 | *
166 | * @tparam E Contained element
167 | * @tparam T Unique tag
168 | * @tparam B Bias
169 | */
170 | implicit def partialOrder[E, T, B: Bias](implicit partialOrder: PartialOrder[Moves[T]], movesMonoid: Monoid[Moves[T]]) = PartialOrder.byLteqv[LWWElementSet[E, T, B]] { (x, y) =>
171 | x.state.keySet.forall { (element) =>
172 | val xMoves = x.state.getOrElse(element, Monoid[Moves[T]].empty)
173 | val yMoves = y.state.getOrElse(element, Monoid[Moves[T]].empty)
174 | partialOrder.lteqv(xMoves, yMoves)
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CRDT
2 |
3 | Comprehensive collection of data types for eventually consistent systems.
4 |
5 | ## Installation
6 |
7 | In your `build.sbt` add
8 |
9 | ```scala
10 | resolvers += "Machinomy" at "http://artifactory.machinomy.com/artifactory/release"
11 |
12 | libraryDependencies += "com.machinomy" %% "crdt" % "0.0.3"
13 | ```
14 |
15 | or if you like to be on a bleeding edge:
16 |
17 | ```scala
18 | resolvers += "Machinomy" at "http://artifactory.machinomy.com/artifactory/snapshot"
19 |
20 | libraryDependencies += "com.machinomy" %% "crdt" % "0.0.4-SNAPSHOT"
21 | ```
22 |
23 | ## Usage
24 |
25 | Eventually consistent system comprises of machines, that work together. They have to maintain a shared global state.
26 | CAP theorem [[GILBERT2002](#GILBERT2002)] limits properties of the state could be supported. Some applications permit to loosen Consistency
27 | in favour of Availability and Partitioning. This leads to eventually consistent systems.
28 |
29 | Conflict-free Replicated Data Type is a data structure designed to support eventual consistency [[SHAPIRO2011](#SHAPIRO2011)]. A machine that belongs to the system maintains a local replica of the global state.
30 | Properties of CRDT guarantee the replicas converge to a common state. That makes the data structure
31 | support simultaneous operations sustainable to network disturbancy.
32 |
33 | CRDTs could be of two types:
34 |
35 | - operation-based, or op-based for short,
36 | - state-based.
37 |
38 | The types can be emulated on top of each other. The difference is in payload the replicas send to each other.
39 | As the name implies, it is an operation, like `add` or `remove`, or full state.
40 |
41 | ### State-based CRDT
42 |
43 | State-based CRDT is a data structure that supports operation `combine`, or `join` for replicas so that:
44 |
45 | * `a combine (b combine c) == (a combine b) combine c`,
46 | * `a combine b == b combine a`,
47 | * `a combine a == a`.
48 |
49 | Data structure like this is a join-semilattice. We could derive a partial order on the replicas. We could say if `a ≤ b`. This effectively means state-based CRDTs converge to some value, the least upper bound. It gives another name then: Convergent Replicated Data Type, or CvRDT.
50 |
51 | 
52 |
53 | `combine` operation resolves any conflicts that happen between the replicas by following a formal rule. The rule differs among the types. A developer is responsible for choosing the right data structure for her need.
54 |
55 | #### G-Counter
56 |
57 | Short for grow-only counter. It could be incremented only. The combine takes the maximum count for each replica. Value is the sum of all replicas.
58 |
59 | Say, replica id is `Int`, and GCounter manages `Int` replica counters as well:
60 |
61 | ```scala
62 | import com.machinomy.crdt.state._
63 | import cats.syntax.all._
64 | import cats._
65 |
66 | val counter = Monoid[GCounter[Int, Int]].empty // empty G-Counter
67 | val firstReplica = counter + (1 -> 1) // increment replica 1
68 | val secondReplica = counter + (2 -> 2) // increment replica 2
69 | val firstReplicacombined = firstReplica |+| secondReplica // combine
70 | val secondReplicacombined = secondReplica |+| firstReplica // combine
71 |
72 | firstReplicacombined == secondReplicacombined // the result is independent of combine order
73 | ```
74 |
75 | #### PN-Counter
76 |
77 | A counter that could be increased, and decreased. Effectively consists of two G-Counters: for increases, and decreases. Value is a sum of all increases minus
78 | all the decreases.
79 |
80 | ```
81 | import com.machinomy.crdt.state._
82 | import cats.syntax.all._
83 | import cats._
84 |
85 | val counter = Monoid[PNCounter[Int, Int]].empty // fresh PN-Counter
86 | val firstReplica = counter + (1 -> 1) // increment replica 1
87 | val secondReplica = counter + (2 -> -2) // decrement replica 2
88 | val firstReplicacombined = firstReplica |+| secondReplica // combine
89 | val secondReplicacombined = secondReplica |+| firstReplica // combine
90 | firstReplicacombined == secondReplicacombined // the result is independent of combine order
91 | firstReplicacombined.value == -1
92 | ```
93 |
94 | #### G-Set
95 |
96 | Short for grow-only set. Supports only addition of an element. `combine` operation is essentially a set union,
97 | which is commutative and convergent.
98 |
99 | ```scala
100 | import com.machinomy.crdt.state._
101 | import cats.syntax.all._
102 | import cats._
103 |
104 | val counter = Monoid[GSet[Int]].empty // empty G-Set
105 | val firstReplica = counter + 1 // add element
106 | val secondReplica = counter + 2 // add element
107 | val firstReplicacombined = firstReplica |+| secondReplica // combine
108 | val secondReplicacombined = secondReplica |+| firstReplica // combine
109 |
110 | firstReplicacombined == secondReplicacombined // the result is independent of combine order
111 | ```
112 |
113 | #### GT-Set
114 |
115 | Grow-only set that also tracks time of addition. `combine` operation is effectively a set union, that takes maximum of timestamps.
116 |
117 | ```scala
118 | import com.github.nscala_time.time.Imports._
119 | import cats._
120 | import cats.syntax.all._
121 | import com.machinomy.crdt.state._
122 |
123 | val set1 = Monoid[GTSet[Int, DateTime]].empty + (1 -> DateTime.now) + (2 -> (DateTime.now + 3.seconds))
124 | val set2 = Monoid[GTSet[Int, DateTime]].empty + (1 -> (DateTime.now + 1.seconds)) + (3 -> (DateTime.now + 3.seconds))
125 | val left = set1 |+| set2
126 | val right = set2 |+| set1
127 |
128 | left == right
129 | left.value == right.value
130 | ```
131 |
132 | #### MC-Set
133 |
134 | Max-Change Set assigns each element an integer. It tracks additions and deletions of the element. Odd means the element is present.
135 | Even means absence of the element. Any update results in the number increment. Addition is allowed to only increment even numbers.
136 | Removal is allowed to only increment odd numbers. That is, one can not add already present element, or remove the absent one.
137 | When combining the element with maximum changes is preferred.
138 |
139 | ```scala
140 | import com.machinomy.crdt.state._
141 | import cats._
142 | import cats.syntax.all._
143 |
144 | val a = Monoid[MCSet[Int, Int]].empty + 1 + 2
145 | val b = Monoid[MCSet[Int, Int]].empty + 1 + 3
146 | val c = Monoid[MCSet[Int, Int]].empty + 2 + 3
147 | val d = a - 2
148 |
149 | (d |+| c).value == b.value
150 | ```
151 |
152 | #### OR-Set
153 |
154 | Observed-Removed Set assigns each element _addition_ a unique tag, and stores the tags in `additions` set per element. Removal of the element
155 | adds the tags observed in `additions` set to `removals` set. `Combine` operation results in a union of `additions` and `removals` sets respectively per element.
156 | Element is present if it is added more times than removed. Thus, addition have precedence over removal.
157 |
158 | ```scala
159 | import java.util.UUID
160 | import com.machinomy.crdt.state._
161 | import cats.syntax.all._
162 | import cats._
163 |
164 | val a = Monoid[ORSet[Int, UUID]].empty + 3
165 | a.value == Set(3)
166 | val b = a - 3
167 | b.value == Set.empty
168 | (a |+| b).value == Set.empty
169 | ```
170 |
171 | #### TP-Set
172 |
173 | 2P-Set, or two-phase set. Contains one G-Set for additions, and one for removals.
174 | Removing of an element is allowed, only if it is present in the set of additions.
175 | `Combine` operation combines additions and removals as a GSet.
176 |
177 | ```scala
178 | import com.machinomy.crdt.state._
179 | import cats._
180 | import cats.syntax.all._
181 |
182 | val a = Monoid[TPSet[Int]].empty + 1 + 2
183 | val b = a - 2
184 | val c = b - 1
185 | val d = c + 2
186 | (a |+| c).value.isEmpty
187 | (a |+| c |+| d).value.isEmpty
188 | ```
189 |
190 | #### LWW-Element-Set
191 |
192 | Last-Writer-Wins Set attaches a timestamp to an element addition or removal.
193 | An element is present iff addition timestamp is bigger than that of removal.
194 | Concurrent addition and removal is resolved using `Bias`: either addition or removal always wins.
195 |
196 | ```scala
197 | import com.machinomy.crdt.state._
198 | import cats._
199 | import cats.syntax.all._
200 | import com.github.nscala_time.time.Imports._
201 |
202 | val now = DateTime.now
203 | val a = Monoid[LWWElementSet[Int, DateTime, Bias.RemovalWins]].empty + 1 + (2, now) + (3, now)
204 | val b = Monoid[LWWElementSet[Int, DateTime, Bias.RemovalWins]].empty - (2, now) - (3, now) - (4, now)
205 | val resultRemoval = a |+| b
206 | resultRemoval.value == Set(1)
207 |
208 | val c = Monoid[LWWElementSet[Int, DateTime, Bias.AdditionWins]].empty + 1 + (2, now) + (3, now)
209 | val d = Monoid[LWWElementSet[Int, DateTime, Bias.AdditionWins]].empty - (2, now) - (3, now) - (4, now)
210 | val resultAddition = c |+| d
211 | resultAddition.value == Set(1, 2, 3)
212 | ```
213 |
214 | ### Operation-based CRDT
215 |
216 | TODO
217 |
218 | #### Counter
219 |
220 | TODO
221 |
222 | #### OR-Set
223 |
224 | TODO
225 |
226 | #### 2P2P-Graph
227 |
228 | TODO
229 |
230 | #### Add-only Monotonic DAG
231 |
232 | TODO
233 |
234 | #### Partial Order Graph
235 |
236 | TODO
237 |
238 | ## To Do
239 |
240 | TODO
241 |
242 | ## License
243 |
244 | This code is open source software licensed under the [Mozilla Public License v2.0](http://mozilla.org/MPL/2.0).
245 |
246 | ## References
247 |
248 | * [GILBERT2002] [Brewer's conjecture and the feasibility of consistent, available, partition-tolerant web services](http://dl.acm.org/citation.cfm?id=564601)
249 | * [SHAPIRO2011] [A comprehensive study of Convergent and Commutative Replicated Data Types](https://hal.inria.fr/inria-00555588/en/)
250 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2016 Machinomy
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------