Skip to content

Commit efd122b

Browse files
author
Unmesh Joshi
committed
Added leader election with generation voting examples
1 parent 38cec15 commit efd122b

File tree

2 files changed

+203
-2
lines changed

2 files changed

+203
-2
lines changed

src/main/java/replicate/generationvoting/GenerationVoting.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,22 @@ private void handlePrepareResponse(Message<PrepareResponse> prepareResponseMessa
5252
}
5353

5454
CompletableFuture<Integer> handleNextNumberRequest(NextNumberRequest request) {
55-
return proposeNumber(generation);
55+
return runElection();
5656
}
5757

58-
private CompletableFuture<Integer> proposeNumber(int proposedNumber) {
58+
/**
59+
* Initiates a leader election process for this node.
60+
* The node will attempt to become leader by proposing a generation number
61+
* higher than any it has seen before.
62+
*
63+
* @return CompletableFuture with the generation number this node was elected with
64+
*/
65+
public CompletableFuture<Integer> runElection() {
66+
return proposeNumber(generation);
67+
}
68+
69+
// Package-private for testing
70+
CompletableFuture<Integer> proposeNumber(int proposedNumber) {
5971
int maxAttempts = 5;
6072
AtomicInteger proposal = new AtomicInteger(proposedNumber);
6173
return FutureUtils.retryWithRandomDelay(() -> {
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package replicate.generationvoting;
2+
3+
import org.junit.Test;
4+
import replicate.common.ClusterTest;
5+
import replicate.common.TestUtils;
6+
7+
import java.io.IOException;
8+
import java.util.Arrays;
9+
import java.util.concurrent.CompletableFuture;
10+
11+
import static org.junit.Assert.*;
12+
13+
/**
14+
* LeaderElectionProcessTest demonstrates the step-by-step leader election process
15+
* with generation numbers as described in the consensus introduction slides.
16+
*
17+
* This test corresponds to the "leader_election_process" diagram reference and shows:
18+
* 1. Athens chooses Generation Number 1 (higher than any seen)
19+
* 2. Sends prepare requests to majority (self + Byzantium)
20+
* 3. Nodes promise not to accept requests with lower generation numbers
21+
* 4. Athens becomes Leader for Generation Number 1
22+
*
23+
* The test validates the mathematical foundation of consensus algorithms:
24+
* - Generation numbers establish clear ordering and authority
25+
* - Majority quorums ensure consistency
26+
* - Promise mechanism prevents conflicting leadership
27+
*/
28+
public class LeaderElectionProcessTest extends ClusterTest<GenerationVoting> {
29+
30+
@Test
31+
public void demonstratesStepByStepLeaderElectionProcess() throws Exception {
32+
// Setup: Start a 3-node cluster (Athens, Byzantium, Cyrene)
33+
// This matches exactly the scenario from leader_election_process.puml
34+
super.nodes = TestUtils.startCluster(
35+
Arrays.asList("athens", "byzantium", "cyrene"),
36+
(name, config, clock, clientConnectionAddress, peerConnectionAddress, peerAddresses)
37+
-> new GenerationVoting(name, config, clock, clientConnectionAddress, peerConnectionAddress, peerAddresses)
38+
);
39+
40+
GenerationVoting athens = nodes.get("athens");
41+
GenerationVoting byzantium = nodes.get("byzantium");
42+
GenerationVoting cyrene = nodes.get("cyrene");
43+
44+
System.out.println("=== LEADER ELECTION PROCESS TEST ===");
45+
System.out.println("Scenario: Athens wants to become leader and chooses Generation Number 1");
46+
47+
// Step 1: Verify initial state - no promises made yet
48+
assertEquals("Athens should start with generation 0", 0, athens.generation);
49+
assertEquals("Byzantium should start with generation 0", 0, byzantium.generation);
50+
assertEquals("Cyrene should start with generation 0", 0, cyrene.generation);
51+
52+
System.out.println("✓ Initial state: Athens(0), Byzantium(0), Cyrene(0)");
53+
54+
// Step 2: Athens wants to become leader (matches diagram exactly)
55+
// This exactly matches the diagram note: "Wants to become leader, Chooses Generation Number 1 (higher than any seen)"
56+
System.out.println("\n--- PHASE 1: PREPARE (Generation Number Selection) ---");
57+
System.out.println("Athens wants to become leader");
58+
System.out.println("Athens chooses Generation Number 1 (higher than any seen)");
59+
60+
// Step 3: Athens initiates leader election directly
61+
// This is much more natural and matches the diagram intention
62+
System.out.println(" Athens -> Byzantium: Prepare(generation=1)");
63+
System.out.println(" Athens -> Cyrene: Prepare(generation=1)");
64+
65+
// Athens runs election - will propose generation number higher than current (0)
66+
CompletableFuture<Integer> leaderElectionResult = athens.runElection();
67+
Integer electedGeneration = leaderElectionResult.get(); // Wait for completion
68+
69+
// Step 4: Verify Promise responses
70+
// This matches: "Byzantium -> Athens: Promise(generation=1)" and "Cyrene -> Athens: Promise(generation=1)"
71+
System.out.println("\n--- PHASE 2: PROMISE (Majority Agreement) ---");
72+
System.out.println(" Byzantium checks: 1 > any promised? Yes! Promises not to accept requests with generation < 1");
73+
System.out.println(" Cyrene checks: 1 > any promised? Yes! Promises not to accept requests with generation < 1");
74+
System.out.println(" Byzantium -> Athens: Promise(generation=1)");
75+
System.out.println(" Cyrene -> Athens: Promise(generation=1)");
76+
77+
// Step 5: Athens becomes leader
78+
// This matches: "Got majority promises! Now Leader for Generation 1 Can proceed with Phase 2"
79+
assertEquals("Athens should be elected with generation 1", 1, electedGeneration.intValue());
80+
assertEquals("Athens should have generation 1", 1, athens.generation);
81+
assertEquals("Byzantium should have promised generation 1", 1, byzantium.generation);
82+
assertEquals("Cyrene should have promised generation 1", 1, cyrene.generation);
83+
84+
System.out.println("\n--- LEADERSHIP ESTABLISHED ---");
85+
System.out.println("Athens got majority promises!");
86+
System.out.println("Athens is now Leader for Generation Number 1");
87+
System.out.println("Athens can proceed with Phase 2 (Accept/Commit)");
88+
89+
// Final state verification matching the diagram outcome
90+
System.out.println("\n--- FINAL STATE VERIFICATION ---");
91+
System.out.printf("Athens (Leader): generation=%d%n", athens.generation);
92+
System.out.printf("Byzantium (Acceptor): generation=%d%n", byzantium.generation);
93+
System.out.printf("Cyrene (Acceptor): generation=%d%n", cyrene.generation);
94+
95+
System.out.println("\n✓ Leader election process completed successfully!");
96+
System.out.println("\nKey Insights Demonstrated (matching leader_election_process.puml):");
97+
System.out.println("1. Candidate chooses generation number higher than any seen");
98+
System.out.println("2. Prepare requests sent to all acceptors");
99+
System.out.println("3. Acceptors promise not to accept lower generation numbers");
100+
System.out.println("4. Majority promises establish leadership authority");
101+
System.out.println("5. Leader can now proceed with consensus (Phase 2)");
102+
}
103+
104+
@Test
105+
public void demonstratesLeaderElectionWithNetworkPartition() throws Exception {
106+
// This test shows what happens when a node tries to become leader
107+
// but cannot reach the full majority due to network issues
108+
super.nodes = TestUtils.startCluster(
109+
Arrays.asList("athens", "byzantium", "cyrene"),
110+
(name, config, clock, clientConnectionAddress, peerConnectionAddress, peerAddresses)
111+
-> new GenerationVoting(name, config, clock, clientConnectionAddress, peerConnectionAddress, peerAddresses)
112+
);
113+
114+
GenerationVoting athens = nodes.get("athens");
115+
GenerationVoting byzantium = nodes.get("byzantium");
116+
GenerationVoting cyrene = nodes.get("cyrene");
117+
118+
System.out.println("\n=== LEADER ELECTION WITH NETWORK PARTITION TEST ===");
119+
120+
// Step 1: Create network partition - Athens cannot reach Cyrene
121+
System.out.println("Creating network partition: Athens cannot communicate with Cyrene");
122+
athens.dropMessagesTo(cyrene);
123+
cyrene.dropMessagesTo(athens);
124+
125+
// Step 2: Athens attempts leader election but can only reach Byzantium
126+
// This should still succeed since Athens + Byzantium = majority (2 out of 3)
127+
System.out.println("Athens attempts leader election with partial connectivity");
128+
System.out.println("Athens can only reach Byzantium (majority = 2 out of 3)");
129+
130+
CompletableFuture<Integer> leaderElectionResult = athens.runElection();
131+
Integer electedGeneration = leaderElectionResult.get();
132+
133+
// Step 3: Verify partial quorum success
134+
assertEquals("Athens should succeed with generation 1", 1, electedGeneration.intValue());
135+
assertEquals("Athens should have generation 1", 1, athens.generation);
136+
assertEquals("Byzantium should have generation 1", 1, byzantium.generation);
137+
assertEquals("Cyrene should remain at generation 0 (partitioned)", 0, cyrene.generation);
138+
139+
System.out.println("✓ Leader election succeeded with partial quorum");
140+
System.out.printf("Final state: Athens(1), Byzantium(1), Cyrene(0)%n");
141+
142+
System.out.println("\nKey Insight: Majority quorum allows progress despite failures");
143+
}
144+
145+
@Test
146+
public void demonstratesCompetingLeaderElection() throws Exception {
147+
// This test shows what happens when multiple nodes try to become leader
148+
// simultaneously, demonstrating how generation numbers resolve conflicts
149+
super.nodes = TestUtils.startCluster(
150+
Arrays.asList("athens", "byzantium", "cyrene"),
151+
(name, config, clock, clientConnectionAddress, peerConnectionAddress, peerAddresses)
152+
-> new GenerationVoting(name, config, clock, clientConnectionAddress, peerConnectionAddress, peerAddresses)
153+
);
154+
155+
GenerationVoting athens = nodes.get("athens");
156+
GenerationVoting byzantium = nodes.get("byzantium");
157+
GenerationVoting cyrene = nodes.get("cyrene");
158+
159+
System.out.println("\n=== COMPETING LEADER ELECTION TEST ===");
160+
161+
// Step 1: Athens becomes leader first
162+
System.out.println("Athens initiates first leader election");
163+
CompletableFuture<Integer> athensResult = athens.runElection();
164+
Integer athensGeneration = athensResult.get();
165+
166+
assertEquals("Athens should get generation 1", 1, athensGeneration.intValue());
167+
System.out.println("✓ Athens elected as leader with generation 1");
168+
169+
// Step 2: Byzantium attempts to become leader
170+
// It should get a higher generation number since it sees Athens' generation 1
171+
System.out.println("Byzantium attempts to become leader");
172+
CompletableFuture<Integer> byzantiumResult = byzantium.runElection();
173+
Integer byzantiumGeneration = byzantiumResult.get();
174+
175+
assertEquals("Byzantium should get generation 2", 2, byzantiumGeneration.intValue());
176+
System.out.println("Byzantium elected as leader with generation 2");
177+
178+
// Step 3: Athens tries again - should get even higher generation
179+
System.out.println("Athens attempts to regain leadership");
180+
CompletableFuture<Integer> athensSecondResult = athens.runElection();
181+
Integer athensSecondGeneration = athensSecondResult.get();
182+
183+
assertEquals("Athens should get generation 3", 3, athensSecondGeneration.intValue());
184+
System.out.println("Athens regains leadership with generation 3");
185+
186+
System.out.println("\nKey Insight: Generation numbers prevent conflicting leadership");
187+
System.out.println("Higher generation numbers always take precedence");
188+
}
189+
}

0 commit comments

Comments
 (0)