├── 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 | ![State-based CRDT Flow](doc/images/state_based_crdt_flow.png) 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. --------------------------------------------------------------------------------