Skip to content

Commit

Permalink
add an automatic lexicographic ordering, and use it instead of error …
Browse files Browse the repository at this point in the history
…prone manual orderings
  • Loading branch information
rmgk committed Jan 26, 2024
1 parent 0d2dea3 commit 33d2acd
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package replication.papoctokens

import kofre.base.{Bottom, Lattice, Uid}
import kofre.base.{Bottom, Lattice, Orderings, Uid}
import kofre.datatypes.contextual.ReplicatedSet
import kofre.datatypes.contextual.ReplicatedSet.syntax
import kofre.datatypes.experiments.RaftState
Expand All @@ -9,34 +9,33 @@ import kofre.syntax.{DeltaBuffer, OpsSyntaxHelper, ReplicaId}

import scala.util.Random

case class Token(epoche: Long, owner: Uid)
case class Ownership(epoche: Long, owner: Uid)

object Token {
given Lattice[Token] = Lattice.fromOrdering(Ordering.by(_.epoche))
object Ownership {
given Lattice[Ownership] = Lattice.fromOrdering(Orderings.lexicographic)

given bottom: Bottom[Token] = Bottom.provide(Token(Long.MinValue, Uid.zero))
given bottom: Bottom[Ownership] = Bottom.provide(Ownership(Long.MinValue, Uid.zero))

def unchanged: Token = bottom.empty
def unchanged: Ownership = bottom.empty
}

case class TokenAgreement(
token: Token,
wants: ReplicatedSet[Uid]
)
object TokenAgreement {
case class Token(owner: Ownership, wants: ReplicatedSet[Uid])
object Token {

val unchanged: Token = Token(Ownership.unchanged, ReplicatedSet.empty)

val unchanged: TokenAgreement = TokenAgreement(Token.unchanged, ReplicatedSet.empty)
given Lattice[Token] = Lattice.derived

extension [C, E](container: C)
def tokens: syntax[C] = syntax(container)

implicit class syntax[C](container: C) extends OpsSyntaxHelper[C, TokenAgreement](container) {
implicit class syntax[C](container: C) extends OpsSyntaxHelper[C, Token](container) {

def updateWant(f: Dotted[ReplicatedSet[Uid]] => Dotted[ReplicatedSet[Uid]]): CausalMutate = mutate:
val addWant: Dotted[ReplicatedSet[Uid]] = f(current.wants.inheritContext)

addWant.map: aw =>
TokenAgreement(Token.unchanged, aw)
Token(Ownership.unchanged, aw)

def request(using ReplicaId): CausalMutate = updateWant(_.add(replicaId))

Expand All @@ -49,14 +48,14 @@ object TokenAgreement {
// This is incredibly “unfair” but does prevent deadlocks in case someone needs multiple tokens.
current.wants.elements.maxOption match
case Some(head) if head != replicaId =>
TokenAgreement(Token(current.token.epoche + 1, head), ReplicatedSet.empty)
Token(Ownership(current.owner.epoche + 1, head), ReplicatedSet.empty)
case _ => unchanged

def isOwner(using ReplicaId, PermQuery): Boolean = replicaId == current.token.owner
def isOwner(using ReplicaId, PermQuery): Boolean = replicaId == current.owner.owner
}
}

case class ExampleTokens(
calendarA: TokenAgreement,
calendarB: TokenAgreement
calendarA: Token,
calendarB: Token
)
34 changes: 34 additions & 0 deletions Modules/RDTs/src/main/scala/kofre/base/Orderings.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kofre.base

import scala.annotation.targetName
import scala.compiletime.{erasedValue, summonAll, summonFrom, summonInline}
import scala.deriving.Mirror

object Orderings {

inline def lexicographic[T <: Product](using pm: Mirror.ProductOf[T]): Ordering[T] = {
val orderings: Tuple = summonAll[Tuple.Map[pm.MirroredElemTypes, Ordering]]
new LexicographicOrdering[T](orderings, pm)
}

class LexicographicOrdering[T <: Product](
orderings: Tuple,
pm: Mirror.ProductOf[T],
) extends Ordering[T] {
override def compare(x: T, y: T): Int =
def rec(idx: Int): Int =
if idx >= orderings.productArity
then 0
else
orderings.productElement(idx).asInstanceOf[Ordering[Any]].compare(
x.productElement(idx),
y.productElement(idx)
) match
case 0 => rec(idx + 1)
case lt if lt < 0 => -1
case gt if gt > 0 => 1
rec(0)

}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package kofre.datatypes

import kofre.base.{Bottom, Lattice}
import kofre.base.{Bottom, Lattice, Orderings}
import kofre.datatypes.contextual.MultiVersionRegister
import kofre.dotted.HasDots
import kofre.syntax.OpsSyntaxHelper
Expand All @@ -27,11 +27,9 @@ object LastWriterWins {
given hasDots[A]: HasDots[LastWriterWins[A]] = HasDots.noDots

given lattice[A]: Lattice[LastWriterWins[A]] =
given Ordering[A] = MultiVersionRegister.assertEqualsOrdering
Lattice.fromOrdering:
CausalTime.ordering.on[LastWriterWins[A]](_.timestamp).orElse:
// Technically, this is not necessary, as equal timestamps are assumed to have equal values by precondition.
// But it is a heck of a lot easier to debug when this throws a proper exception instead of producing inconsistencies due to the violated assumptions.
MultiVersionRegister.assertEqualsOrdering.on(_.payload)
Orderings.lexicographic

inline def generalizedLattice[A]: Lattice[LastWriterWins[A]] = scala.compiletime.summonFrom {
case conflictCase: Lattice[A] => GenericLastWriterWinsLattice(conflictCase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ object MultiVersionRegister {

given bottomInstance[A]: Bottom[MultiVersionRegister[A]] = Bottom.derived

val assertEqualsOrdering: Ordering[Any] = (l, r) =>
private val _assertEqualsOrdering: Ordering[Any] = (l, r) =>
if l == r then 0
else throw IllegalStateException(s"assumed equality does not hold for »$l« and »$r« ")
def assertEqualsLattice[A]: Lattice[A] = Lattice.fromOrdering(assertEqualsOrdering.on(identity))
// we could replace this by casting …
def assertEqualsOrdering[A]: Ordering[A] = _assertEqualsOrdering.on(identity)
def assertEqualsLattice[A]: Lattice[A] = Lattice.fromOrdering(assertEqualsOrdering)

given dottedLattice[A]: Lattice[MultiVersionRegister[A]] =
given Lattice[A] = Lattice.fromOrdering(assertEqualsOrdering.on(identity))
given Lattice[A] = Lattice.fromOrdering(assertEqualsOrdering)
Lattice.derived

given hasDot[A]: HasDots[MultiVersionRegister[A]] = HasDots.derived
Expand Down
7 changes: 2 additions & 5 deletions Modules/RDTs/src/main/scala/kofre/time/CausalTime.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package kofre.time

import kofre.base.Lattice
import kofre.base.{Lattice, Orderings}

import java.util.concurrent.atomic.AtomicLong
import scala.math.Ordering.Implicits.infixOrderingOps
Expand All @@ -20,10 +20,7 @@ case class CausalTime(time: Time, causal: Long, random: Long):
else now

object CausalTime:
given ordering: Ordering[CausalTime] =
Ordering.by[CausalTime, Long](_.time)
.orElseBy(_.causal)
.orElseBy(_.random)
given ordering: Ordering[CausalTime] = Orderings.lexicographic

given lattice: Lattice[CausalTime] = Lattice.fromOrdering(ordering)

Expand Down

0 comments on commit 33d2acd

Please sign in to comment.