From fa5cdc60d173dbb01479ce3c2380d112229fca35 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 14 Jan 2025 13:39:31 +0100 Subject: [PATCH] add tests for multipaxos --- .../experiments/protocols/Paxos.scala | 9 +- .../test/rdts/protocols/MultiPaxosTest.scala | 93 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 Modules/RDTs/src/test/scala/test/rdts/protocols/MultiPaxosTest.scala diff --git a/Modules/RDTs/src/main/scala/rdts/datatypes/experiments/protocols/Paxos.scala b/Modules/RDTs/src/main/scala/rdts/datatypes/experiments/protocols/Paxos.scala index 23f6b6779..060e19a1f 100644 --- a/Modules/RDTs/src/main/scala/rdts/datatypes/experiments/protocols/Paxos.scala +++ b/Modules/RDTs/src/main/scala/rdts/datatypes/experiments/protocols/Paxos.scala @@ -39,7 +39,8 @@ case class Paxos[A]( rounds.filter { case (b, p) => b.uid == replicaId }.maxOption def lastValueVote: Option[(BallotNum, PaxosRound[A])] = rounds.filter(_._2.proposals.votes.nonEmpty).maxOption def newestReceivedVal(using LocalUid) = lastValueVote.map(_._2.proposals.votes.head.value) - def myValue(using LocalUid): A = rounds(BallotNum(replicaId, -1)).proposals.votes.head.value + def myValue(using LocalUid): Option[A] = + rounds.get(BallotNum(replicaId, -1)).map(_.proposals.votes.head.value) def decidedVal(using Participants): Option[A] = rounds.collectFirst { case (b, PaxosRound(_, voting)) if voting.result.isDefined => voting.result.get } def decidedLeaderElection(using Participants): Option[(BallotNum, LeaderElection)] = @@ -70,8 +71,10 @@ case class Paxos[A]( case _ => Paxos() // do nothing def phase2a(using LocalUid, Participants): Paxos[A] = - // propose a value if I am the leader - phase2a(myValue) + // try to determine my process' value from previous ballot + myValue match + case Some(value) => phase2a(value) + case None => Paxos() def phase2a(value: A)(using LocalUid, Participants): Paxos[A] = // propose a value if I am the leader diff --git a/Modules/RDTs/src/test/scala/test/rdts/protocols/MultiPaxosTest.scala b/Modules/RDTs/src/test/scala/test/rdts/protocols/MultiPaxosTest.scala new file mode 100644 index 000000000..f887a35e6 --- /dev/null +++ b/Modules/RDTs/src/test/scala/test/rdts/protocols/MultiPaxosTest.scala @@ -0,0 +1,93 @@ +package test.rdts.protocols + +import rdts.base.LocalUid +import rdts.datatypes.experiments.protocols.Paxos.given +import rdts.datatypes.experiments.protocols.{MultiPaxos, MultipaxosPhase, Participants, Paxos} + +class MultiPaxosTest extends munit.FunSuite { + + val id1 = LocalUid.gen() + val id2 = LocalUid.gen() + val id3 = LocalUid.gen() + + given Participants = Participants(Set(id1, id2, id3).map(_.uid)) + + val emptyPaxosObject: MultiPaxos[Int] = MultiPaxos() + test("happy path") { + var testPaxosObject = emptyPaxosObject + + assertEquals(testPaxosObject.leader, None) + assertEquals(testPaxosObject.phase, MultipaxosPhase.LeaderElection, "multipaxos starts in leader election phase") + + val proposeValue = 1 + // replica 1 tries to become leader + testPaxosObject = testPaxosObject.merge(testPaxosObject.startLeaderElection(using id1)) + + // testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id1)) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id2)) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id3)) + + assertEquals(testPaxosObject.leader, Some(id1.uid), "id1 should be the leader") + + // replica 2 tries to write + val afterwrite = testPaxosObject.merge(testPaxosObject.proposeIfLeader(2)(using id2)) + assertEquals(afterwrite, testPaxosObject) + + // replica 1 (leader) writes + val afterLeaderWrite = testPaxosObject.merge(testPaxosObject.proposeIfLeader(1)(using id1)) + assertNotEquals(afterLeaderWrite, testPaxosObject) + testPaxosObject = afterLeaderWrite + + assertEquals(testPaxosObject.log, Map.empty) + assertEquals(testPaxosObject.phase, MultipaxosPhase.Voting) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id3)) + assertEquals(testPaxosObject.log.values.toList, List(1)) + assertEquals(testPaxosObject.phase, MultipaxosPhase.Idle) + + testPaxosObject.merge(testPaxosObject.proposeIfLeader(2)(using id2)) + + // replica 1 (leader) writes again + testPaxosObject = testPaxosObject.merge(testPaxosObject.proposeIfLeader(12)(using id1)) + assertEquals(testPaxosObject.log.values.toList, List(1)) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id2)) + assertEquals(testPaxosObject.log.values.toList.sorted, List(1, 12)) + + // replica 3 starts new leader election + testPaxosObject = testPaxosObject.merge(testPaxosObject.startLeaderElection(using id3)) + assertEquals(testPaxosObject.log.values.toList.sorted, List(1, 12), "log survives new leader election") + assertEquals(testPaxosObject.phase, MultipaxosPhase.LeaderElection) + assertEquals(testPaxosObject.leader, None) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id3)) + assertEquals(testPaxosObject.phase, MultipaxosPhase.LeaderElection) + assertEquals(testPaxosObject.leader, None) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id2)) + assertEquals(testPaxosObject.phase, MultipaxosPhase.Idle) + assertEquals(testPaxosObject.leader, Some(id3.uid)) + } + + test("conflicting proposals") { + var testPaxosObject = emptyPaxosObject + + // replicas 1 and 2 try to become leader + var rep1 = emptyPaxosObject.merge(emptyPaxosObject.startLeaderElection(using id1)) + var rep2 = emptyPaxosObject.merge(emptyPaxosObject.startLeaderElection(using id2)) + + // sync + testPaxosObject = testPaxosObject.merge(rep1).merge(rep2) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id1)) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id2)) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id3)) + + // propose values + rep1 = testPaxosObject.proposeIfLeader(1)(using id1) + rep2 = testPaxosObject.proposeIfLeader(2)(using id2) + + // sync + testPaxosObject = testPaxosObject.merge(rep1).merge(rep2) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id1)) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id2)) + testPaxosObject = testPaxosObject.merge(testPaxosObject.upkeep(using id3)) + + assert(testPaxosObject.log.values.head == 1 || testPaxosObject.log.values.head == 2) + } +}