Skip to content

Commit

Permalink
rewrite simple voting and introduce new abstractions
Browse files Browse the repository at this point in the history
  • Loading branch information
haaase committed Oct 23, 2024
1 parent df1a00a commit f3e656d
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,70 @@ package rdts.datatypes.experiments.protocols.simplified

import rdts.base.LocalUid.replicaId
import rdts.base.{Bottom, Lattice, LocalUid, Uid}
import rdts.datatypes.experiments.protocols.simplified.Participants.participants
import rdts.datatypes.{Epoch, GrowOnlySet}

case class Vote(leader: Uid, voter: Uid)
case class Vote[A](value: A, voter: Uid)

val numParticipants = 4

case class SimpleVoting(votes: Set[Vote]) {
def threshold: Int = numParticipants / 2 + 1
case class SimpleVoting[A](votes: Set[Vote[A]]) {
def threshold(using Participants): Int = participants.size / 2 + 1

def isLeader(using LocalUid): Boolean =
val (id, count) = leadingCount
id == replicaId && count >= threshold
def result(using Participants): Option[A] =
leadingCount match
case Some((v, count)) if count >= threshold => Some(v)
case _ => None

def voteFor(uid: Uid)(using LocalUid): SimpleVoting =
if votes.exists { case Vote(_, voter) => voter == replicaId }
then SimpleVoting(Set.empty) // already voted!
def voteFor(v: A)(using LocalUid, Participants): SimpleVoting[A] =
if !participants.contains(replicaId) || votes.exists { case Vote(_, voter) => voter == replicaId }
then SimpleVoting.unchanged // already voted!
else
SimpleVoting(Set(Vote(uid, replicaId)))
SimpleVoting(Set(Vote(v, replicaId)))

def leadingCount(using id: LocalUid): (Uid, Int) =
val grouped: Map[Uid, Int] = votes.groupBy(_.leader).map((o, elems) => (o, elems.size))
if grouped.isEmpty
then (replicaId, 0)
else grouped.maxBy((o, size) => size)
def leadingCount: Option[(A, Int)] =
val grouped: Map[A, Int] = votes.groupBy(_.value).map((value, vts) => (value, vts.size))
grouped.maxByOption((_, size) => size)
}

case class MultiRoundVoting(rounds: Epoch[SimpleVoting]):
def release: MultiRoundVoting =
MultiRoundVoting(Epoch(rounds.counter + 1, SimpleVoting(Set.empty)))
type LeaderElection = SimpleVoting[Uid]

def upkeep(using LocalUid): MultiRoundVoting =
val (id, count) = rounds.value.leadingCount
if checkIfMajorityPossible
then voteFor(id)
else release
case class Participants(members: Set[Uid])

def checkIfMajorityPossible(using localUid: LocalUid): Boolean =
val (id, count) = rounds.value.leadingCount
object Participants:
def participants(using p: Participants): Set[Uid] =
p.members

case class MultiRoundVoting[A](rounds: Epoch[SimpleVoting[A]]):
def release(using Participants): MultiRoundVoting[A] =
MultiRoundVoting(Epoch(rounds.counter + 1, SimpleVoting.unchanged))

def upkeep(using LocalUid, Participants): MultiRoundVoting[A] =
rounds.value.leadingCount match
case Some(value, count) if checkIfMajorityPossible => voteFor(value)
case Some(_) => release // we have a leading proposal but majority is not possible anymore
case None => MultiRoundVoting.unchanged // no change yet

def checkIfMajorityPossible(using Participants): Boolean =
val totalVotes = rounds.value.votes.size
val remainingVotes = numParticipants - totalVotes
(count + remainingVotes) > rounds.value.threshold
val remainingVotes = participants.size - totalVotes
val possible = rounds.value.leadingCount.map((_, count) => (count + remainingVotes) >= rounds.value.threshold)
possible.getOrElse(true) // if there is no leading vote, majority is always possible

// api
def voteFor(uid: Uid)(using LocalUid): MultiRoundVoting =
MultiRoundVoting(Epoch(rounds.counter, rounds.value.voteFor(uid)))
def voteFor(c: A)(using LocalUid, Participants): MultiRoundVoting[A] =
MultiRoundVoting(Epoch(rounds.counter, rounds.value.voteFor(c)))

def isLeader(using LocalUid): Boolean =
rounds.value.isLeader
def result(using Participants): Option[A] =
rounds.value.result

object SimpleVoting {
given Lattice[SimpleVoting] = Lattice.derived
given Bottom[SimpleVoting] with
override def empty: SimpleVoting = unchanged
def unchanged: SimpleVoting = SimpleVoting(GrowOnlySet.empty)
given lattice[A]: Lattice[SimpleVoting[A]] = Lattice.derived
given bottom[A](using Participants): Bottom[SimpleVoting[A]] with
override def empty: SimpleVoting[A] = unchanged
def unchanged[A](using Participants): SimpleVoting[A] = SimpleVoting(Set.empty)
}
object MultiRoundVoting {
def unchanged: MultiRoundVoting = MultiRoundVoting(Epoch.empty[SimpleVoting])
given Lattice[MultiRoundVoting] = Lattice.derived
def unchanged[A](using Participants): MultiRoundVoting[A] = MultiRoundVoting(Epoch.empty[SimpleVoting[A]])
given lattice[A]: Lattice[MultiRoundVoting[A]] = Lattice.derived
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
package test.rdts.protocols

import rdts.base.Lattice.syntax.merge
import rdts.base.{Lattice, LocalUid}
import rdts.datatypes.experiments.protocols.simplified.{MultiRoundVoting, SimpleVoting}
import rdts.base.{Lattice, LocalUid, Uid}
import rdts.datatypes.experiments.protocols.simplified.{LeaderElection, MultiRoundVoting, Participants, SimpleVoting}

class SimpleVotingTests extends munit.FunSuite {

val id1 = LocalUid.gen()
val id2 = LocalUid.gen()
val id3 = LocalUid.gen()
val id4 = LocalUid.gen()
given Participants(Set(id1, id2, id3, id4).map(_.uid))

test("Voting for 4 participants") {
var voting: SimpleVoting = SimpleVoting.unchanged
var voting: LeaderElection = SimpleVoting.unchanged
voting = voting `merge` voting.voteFor(id1.uid)(using id1)
assert(!voting.isLeader(using id1))
assertEquals(voting.result, None)
voting = voting `merge` voting.voteFor(id1.uid)(using id2) `merge` voting.voteFor(id1.uid)(using id3)
assert(voting.isLeader(using id1))
assert(!voting.isLeader(using id2))

assertEquals(voting.result, Some(id1.uid))
// voting again does not change anything:
assertEquals(voting.voteFor(id1.uid)(using id1), SimpleVoting.unchanged)
}

test("Multiroundvoting for 4 participants") {
var voting: MultiRoundVoting = MultiRoundVoting.unchanged
var voting: MultiRoundVoting[Uid] = MultiRoundVoting.unchanged
// everybody voting for id1
voting = voting `merge` voting.voteFor(id1.uid)(using id1)
assert(!voting.isLeader(using id1))
assertEquals(voting.result, None)
voting = voting `merge` voting.voteFor(id1.uid)(using id2) `merge` voting.voteFor(id1.uid)(using id3)
assert(voting.isLeader(using id1))
assert(!voting.isLeader(using id2))
assertEquals(voting.result, Some(id1.uid))
// releasing
voting = voting `merge` voting.release
assert(!voting.isLeader(using id1))
assertEquals(voting.result, None)
// voting with upkeep
voting = voting `merge` voting.voteFor(id1.uid)(using id1)
assert(!voting.isLeader(using id1))
assertEquals(voting.result, None)
voting = voting `merge` voting.upkeep(using id2) `merge` voting.upkeep(using id3)
assert(voting.isLeader(using id1))
assertEquals(voting.result, Some(id1.uid))
// majority not possible
voting = voting `merge` voting.release
voting = voting `merge` voting.voteFor(id1.uid)(using id1) `merge` voting.voteFor(id2.uid)(using
id2
) `merge` voting.voteFor(id3.uid)(using id3)
assert(!voting.checkIfMajorityPossible(using id1))
assert(!voting.checkIfMajorityPossible)
// check that upkeep cleans
voting = voting `merge` voting.upkeep(using id1)
assertEquals(voting.rounds.counter, Integer.toUnsignedLong(3))
Expand Down

0 comments on commit f3e656d

Please sign in to comment.