org.apache.pekko.cluster.ddata.ORSet.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pekko-distributed-data_2.12 Show documentation
Show all versions of pekko-distributed-data_2.12 Show documentation
Apache Pekko is a toolkit for building highly concurrent, distributed, and resilient message-driven applications for Java and Scala.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* license agreements; and to You under the Apache License, version 2.0:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* This file is part of the Apache Pekko project, which was derived from Akka.
*/
/*
* Copyright (C) 2009-2022 Lightbend Inc.
*/
/*
* Copyright (c) 2007-2013 Basho Technologies, Inc. All Rights Reserved.
*
* This file is provided to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.pekko.cluster.ddata
import scala.annotation.tailrec
import scala.collection.immutable
import org.apache.pekko
import pekko.annotation.InternalApi
import pekko.cluster.Cluster
import pekko.cluster.UniqueAddress
import pekko.util.{ unused, HashCode }
object ORSet {
private val _empty: ORSet[Any] = new ORSet(Map.empty, VersionVector.empty)
def empty[A]: ORSet[A] = _empty.asInstanceOf[ORSet[A]]
def apply(): ORSet[Any] = _empty
/**
* Java API
*/
def create[A](): ORSet[A] = empty[A]
/**
* Extract the [[ORSet#elements]].
*/
def unapply[A](s: ORSet[A]): Option[Set[A]] = Some(s.elements)
/**
* Extract the [[ORSet#elements]] of an `ORSet`.
*/
def unapply(a: ReplicatedData): Option[Set[Any]] = a match {
case s: ORSet[Any] @unchecked => Some(s.elements)
case _ => None
}
/**
* INTERNAL API
*/
@InternalApi private[pekko] type Dot = VersionVector
sealed trait DeltaOp extends ReplicatedDelta with RequiresCausalDeliveryOfDeltas with ReplicatedDataSerialization {
type T = DeltaOp
}
/**
* INTERNAL API
*/
@InternalApi private[pekko] sealed abstract class AtomicDeltaOp[A] extends DeltaOp with ReplicatedDeltaSize {
def underlying: ORSet[A]
override def zero: ORSet[A] = ORSet.empty
override def deltaSize: Int = 1
}
/** INTERNAL API */
@InternalApi private[pekko] final case class AddDeltaOp[A](underlying: ORSet[A]) extends AtomicDeltaOp[A] {
override def merge(that: DeltaOp): DeltaOp = that match {
case AddDeltaOp(u) =>
// Note that we only merge deltas originating from the same node
AddDeltaOp(
new ORSet(concatElementsMap(u.elementsMap.asInstanceOf[Map[A, Dot]]), underlying.vvector.merge(u.vvector)))
case _: AtomicDeltaOp[_] => DeltaGroup(Vector(this, that))
case DeltaGroup(ops) => DeltaGroup(this +: ops)
}
private def concatElementsMap(thatMap: Map[A, Dot]): Map[A, Dot] = {
if (thatMap.size == 1) {
val head = thatMap.head
underlying.elementsMap.updated(head._1, head._2)
} else
underlying.elementsMap ++ thatMap
}
}
/** INTERNAL API */
@InternalApi private[pekko] final case class RemoveDeltaOp[A](underlying: ORSet[A]) extends AtomicDeltaOp[A] {
if (underlying.size != 1)
throw new IllegalArgumentException(s"RemoveDeltaOp should contain one removed element, but was $underlying")
override def merge(that: DeltaOp): DeltaOp = that match {
case _: AtomicDeltaOp[_] => DeltaGroup(Vector(this, that)) // keep it simple for removals
case DeltaGroup(ops) => DeltaGroup(this +: ops)
}
}
/** INTERNAL API: Used for `clear` but could be used for other cases also */
@InternalApi private[pekko] final case class FullStateDeltaOp[A](underlying: ORSet[A]) extends AtomicDeltaOp[A] {
override def merge(that: DeltaOp): DeltaOp = that match {
case _: AtomicDeltaOp[_] => DeltaGroup(Vector(this, that))
case DeltaGroup(ops) => DeltaGroup(this +: ops)
}
}
/**
* INTERNAL API
*/
@InternalApi private[pekko] final case class DeltaGroup[A](ops: immutable.IndexedSeq[DeltaOp])
extends DeltaOp
with ReplicatedDeltaSize {
override def merge(that: DeltaOp): DeltaOp = that match {
case thatAdd: AddDeltaOp[_] =>
// merge AddDeltaOp into last AddDeltaOp in the group, if possible
ops.last match {
case thisAdd: AddDeltaOp[_] => DeltaGroup(ops.dropRight(1) :+ thisAdd.merge(thatAdd))
case _ => DeltaGroup(ops :+ thatAdd)
}
case DeltaGroup(thatOps) => DeltaGroup(ops ++ thatOps)
case _ => DeltaGroup(ops :+ that)
}
override def zero: ORSet[A] = ORSet.empty
override def deltaSize: Int = ops.size
}
/**
* INTERNAL API
* Subtract the `vvector` from the `dot`.
* What this means is that any (node, version) pair in
* `dot` that is <= an entry in `vvector` is removed from `dot`.
* Example [{a, 3}, {b, 2}, {d, 14}, {g, 22}] -
* [{a, 4}, {b, 1}, {c, 1}, {d, 14}, {e, 5}, {f, 2}] =
* [{b, 2}, {g, 22}]
*/
@InternalApi private[pekko] def subtractDots(dot: Dot, vvector: VersionVector): Dot = {
@tailrec def dropDots(
remaining: List[(UniqueAddress, Long)],
acc: List[(UniqueAddress, Long)]): List[(UniqueAddress, Long)] =
remaining match {
case Nil => acc
case (d @ (node, v1)) :: rest =>
val v2 = vvector.versionAt(node)
if (v2 >= v1)
// dot is dominated by version vector, drop it
dropDots(rest, acc)
else
dropDots(rest, d :: acc)
}
if (dot.isEmpty)
VersionVector.empty
else {
dot match {
case OneVersionVector(node, v1) =>
// if dot is dominated by version vector, drop it
if (vvector.versionAt(node) >= v1) VersionVector.empty
else dot
case ManyVersionVector(vs) =>
val remaining = vs.toList
val newDots = dropDots(remaining, Nil)
VersionVector(newDots)
}
}
}
/**
* INTERNAL API
* @see [[ORSet#merge]]
*/
@InternalApi private[pekko] def mergeCommonKeys[A](
commonKeys: Set[A],
lhs: ORSet[A],
rhs: ORSet[A]): Map[A, ORSet.Dot] =
mergeCommonKeys(commonKeys.iterator, lhs, rhs)
private def mergeCommonKeys[A](commonKeys: Iterator[A], lhs: ORSet[A], rhs: ORSet[A]): Map[A, ORSet.Dot] = {
commonKeys.foldLeft(Map.empty[A, ORSet.Dot]) {
case (acc, k) =>
val lhsDots = lhs.elementsMap(k)
val rhsDots = rhs.elementsMap(k)
(lhsDots, rhsDots) match {
case (OneVersionVector(n1, v1), OneVersionVector(n2, v2)) =>
if (n1 == n2 && v1 == v2)
// one single common dot
acc.updated(k, lhsDots)
else {
// no common, lhsUniqueDots == lhsDots, rhsUniqueDots == rhsDots
val lhsKeep = ORSet.subtractDots(lhsDots, rhs.vvector)
val rhsKeep = ORSet.subtractDots(rhsDots, lhs.vvector)
val merged = lhsKeep.merge(rhsKeep)
// Perfectly possible that an item in both sets should be dropped
if (merged.isEmpty) acc
else acc.updated(k, merged)
}
case (ManyVersionVector(lhsVs), ManyVersionVector(rhsVs)) =>
val commonDots = lhsVs.filter {
case (thisDotNode, v) => rhsVs.get(thisDotNode).contains(v)
}
val commonDotsKeys = commonDots.keys
val lhsUniqueDots = lhsVs -- commonDotsKeys
val rhsUniqueDots = rhsVs -- commonDotsKeys
val lhsKeep = ORSet.subtractDots(VersionVector(lhsUniqueDots), rhs.vvector)
val rhsKeep = ORSet.subtractDots(VersionVector(rhsUniqueDots), lhs.vvector)
val merged = lhsKeep.merge(rhsKeep).merge(VersionVector(commonDots))
// Perfectly possible that an item in both sets should be dropped
if (merged.isEmpty) acc
else acc.updated(k, merged)
case (ManyVersionVector(lhsVs), OneVersionVector(n2, v2)) =>
val commonDots = lhsVs.filter {
case (n1, v1) => v1 == v2 && n1 == n2
}
val commonDotsKeys = commonDots.keys
val lhsUniqueDots = lhsVs -- commonDotsKeys
val rhsUnique = if (commonDotsKeys.isEmpty) rhsDots else VersionVector.empty
val lhsKeep = ORSet.subtractDots(VersionVector(lhsUniqueDots), rhs.vvector)
val rhsKeep = ORSet.subtractDots(rhsUnique, lhs.vvector)
val merged = lhsKeep.merge(rhsKeep).merge(VersionVector(commonDots))
// Perfectly possible that an item in both sets should be dropped
if (merged.isEmpty) acc
else acc.updated(k, merged)
case (OneVersionVector(n1, v1), ManyVersionVector(rhsVs)) =>
val commonDots = rhsVs.filter {
case (n2, v2) => v1 == v2 && n1 == n2
}
val commonDotsKeys = commonDots.keys
val lhsUnique = if (commonDotsKeys.isEmpty) lhsDots else VersionVector.empty
val rhsUniqueDots = rhsVs -- commonDotsKeys
val lhsKeep = ORSet.subtractDots(lhsUnique, rhs.vvector)
val rhsKeep = ORSet.subtractDots(VersionVector(rhsUniqueDots), lhs.vvector)
val merged = lhsKeep.merge(rhsKeep).merge(VersionVector(commonDots))
// Perfectly possible that an item in both sets should be dropped
if (merged.isEmpty) acc
else acc.updated(k, merged)
}
}
}
/**
* INTERNAL API
* @see [[ORSet#merge]]
*/
@InternalApi private[pekko] def mergeDisjointKeys[A](
keys: Set[A],
elementsMap: Map[A, ORSet.Dot],
vvector: VersionVector,
accumulator: Map[A, ORSet.Dot]): Map[A, ORSet.Dot] =
mergeDisjointKeys(keys.iterator, elementsMap, vvector, accumulator)
private def mergeDisjointKeys[A](
keys: Iterator[A],
elementsMap: Map[A, ORSet.Dot],
vvector: VersionVector,
accumulator: Map[A, ORSet.Dot]): Map[A, ORSet.Dot] = {
keys.foldLeft(accumulator) {
case (acc, k) =>
val dots = elementsMap(k)
if (vvector > dots || vvector == dots)
acc
else {
// Optimise the set of stored dots to include only those unseen
val newDots = subtractDots(dots, vvector)
acc.updated(k, newDots)
}
}
}
}
/**
* Implements a 'Observed Remove Set' CRDT, also called a 'OR-Set'.
* Elements can be added and removed any number of times. Concurrent add wins
* over remove.
*
* It is not implemented as in the paper
* A comprehensive study of Convergent and Commutative Replicated Data Types.
* This is more space efficient and doesn't accumulate garbage for removed elements.
* It is described in the paper
* An optimized conflict-free replicated set
* The implementation is inspired by the Riak DT
* riak_dt_orswot.
*
* The ORSet has a version vector that is incremented when an element is added to
* the set. The `node -> count` pair for that increment is stored against the
* element as its "birth dot". Every time the element is re-added to the set,
* its "birth dot" is updated to that of the `node -> count` version vector entry
* resulting from the add. When an element is removed, we simply drop it, no tombstones.
*
* When an element exists in replica A and not replica B, is it because A added
* it and B has not yet seen that, or that B removed it and A has not yet seen that?
* In this implementation we compare the `dot` of the present element to the version vector
* in the Set it is absent from. If the element dot is not "seen" by the Set version vector,
* that means the other set has yet to see this add, and the item is in the merged
* Set. If the Set version vector dominates the dot, that means the other Set has removed this
* element already, and the item is not in the merged Set.
*
* This class is immutable, i.e. "modifying" methods return a new instance.
*/
@SerialVersionUID(1L)
final class ORSet[A] private[pekko] (
private[pekko] val elementsMap: Map[A, ORSet.Dot],
private[pekko] val vvector: VersionVector,
override val delta: Option[ORSet.DeltaOp] = None)
extends DeltaReplicatedData
with ReplicatedDataSerialization
with RemovedNodePruning
with FastMerge {
type T = ORSet[A]
type D = ORSet.DeltaOp
/**
* Scala API
*/
def elements: Set[A] = elementsMap.keySet
/**
* Java API
*/
def getElements(): java.util.Set[A] = {
import pekko.util.ccompat.JavaConverters._
elements.asJava
}
def contains(a: A): Boolean = elementsMap.contains(a)
def isEmpty: Boolean = elementsMap.isEmpty
def size: Int = elementsMap.size
/** Adds an element to the set. */
def :+(element: A)(implicit node: SelfUniqueAddress): ORSet[A] = add(node, element)
@deprecated("Use `:+` that takes a `SelfUniqueAddress` parameter instead.", since = "Akka 2.5.20")
def +(element: A)(implicit node: Cluster): ORSet[A] = add(node.selfUniqueAddress, element)
/** Adds an element to the set. */
def add(node: SelfUniqueAddress, element: A): ORSet[A] = add(node.uniqueAddress, element)
@deprecated("Use `add` that takes a `SelfUniqueAddress` parameter instead.", since = "Akka 2.5.20")
def add(node: Cluster, element: A): ORSet[A] = add(node.selfUniqueAddress, element)
/**
* INTERNAL API
*/
@InternalApi private[pekko] def add(node: UniqueAddress, element: A): ORSet[A] = {
val newVvector = vvector + node
val newDot = VersionVector(node, newVvector.versionAt(node))
val newDelta = delta match {
case None =>
ORSet.AddDeltaOp(new ORSet(Map(element -> newDot), newDot))
case Some(existing: ORSet.AddDeltaOp[_]) =>
existing.merge(ORSet.AddDeltaOp(new ORSet(Map(element -> newDot), newDot)))
case Some(d) =>
d.merge(ORSet.AddDeltaOp(new ORSet(Map(element -> newDot), newDot)))
}
assignAncestor(new ORSet(elementsMap.updated(element, newDot), newVvector, Some(newDelta)))
}
/**
* Scala API
* Removes an element from the set.
*/
def remove(element: A)(implicit node: SelfUniqueAddress): ORSet[A] = remove(node.uniqueAddress, element)
/**
* Java API
* Removes an element from the set.
*/
def remove(node: SelfUniqueAddress, element: A): ORSet[A] = remove(node.uniqueAddress, element)
/**
* Removes an element from the set.
*/
@deprecated("Use `remove` that takes a `SelfUniqueAddress` parameter instead.", since = "Akka 2.5.20")
def -(element: A)(implicit node: Cluster): ORSet[A] = remove(node.selfUniqueAddress, element)
/**
* Removes an element from the set.
*/
@deprecated("Use `remove` that takes a `SelfUniqueAddress` parameter instead.", since = "Akka 2.5.20")
def remove(node: Cluster, element: A): ORSet[A] = remove(node.selfUniqueAddress, element)
/**
* INTERNAL API
*/
@InternalApi private[pekko] def remove(node: UniqueAddress, element: A): ORSet[A] = {
val deltaDot = VersionVector(node, vvector.versionAt(node))
val rmOp = ORSet.RemoveDeltaOp(new ORSet(Map(element -> deltaDot), vvector))
val newDelta = delta match {
case None => rmOp
case Some(d) => d.merge(rmOp)
}
assignAncestor(copy(elementsMap = elementsMap - element, delta = Some(newDelta)))
}
/**
* Removes all elements from the set, but keeps the history.
* This has the same result as using
* [[ORSet#remove(node:org\.apache\.pekko\.cluster\.ddata\.SelfUniqueAddress*]]
* for each element, but it is more efficient.
*/
def clear(@unused node: SelfUniqueAddress): ORSet[A] = clear()
@deprecated("Use `remove` that takes a `SelfUniqueAddress` parameter instead.", since = "Akka 2.5.20")
def clear(@unused node: Cluster): ORSet[A] = clear()
/**
* INTERNAL API
*/
@InternalApi private[pekko] def clear(): ORSet[A] = {
val newFullState = new ORSet[A](elementsMap = Map.empty, vvector)
val clearOp = ORSet.FullStateDeltaOp(newFullState)
val newDelta = delta match {
case None => clearOp
case Some(d) => d.merge(clearOp)
}
assignAncestor(newFullState.copy(delta = Some(newDelta)))
}
/**
* When element is in this Set but not in that Set:
* Compare the "birth dot" of the present element to the version vector in the Set it is absent from.
* If the element dot is not "seen" by other Set version vector, that means the other set has yet to
* see this add, and the element is to be in the merged Set.
* If the other Set version vector dominates the dot, that means the other Set has removed
* the element already, and the element is not to be in the merged Set.
*
* When element in both this Set and in that Set:
* Some dots may still need to be shed. If this Set has dots that the other Set does not have,
* and the other Set version vector dominates those dots, then we need to drop those dots.
* Keep only common dots, and dots that are not dominated by the other sides version vector
*/
override def merge(that: ORSet[A]): ORSet[A] = {
if ((this eq that) || that.isAncestorOf(this)) this.clearAncestor()
else if (this.isAncestorOf(that)) that.clearAncestor()
else dryMerge(that, addDeltaOp = false)
}
// share merge impl between full state merge and AddDeltaOp merge
private def dryMerge(that: ORSet[A], addDeltaOp: Boolean): ORSet[A] = {
val commonKeys =
if (this.elementsMap.size < that.elementsMap.size)
this.elementsMap.keysIterator.filter(that.elementsMap.contains)
else
that.elementsMap.keysIterator.filter(this.elementsMap.contains)
val entries00 = ORSet.mergeCommonKeys(commonKeys, this, that)
val entries0 =
if (addDeltaOp)
entries00 ++ this.elementsMap.filter { case (elem, _) => !that.elementsMap.contains(elem) }
else {
val thisUniqueKeys = this.elementsMap.keysIterator.filterNot(that.elementsMap.contains)
ORSet.mergeDisjointKeys(thisUniqueKeys, this.elementsMap, that.vvector, entries00)
}
val thatUniqueKeys = that.elementsMap.keysIterator.filterNot(this.elementsMap.contains)
val entries = ORSet.mergeDisjointKeys(thatUniqueKeys, that.elementsMap, this.vvector, entries0)
val mergedVvector = this.vvector.merge(that.vvector)
clearAncestor()
new ORSet(entries, mergedVvector)
}
override def mergeDelta(thatDelta: ORSet.DeltaOp): ORSet[A] = {
thatDelta match {
case d: ORSet.AddDeltaOp[_] => dryMerge(d.asInstanceOf[ORSet.AddDeltaOp[A]].underlying, addDeltaOp = true)
case d: ORSet.RemoveDeltaOp[_] => mergeRemoveDelta(d.asInstanceOf[ORSet.RemoveDeltaOp[A]])
case d: ORSet.FullStateDeltaOp[_] =>
dryMerge(d.asInstanceOf[ORSet.FullStateDeltaOp[A]].underlying, addDeltaOp = false)
case ORSet.DeltaGroup(ops) =>
ops.foldLeft(this) {
case (acc, op: ORSet.AddDeltaOp[_]) =>
acc.dryMerge(op.asInstanceOf[ORSet.AddDeltaOp[A]].underlying, addDeltaOp = true)
case (acc, op: ORSet.RemoveDeltaOp[_]) => acc.mergeRemoveDelta(op.asInstanceOf[ORSet.RemoveDeltaOp[A]])
case (acc, op: ORSet.FullStateDeltaOp[_]) =>
acc.dryMerge(op.asInstanceOf[ORSet.FullStateDeltaOp[A]].underlying, addDeltaOp = false)
case (_, _: ORSet.DeltaGroup[_]) =>
throw new IllegalArgumentException("ORSet.DeltaGroup should not be nested")
}
}
}
private def mergeRemoveDelta(thatDelta: ORSet.RemoveDeltaOp[A]): ORSet[A] = {
val that = thatDelta.underlying
val (elem, thatDot) = that.elementsMap.head
def deleteDots = that.vvector.versionsIterator
def deleteDotsNodes = deleteDots.map { case (dotNode, _) => dotNode }
val newElementsMap = {
val thisDotOption = this.elementsMap.get(elem)
val deleteDotsAreGreater = deleteDots.forall {
case (dotNode, dotV) =>
thisDotOption match {
case Some(thisDot) => thisDot.versionAt(dotNode) <= dotV
case None => false
}
}
if (deleteDotsAreGreater) {
thisDotOption match {
case Some(thisDot) =>
if (thisDot.versionsIterator.forall { case (thisDotNode, _) => deleteDotsNodes.contains(thisDotNode) })
elementsMap - elem
else elementsMap
case None =>
elementsMap
}
} else
elementsMap
}
clearAncestor()
val newVvector = vvector.merge(thatDot)
new ORSet(newElementsMap, newVvector)
}
override def resetDelta: ORSet[A] =
if (delta.isEmpty) this
else assignAncestor(new ORSet(elementsMap, vvector))
override def modifiedByNodes: Set[UniqueAddress] =
vvector.modifiedByNodes
override def needPruningFrom(removedNode: UniqueAddress): Boolean =
vvector.needPruningFrom(removedNode)
override def prune(removedNode: UniqueAddress, collapseInto: UniqueAddress): ORSet[A] = {
val pruned = elementsMap.foldLeft(Map.empty[A, ORSet.Dot]) {
case (acc, (elem, dot)) =>
if (dot.needPruningFrom(removedNode)) acc.updated(elem, dot.prune(removedNode, collapseInto))
else acc
}
if (pruned.isEmpty)
copy(vvector = vvector.prune(removedNode, collapseInto))
else {
// re-add elements that were pruned, to bump dots to right vvector
val newSet = new ORSet(elementsMap = elementsMap ++ pruned, vvector = vvector.prune(removedNode, collapseInto))
pruned.keys.foldLeft(newSet) {
case (s, elem) => s.add(collapseInto, elem)
}
}
}
override def pruningCleanup(removedNode: UniqueAddress): ORSet[A] = {
val updated = elementsMap.foldLeft(elementsMap) {
case (acc, (elem, dot)) =>
if (dot.needPruningFrom(removedNode)) acc.updated(elem, dot.pruningCleanup(removedNode))
else acc
}
new ORSet(updated, vvector.pruningCleanup(removedNode))
}
private def copy(
elementsMap: Map[A, ORSet.Dot] = this.elementsMap,
vvector: VersionVector = this.vvector,
delta: Option[ORSet.DeltaOp] = this.delta): ORSet[A] =
new ORSet(elementsMap, vvector, delta)
// this class cannot be a `case class` because we need different `unapply`
override def toString: String = s"OR$elements"
override def equals(o: Any): Boolean = o match {
case other: ORSet[_] => vvector == other.vvector && elementsMap == other.elementsMap
case _ => false
}
override def hashCode: Int = {
var result = HashCode.SEED
result = HashCode.hash(result, elementsMap)
result = HashCode.hash(result, vvector)
result
}
}
object ORSetKey {
def create[A](id: String): Key[ORSet[A]] = ORSetKey(id)
}
@SerialVersionUID(1L)
final case class ORSetKey[A](_id: String) extends Key[ORSet[A]](_id) with ReplicatedDataSerialization
© 2015 - 2025 Weber Informatics LLC | Privacy Policy