Merge pull request #22508 from gosubpl/wip/22350-delta-crdt-orset-friends
ORMap and friends are delta-CRDTs (#22350)
This commit is contained in:
commit
edee6ae544
15 changed files with 4371 additions and 140 deletions
|
|
@ -47,6 +47,27 @@ class LWWMapSpec extends WordSpec with Matchers {
|
|||
(m3 merge m4).entries should be(Map("a" → 1, "b" → 22, "c" → 3))
|
||||
}
|
||||
|
||||
"be able to work with deltas" in {
|
||||
val m1 = LWWMap.empty.put(node1, "a", 1, defaultClock[Int]).put(node1, "b", 2, defaultClock[Int])
|
||||
val m2 = LWWMap.empty.put(node2, "c", 3, defaultClock[Int])
|
||||
|
||||
val expected = Map("a" → 1, "b" → 2, "c" → 3)
|
||||
(m1 merge m2).entries should be(expected)
|
||||
(m2 merge m1).entries should be(expected)
|
||||
|
||||
LWWMap.empty.mergeDelta(m1.delta.get).mergeDelta(m2.delta.get).entries should be(expected)
|
||||
LWWMap.empty.mergeDelta(m2.delta.get).mergeDelta(m1.delta.get).entries should be(expected)
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
(merged1 mergeDelta m3.delta.get).entries should be(Map("a" → 1, "c" → 3))
|
||||
|
||||
// but if there is a conflicting update the entry is not removed
|
||||
val m4 = merged1.resetDelta.put(node2, "b", 22, defaultClock[Int])
|
||||
(m3 mergeDelta m4.delta.get).entries should be(Map("a" → 1, "b" → 22, "c" → 3))
|
||||
}
|
||||
|
||||
"have unapply extractor" in {
|
||||
val m1 = LWWMap.empty.put(node1, "a", 1L, defaultClock[Long])
|
||||
val LWWMap(entries1) = m1
|
||||
|
|
|
|||
|
|
@ -30,12 +30,43 @@ class ORMapSpec extends WordSpec with Matchers {
|
|||
|
||||
}
|
||||
|
||||
"be able to add entries with deltas" in {
|
||||
val m = ORMap().put(node1, "a", GSet() + "A").put(node1, "b", GSet() + "B")
|
||||
val md = m.delta.get
|
||||
|
||||
val m1 = ORMap().mergeDelta(md)
|
||||
|
||||
val GSet(a) = m1.entries("a")
|
||||
a should be(Set("A"))
|
||||
val GSet(b) = m1.entries("b")
|
||||
b should be(Set("B"))
|
||||
|
||||
val m2 = m1.put(node1, "a", GSet() + "C")
|
||||
val GSet(a2) = m2.entries("a")
|
||||
a2 should be(Set("C"))
|
||||
|
||||
}
|
||||
|
||||
"be able to remove entry" in {
|
||||
val m = ORMap().put(node1, "a", GSet() + "A").put(node1, "b", GSet() + "B").remove(node1, "a")
|
||||
m.entries.keySet should not contain ("a")
|
||||
m.entries.keySet should contain("b")
|
||||
}
|
||||
|
||||
"be able to remove entry using a delta" in {
|
||||
val m = ORMap().put(node1, "a", GSet() + "A").put(node1, "b", GSet() + "B")
|
||||
val addDelta = m.delta.get
|
||||
|
||||
val removeDelta = m.resetDelta.remove(node1, "a").delta.get
|
||||
|
||||
val m1 = ORMap().mergeDelta(addDelta)
|
||||
m1.entries.keySet should contain("a")
|
||||
|
||||
val m2 = m1.mergeDelta(removeDelta)
|
||||
m2.entries.keySet should not contain ("a")
|
||||
m2.entries.keySet should contain("b")
|
||||
}
|
||||
|
||||
"be able to add removed" in {
|
||||
val m = ORMap().put(node1, "a", GSet() + "A").put(node1, "b", GSet() + "B").remove(node1, "a")
|
||||
m.entries.keySet should not contain ("a")
|
||||
|
|
@ -110,6 +141,330 @@ class ORMapSpec extends WordSpec with Matchers {
|
|||
merged2.entries("c").elements should be(Set("C"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", GSet.empty + "A").put(node1, "b", GSet.empty + "B")
|
||||
val m2 = ORMap.empty.put(node2, "c", GSet.empty + "C")
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
val m4 = merged1.resetDelta.updated(node1, "b", GSet.empty[String])(_.add("B2"))
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a").elements should be(Set("A"))
|
||||
// note that B is included, because GSet("B") is merged with GSet("B2")
|
||||
merged2.entries("b").elements should be(Set("B", "B2"))
|
||||
merged2.entries("c").elements should be(Set("C"))
|
||||
|
||||
val merged3 = m3 mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
// note that B is included, because GSet("B") is merged with GSet("B2")
|
||||
merged3.entries("b").elements should be(Set("B", "B2"))
|
||||
merged3.entries("c").elements should be(Set("C"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas 2" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", ORSet.empty.add(node1, "A")).put(node1, "b", ORSet.empty.add(node1, "B"))
|
||||
val m2 = ORMap.empty.put(node2, "c", ORSet.empty.add(node2, "C"))
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
val m4 = merged1.resetDelta.remove(node1, "b").updated(node1, "b", ORSet.empty[String])(_.add(node1, "B2"))
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a").elements should be(Set("A"))
|
||||
// note that B is not included, because it was removed in both timelines
|
||||
merged2.entries("b").elements should be(Set("B2"))
|
||||
merged2.entries("c").elements should be(Set("C"))
|
||||
|
||||
val merged3 = m3 mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
// note that B is not included, because it was removed in both timelines
|
||||
merged3.entries("b").elements should be(Set("B2"))
|
||||
merged3.entries("c").elements should be(Set("C"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas 3" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", ORSet.empty.add(node1, "A")).put(node1, "b", ORSet.empty.add(node1, "B"))
|
||||
val m2 = ORMap.empty.put(node2, "c", ORSet.empty.add(node2, "C"))
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
val m4 = merged1.resetDelta.remove(node2, "b").updated(node2, "b", ORSet.empty[String])(_.add(node2, "B2"))
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a").elements should be(Set("A"))
|
||||
// note that B is not included, because it was removed in both timelines
|
||||
merged2.entries("b").elements should be(Set("B2"))
|
||||
merged2.entries("c").elements should be(Set("C"))
|
||||
|
||||
val merged3 = m3 mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
// note that B is not included, because it was removed in both timelines
|
||||
merged3.entries("b").elements should be(Set("B2"))
|
||||
merged3.entries("c").elements should be(Set("C"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas 4" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", ORSet.empty.add(node1, "A")).put(node1, "b", ORSet.empty.add(node1, "B"))
|
||||
val m2 = ORMap.empty.put(node2, "c", ORSet.empty.add(node2, "C"))
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
val m4 = merged1.resetDelta.updated(node1, "b", ORSet.empty[String])(_.add(node1, "B2"))
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a").elements should be(Set("A"))
|
||||
// note that B is included, because ORSet("B") is merged with ORSet("B2")
|
||||
merged2.entries("b").elements should be(Set("B", "B2"))
|
||||
merged2.entries("c").elements should be(Set("C"))
|
||||
|
||||
val merged3 = m3 mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
// note that B is included, because ORSet("B") is merged with ORSet("B2")
|
||||
merged3.entries("b").elements should be(Set("B", "B2"))
|
||||
merged3.entries("c").elements should be(Set("C"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas 5" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", GSet.empty + "A").put(node1, "b", GSet.empty + "B")
|
||||
val m2 = ORMap.empty.put(node2, "c", GSet.empty + "C")
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
val m4 = merged1.resetDelta.put(node2, "b", GSet.empty + "B2")
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a").elements should be(Set("A"))
|
||||
// note that B is not included, because it was removed in both timelines
|
||||
merged2.entries("b").elements should be(Set("B2"))
|
||||
merged2.entries("c").elements should be(Set("C"))
|
||||
|
||||
val merged3 = m3 mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
// note that B is not included, because it was removed in both timelines
|
||||
merged3.entries("b").elements should be(Set("B2"))
|
||||
merged3.entries("c").elements should be(Set("C"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas 6" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", ORSet.empty.add(node1, "A")).put(node1, "b", ORSet.empty.add(node1, "B"))
|
||||
val m2 = ORMap.empty.put(node2, "b", ORSet.empty.add(node2, "B3"))
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
val m4 = merged1.resetDelta.remove(node2, "b").updated(node2, "b", ORSet.empty[String])(_.add(node2, "B1"))
|
||||
.updated(node2, "b", ORSet.empty[String])(_.add(node2, "B2"))
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a").elements should be(Set("A"))
|
||||
// note that B is not included, because it was removed in both timelines
|
||||
merged2.entries("b").elements should be(Set("B1", "B2"))
|
||||
|
||||
val merged3 = m3 mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
// note that B is not included, because it was removed in both timelines
|
||||
merged3.entries("b").elements should be(Set("B1", "B2"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas 7" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", ORSet.empty.add(node1, "A"))
|
||||
.put(node1, "b", ORSet.empty.add(node1, "B1")).remove(node1, "b")
|
||||
val m2 = ORMap.empty.put(node1, "a", ORSet.empty.add(node1, "A")).put(node1, "b", ORSet.empty.add(node1, "B2"))
|
||||
val m2d = m2.resetDelta.remove(node1, "b")
|
||||
val m2u = m2.resetDelta.updated(node1, "b", ORSet.empty[String])(_.add(node1, "B3"))
|
||||
.updated(node2, "b", ORSet.empty[String])(_.add(node2, "B4"))
|
||||
|
||||
val merged1 = (m1 merge m2d) mergeDelta m2u.delta.get
|
||||
|
||||
merged1.entries("a").elements should be(Set("A"))
|
||||
// note that B1 is lost as it was added and removed earlier in timeline than B2
|
||||
merged1.entries("b").elements should be(Set("B2", "B3", "B4"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas 8" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", GSet.empty + "A")
|
||||
.put(node1, "b", GSet.empty + "B").put(node2, "b", GSet.empty + "B")
|
||||
val m2 = ORMap.empty.put(node2, "c", GSet.empty + "C")
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b").remove(node2, "b")
|
||||
val m4 = merged1.resetDelta.put(node2, "b", GSet.empty + "B2").put(node2, "b", GSet.empty + "B3")
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a").elements should be(Set("A"))
|
||||
merged2.entries("b").elements should be(Set("B3"))
|
||||
merged2.entries("c").elements should be(Set("C"))
|
||||
|
||||
val merged3 = (merged1 mergeDelta m3.delta.get) mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
merged3.entries("b").elements should be(Set("B3"))
|
||||
merged3.entries("c").elements should be(Set("C"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas 9" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", GSet.empty + "A")
|
||||
.put(node1, "b", GSet.empty + "B").put(node2, "b", GSet.empty + "B")
|
||||
val m2 = ORMap.empty.put(node2, "c", GSet.empty + "C")
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b").remove(node2, "b")
|
||||
val m4 = merged1.resetDelta.updated(node2, "b", GSet.empty[String])(_.add("B2"))
|
||||
.updated(node2, "b", GSet.empty[String])(_.add("B3"))
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a").elements should be(Set("A"))
|
||||
merged2.entries("b").elements should be(Set("B2", "B3"))
|
||||
merged2.entries("c").elements should be(Set("C"))
|
||||
|
||||
val merged3 = (merged1 mergeDelta m3.delta.get) mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
merged3.entries("b").elements should be(Set("B2", "B3"))
|
||||
merged3.entries("c").elements should be(Set("C"))
|
||||
}
|
||||
|
||||
"not have anomalies for remove+updated scenario and deltas 10" in {
|
||||
val m1 = ORMap.empty.put(node2, "a", GSet.empty + "A")
|
||||
.put(node2, "b", GSet.empty + "B")
|
||||
|
||||
val m3 = m1.resetDelta.remove(node2, "b")
|
||||
val m4 = m3.resetDelta.put(node2, "b", GSet.empty + "B2").updated(node2, "b", GSet.empty[String])(_.add("B3"))
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a").elements should be(Set("A"))
|
||||
merged2.entries("b").elements should be(Set("B2", "B3"))
|
||||
|
||||
val merged3 = m3 mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
merged3.entries("b").elements should be(Set("B2", "B3"))
|
||||
}
|
||||
|
||||
"have the usual anomalies for remove+updated scenario" in {
|
||||
// please note that the current ORMultiMap has the same anomaly
|
||||
// because the condition of keeping global vvector is violated
|
||||
// by removal of the whole entry for the removed key "b" which results in removal of it's value's vvector
|
||||
val m1 = ORMap.empty.put(node1, "a", ORSet.empty.add(node1, "A")).put(node1, "b", ORSet.empty.add(node1, "B"))
|
||||
val m2 = ORMap.empty.put(node2, "c", ORSet.empty.add(node2, "C"))
|
||||
|
||||
// m1 - node1 gets the update from m2
|
||||
val merged1 = m1 merge m2
|
||||
// m2 - node2 gets the update from m1
|
||||
val merged2 = m2 merge m1
|
||||
|
||||
// RACE CONDITION ahead!
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
// let's imagine that m3 (node1) update gets propagated here (full state or delta - doesn't matter)
|
||||
// and is in flight, but in the meantime, an element is being added somewhere else (m4 - node2)
|
||||
// and the update is propagated before the update from node1 is merged
|
||||
val m4 = merged2.resetDelta.updated(node2, "b", ORSet.empty[String])(_.add(node2, "B2"))
|
||||
// and later merged on node1
|
||||
val merged3 = m3 merge m4
|
||||
// and the other way round...
|
||||
val merged4 = m4 merge m3
|
||||
|
||||
// result - the element "B" is kept on both sides...
|
||||
merged3.entries("a").elements should be(Set("A"))
|
||||
merged3.entries("b").elements should be(Set("B", "B2"))
|
||||
merged3.entries("c").elements should be(Set("C"))
|
||||
|
||||
merged4.entries("a").elements should be(Set("A"))
|
||||
merged4.entries("b").elements should be(Set("B", "B2"))
|
||||
merged4.entries("c").elements should be(Set("C"))
|
||||
|
||||
// but if the timing was slightly different, so that the update from node1
|
||||
// would get merged just before update on node2:
|
||||
val merged5 = (m2 merge m3).resetDelta.updated(node2, "b", ORSet.empty[String])(_.add(node2, "B2"))
|
||||
// the update propagated ... and merged on node1:
|
||||
val merged6 = m3 merge merged5
|
||||
|
||||
// then the outcome is different... because the vvector of value("b") was lost...
|
||||
merged5.entries("a").elements should be(Set("A"))
|
||||
// this time it's different...
|
||||
merged5.entries("b").elements should be(Set("B2"))
|
||||
merged5.entries("c").elements should be(Set("C"))
|
||||
|
||||
merged6.entries("a").elements should be(Set("A"))
|
||||
// this time it's different...
|
||||
merged6.entries("b").elements should be(Set("B2"))
|
||||
merged6.entries("c").elements should be(Set("C"))
|
||||
}
|
||||
|
||||
"work with deltas and updated for GSet elements type" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", GSet.empty + "A")
|
||||
val m2 = m1.resetDelta.updated(node1, "a", GSet.empty[String])(_.add("B"))
|
||||
val m3 = ORMap().mergeDelta(m1.delta.get).mergeDelta(m2.delta.get)
|
||||
val GSet(d3) = m3.entries("a")
|
||||
d3 should be(Set("A", "B"))
|
||||
}
|
||||
|
||||
"work with deltas and updated for ORSet elements type" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", ORSet.empty.add(node1, "A"))
|
||||
val m2 = m1.resetDelta.updated(node1, "a", ORSet.empty[String])(_.add(node1, "B"))
|
||||
val m3 = ORMap().mergeDelta(m1.delta.get).mergeDelta(m2.delta.get)
|
||||
|
||||
val ORSet(d3) = m3.entries("a")
|
||||
d3 should be(Set("A", "B"))
|
||||
}
|
||||
|
||||
"work with aggregated deltas and updated for GSet elements type" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", GSet.empty + "A")
|
||||
val m2 = m1.resetDelta.updated(node1, "a", GSet.empty[String])(_.add("B")).updated(node1, "a", GSet.empty[String])(_.add("C"))
|
||||
val m3 = ORMap().mergeDelta(m1.delta.get).mergeDelta(m2.delta.get)
|
||||
val GSet(d3) = m3.entries("a")
|
||||
d3 should be(Set("A", "B", "C"))
|
||||
}
|
||||
|
||||
"work with deltas and updated for GCounter elements type" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", GCounter.empty)
|
||||
val m2 = m1.resetDelta.updated(node1, "a", GCounter.empty)(_.increment(node1, 10))
|
||||
val m3 = m2.resetDelta.updated(node2, "a", GCounter.empty)(_.increment(node2, 10))
|
||||
val m4 = ORMap().mergeDelta(m1.delta.get).mergeDelta(m2.delta.get).mergeDelta(m3.delta.get)
|
||||
val GCounter(num) = m4.entries("a")
|
||||
num should ===(20)
|
||||
}
|
||||
|
||||
"work with deltas and updated for PNCounter elements type" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", PNCounter.empty)
|
||||
val m2 = m1.resetDelta.updated(node1, "a", PNCounter.empty)(_.increment(node1, 10))
|
||||
val m3 = m2.resetDelta.updated(node2, "a", PNCounter.empty)(_.decrement(node2, 10))
|
||||
val m4 = ORMap().mergeDelta(m1.delta.get).mergeDelta(m2.delta.get).mergeDelta(m3.delta.get)
|
||||
val PNCounter(num) = m4.entries("a")
|
||||
num should ===(0)
|
||||
}
|
||||
|
||||
"work with deltas and updated for Flag elements type" in {
|
||||
val m1 = ORMap.empty.put(node1, "a", Flag(false))
|
||||
val m2 = m1.resetDelta.updated(node1, "a", Flag.empty)(_.switchOn)
|
||||
val m3 = ORMap().mergeDelta(m1.delta.get).mergeDelta(m2.delta.get)
|
||||
val Flag(d3) = m3.entries("a")
|
||||
d3 should be(true)
|
||||
}
|
||||
|
||||
"not allow put for ORSet elements type" in {
|
||||
val m = ORMap().put(node1, "a", ORSet().add(node1, "A"))
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,12 @@ class ORMultiMapSpec extends WordSpec with Matchers {
|
|||
|
||||
val merged2 = m2 merge m1
|
||||
merged2.entries should be(expectedMerged)
|
||||
|
||||
val merged3 = m1 mergeDelta m2.delta.get
|
||||
merged3.entries should be(expectedMerged)
|
||||
|
||||
val merged4 = m2 mergeDelta m1.delta.get
|
||||
merged4.entries should be(expectedMerged)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +113,74 @@ class ORMultiMapSpec extends WordSpec with Matchers {
|
|||
m2.entries should be(Map("b" → Set("B1")))
|
||||
}
|
||||
|
||||
"not have usual anomalies for remove+addBinding scenario and delta-deltas" in {
|
||||
val m1 = ORMultiMap.emptyWithValueDeltas[String, String].put(node1, "a", Set("A")).put(node1, "b", Set("B"))
|
||||
val m2 = ORMultiMap.emptyWithValueDeltas[String, String].put(node2, "c", Set("C"))
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
val m4 = merged1.resetDelta.addBinding(node1, "b", "B2")
|
||||
|
||||
val merged2 = m3 merge m4
|
||||
|
||||
merged2.entries("a") should be(Set("A"))
|
||||
merged2.entries("b") should be(Set("B2"))
|
||||
merged2.entries("c") should be(Set("C"))
|
||||
|
||||
val merged3 = m3 mergeDelta m4.delta.get
|
||||
|
||||
merged3.entries("a") should be(Set("A"))
|
||||
merged3.entries("b") should be(Set("B2"))
|
||||
merged3.entries("c") should be(Set("C"))
|
||||
}
|
||||
|
||||
"not have usual anomalies for remove+addBinding scenario and delta-deltas 2" in {
|
||||
// the new delta-delta ORMultiMap is free from this anomaly
|
||||
val m1 = ORMultiMap.emptyWithValueDeltas[String, String].put(node1, "a", Set("A")).put(node1, "b", Set("B"))
|
||||
val m2 = ORMultiMap.emptyWithValueDeltas[String, String].put(node2, "c", Set("C"))
|
||||
|
||||
// m1 - node1 gets the update from m2
|
||||
val merged1 = m1 merge m2
|
||||
// m2 - node2 gets the update from m1
|
||||
val merged2 = m2 merge m1
|
||||
|
||||
// no race condition
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
// let's imagine that m3 (node1) update gets propagated here (full state or delta - doesn't matter)
|
||||
// and is in flight, but in the meantime, an element is being added somewhere else (m4 - node2)
|
||||
// and the update is propagated before the update from node1 is merged
|
||||
val m4 = merged2.resetDelta.addBinding(node2, "b", "B2")
|
||||
// and later merged on node1
|
||||
val merged3 = m3 merge m4
|
||||
// and the other way round...
|
||||
val merged4 = m4 merge m3
|
||||
|
||||
// result - the element "B" is kept on both sides...
|
||||
merged3.entries("a") should be(Set("A"))
|
||||
merged3.entries("b") should be(Set("B2"))
|
||||
merged3.entries("c") should be(Set("C"))
|
||||
|
||||
merged4.entries("a") should be(Set("A"))
|
||||
merged4.entries("b") should be(Set("B2"))
|
||||
merged4.entries("c") should be(Set("C"))
|
||||
|
||||
// but if the timing was slightly different, so that the update from node1
|
||||
// would get merged just before update on node2:
|
||||
val merged5 = (m2 merge m3).resetDelta.addBinding(node2, "b", "B2")
|
||||
// the update propagated ... and merged on node1:
|
||||
val merged6 = m3 merge merged5
|
||||
|
||||
// then the outcome would be the same...
|
||||
merged5.entries("a") should be(Set("A"))
|
||||
merged5.entries("b") should be(Set("B2"))
|
||||
merged5.entries("c") should be(Set("C"))
|
||||
|
||||
merged6.entries("a") should be(Set("A"))
|
||||
merged6.entries("b") should be(Set("B2"))
|
||||
merged6.entries("c") should be(Set("C"))
|
||||
}
|
||||
|
||||
"have unapply extractor" in {
|
||||
val m1 = ORMultiMap.empty.put(node1, "a", Set(1L, 2L)).put(node2, "b", Set(3L))
|
||||
val m2: ORMultiMap[String, Long] = m1
|
||||
|
|
|
|||
|
|
@ -46,6 +46,24 @@ class PNCounterMapSpec extends WordSpec with Matchers {
|
|||
(m3 merge m4).entries should be(Map("a" → 1, "b" → 13, "c" → 7))
|
||||
}
|
||||
|
||||
"be able to work with deltas" in {
|
||||
val m1 = PNCounterMap().increment(node1, "a", 1).increment(node1, "b", 3).increment(node1, "c", 2)
|
||||
val m2 = PNCounterMap().increment(node2, "c", 5)
|
||||
|
||||
val expected = Map("a" → 1, "b" → 3, "c" → 7)
|
||||
(PNCounterMap() mergeDelta m1.delta.get mergeDelta m2.delta.get).entries should be(expected)
|
||||
(PNCounterMap() mergeDelta m2.delta.get mergeDelta m1.delta.get).entries should be(expected)
|
||||
|
||||
val merged1 = m1 merge m2
|
||||
|
||||
val m3 = merged1.resetDelta.remove(node1, "b")
|
||||
(merged1 mergeDelta m3.delta.get).entries should be(Map("a" → 1, "c" → 7))
|
||||
|
||||
// but if there is a conflicting update the entry is not removed
|
||||
val m4 = merged1.resetDelta.increment(node2, "b", 10)
|
||||
(m3 mergeDelta m4.delta.get).entries should be(Map("a" → 1, "b" → 13, "c" → 7))
|
||||
}
|
||||
|
||||
"have unapply extractor" in {
|
||||
val m1 = PNCounterMap.empty.increment(node1, "a", 1).increment(node2, "b", 2)
|
||||
val PNCounterMap(entries1) = m1
|
||||
|
|
|
|||
|
|
@ -184,6 +184,23 @@ class ReplicatedDataSerializerSpec extends TestKit(ActorSystem(
|
|||
checkSerialization(ORMap().put(address1, Flag(), GSet() + "A"))
|
||||
}
|
||||
|
||||
"serialize ORMap delta" in {
|
||||
checkSerialization(ORMap().put(address1, "a", GSet() + "A").put(address2, "b", GSet() + "B").delta.get)
|
||||
checkSerialization(ORMap().put(address1, "a", GSet() + "A").resetDelta.remove(address2, "a").delta.get)
|
||||
checkSerialization(ORMap().put(address1, "a", GSet() + "A").remove(address2, "a").delta.get)
|
||||
checkSerialization(ORMap().put(address1, 1, GSet() + "A").delta.get)
|
||||
checkSerialization(ORMap().put(address1, 1L, GSet() + "A").delta.get)
|
||||
checkSerialization(ORMap.empty[String, ORSet[String]]
|
||||
.put(address1, "a", ORSet.empty[String].add(address1, "A"))
|
||||
.put(address2, "b", ORSet.empty[String].add(address2, "B"))
|
||||
.updated(address1, "a", ORSet.empty[String])(_.add(address1, "C")).delta.get)
|
||||
checkSerialization(ORMap.empty[String, ORSet[String]]
|
||||
.resetDelta
|
||||
.updated(address1, "a", ORSet.empty[String])(_.add(address1, "C")).delta.get)
|
||||
// use Flag for this test as object key because it is serializable
|
||||
checkSerialization(ORMap().put(address1, Flag(), GSet() + "A").delta.get)
|
||||
}
|
||||
|
||||
"be compatible with old ORMap serialization" in {
|
||||
// Below blob was created with previous version of the serializer
|
||||
val oldBlobAsBase64 = "H4sIAAAAAAAAAOOax8jlyaXMJc8lzMWXX5KRWqSXkV9copdflC7wXEWUiYGBQRaIGQQkuJS45LiEuHiL83NTUdQwwtWIC6kQpUqVKAulGBOlGJOE+LkYE4W4uJi5GB0FuJUYnUACSRABJ7AAAOLO3C3DAAAA"
|
||||
|
|
@ -244,6 +261,22 @@ class ReplicatedDataSerializerSpec extends TestKit(ActorSystem(
|
|||
checkCompatibility(oldBlobAsBase64, ORMultiMap())
|
||||
}
|
||||
|
||||
"serialize ORMultiMap withValueDeltas" in {
|
||||
checkSerialization(ORMultiMap._emptyWithValueDeltas)
|
||||
checkSerialization(ORMultiMap._emptyWithValueDeltas.addBinding(address1, "a", "A"))
|
||||
checkSerialization(ORMultiMap._emptyWithValueDeltas.addBinding(address1, 1, "A"))
|
||||
checkSerialization(ORMultiMap._emptyWithValueDeltas.addBinding(address1, 1L, "A"))
|
||||
checkSerialization(ORMultiMap._emptyWithValueDeltas.addBinding(address1, Flag(), "A"))
|
||||
checkSerialization(ORMultiMap.emptyWithValueDeltas[String, String]
|
||||
.addBinding(address1, "a", "A1")
|
||||
.put(address2, "b", Set("B1", "B2", "B3"))
|
||||
.addBinding(address2, "a", "A2"))
|
||||
|
||||
val m1 = ORMultiMap.emptyWithValueDeltas[String, String].addBinding(address1, "a", "A1").addBinding(address2, "a", "A2")
|
||||
val m2 = ORMultiMap.emptyWithValueDeltas[String, String].put(address2, "b", Set("B1", "B2", "B3"))
|
||||
checkSameContent(m1.merge(m2), m2.merge(m1))
|
||||
}
|
||||
|
||||
"serialize DeletedData" in {
|
||||
checkSerialization(DeletedData)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue