summaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-20 23:04:48 +0300
committerPaul Buetow <paul@buetow.org>2025-06-20 23:04:48 +0300
commitdedec9b18bafa2bcfdb05429f717f95f2236d811 (patch)
tree379a6d18ebf95aa552a6bd8722f1b7ebee9f8ca6 /src/test
parent32882ca8582a102b9357e8d7f2c313d52c568977 (diff)
Implement Raft consensus algorithm for distributed systems simulator
This commit adds a complete implementation of the Raft consensus algorithm, providing a robust solution for achieving consensus in distributed systems. Key features implemented: - Three-state system: Follower, Candidate, and Leader states - Leader election with randomized election timeouts (150-300ms) - Log replication for state machine commands - Heartbeat mechanism to maintain leader authority - Safety guarantees through term numbers and log consistency checks - Proper handling of split votes and network partitions Implementation details: - Added VSRaftProtocol.java with full Raft algorithm logic - Integrated with existing event-driven simulation framework - Supports dynamic cluster sizes with proper quorum calculations - Implements RequestVote and AppendEntries RPCs - Maintains persistent state (currentTerm, votedFor, log entries) - Includes comprehensive logging for debugging and visualization Testing: - Added VSRaftProtocolTest.java with unit tests covering: - Leader election scenarios - Log replication mechanics - State transitions - Message handling for all RPC types Integration: - Registered protocol in VSRegisteredEvents for simulator discovery - Added human-readable names in VSDefaultPrefs for UI display - Compatible with existing visualization and timing systems This implementation follows the Raft paper closely while adapting to the simulator's event-driven architecture and message-passing model. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/protocols/implementations/VSRaftProtocolTest.java308
1 files changed, 308 insertions, 0 deletions
diff --git a/src/test/java/protocols/implementations/VSRaftProtocolTest.java b/src/test/java/protocols/implementations/VSRaftProtocolTest.java
new file mode 100644
index 0000000..e838f51
--- /dev/null
+++ b/src/test/java/protocols/implementations/VSRaftProtocolTest.java
@@ -0,0 +1,308 @@
+package protocols.implementations;
+
+import core.VSAbstractProcess;
+import core.VSInternalProcess;
+import core.VSMessage;
+import core.VSTaskManager;
+import core.time.VSVectorTime;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import prefs.VSPrefs;
+import simulator.VSLogging;
+import simulator.VSSimulatorVisualization;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class VSRaftProtocolTest {
+
+ private VSRaftProtocol protocol;
+ private VSInternalProcess mockProcess;
+ private VSPrefs mockPrefs;
+ private VSSimulatorVisualization mockCanvas;
+ private VSTaskManager mockTaskManager;
+ private VSVectorTime mockVectorTime;
+
+ @BeforeEach
+ void setUp() {
+ protocol = new VSRaftProtocol();
+ mockProcess = mock(VSInternalProcess.class);
+ mockPrefs = mock(VSPrefs.class);
+ mockCanvas = mock(VSSimulatorVisualization.class);
+ mockTaskManager = mock(VSTaskManager.class);
+ mockVectorTime = mock(VSVectorTime.class);
+
+ // Set up process behavior
+ when(mockProcess.getProcessNum()).thenReturn(1);
+ when(mockProcess.getTime()).thenReturn(1000L);
+ when(mockProcess.getRandomPercentage()).thenReturn(50);
+ when(mockProcess.getSimulatorCanvas()).thenReturn(mockCanvas);
+ when(mockCanvas.getNumProcesses()).thenReturn(3);
+ when(mockCanvas.getTaskManager()).thenReturn(mockTaskManager);
+ when(mockProcess.getVectorTime()).thenReturn(mockVectorTime);
+ when(mockVectorTime.getCopy()).thenReturn(mockVectorTime);
+ when(mockProcess.getLamportTime()).thenReturn(100L);
+
+ // Set process and prefs directly via field access (like other protocol tests)
+ protocol.process = mockProcess;
+ protocol.prefs = mockPrefs;
+ protocol.isServer(true);
+ }
+
+ @Test
+ void testServerInitialization() {
+ // Test server initialization
+ protocol.onServerInit();
+ protocol.onServerStart();
+
+ // Protocol should start as follower
+ verify(mockProcess).log(contains("FOLLOWER"));
+ }
+
+ @Test
+ void testElectionTimeout() {
+ // Initialize protocol
+ protocol.onServerInit();
+
+ // Remove any scheduled tasks to clean state
+ doNothing().when(mockTaskManager).removeAllTasks(any());
+ protocol.onServerReset();
+ protocol.onServerInit();
+ protocol.onServerStart();
+
+ // Simulate election timeout by calling onServerSchedule
+ when(mockProcess.getTime()).thenReturn(2000L); // Well past timeout
+ protocol.onServerSchedule();
+
+ // Should start election and send vote requests
+ ArgumentCaptor<VSMessage> messageCaptor = ArgumentCaptor.forClass(VSMessage.class);
+ verify(mockProcess, atLeastOnce()).sendMessage(messageCaptor.capture());
+
+ VSMessage sentMessage = messageCaptor.getValue();
+ assertEquals("REQUEST_VOTE", sentMessage.getString("type"));
+ }
+
+ @Test
+ void testVoteRequest() {
+ // Initialize protocol
+ protocol.onServerInit();
+
+ // Create vote request from another node
+ VSMessage voteRequest = mock(VSMessage.class);
+ when(voteRequest.getString("type")).thenReturn("REQUEST_VOTE");
+ when(voteRequest.getInteger("term")).thenReturn(2);
+ when(voteRequest.getInteger("candidateId")).thenReturn(2);
+ when(voteRequest.getInteger("lastLogIndex")).thenReturn(0);
+ when(voteRequest.getInteger("lastLogTerm")).thenReturn(0);
+
+ // Mock sender process
+ VSInternalProcess mockSender = mock(VSInternalProcess.class);
+ when(mockSender.getProcessNum()).thenReturn(2);
+ when(voteRequest.getSendingProcess()).thenReturn(mockSender);
+
+ // Process vote request
+ protocol.onServerRecv(voteRequest);
+
+ // Should send vote response
+ ArgumentCaptor<VSMessage> responseCaptor = ArgumentCaptor.forClass(VSMessage.class);
+ verify(mockProcess).sendMessage(responseCaptor.capture());
+
+ VSMessage response = responseCaptor.getValue();
+ assertEquals("VOTE_RESPONSE", response.getString("type"));
+ assertTrue(response.getBoolean("voteGranted"));
+ }
+
+ @Test
+ void testBecomeLeader() {
+ // Initialize protocol
+ protocol.onServerInit();
+ protocol.isServer(true);
+
+ // Start election
+ when(mockProcess.getTime()).thenReturn(2000L);
+ protocol.onServerSchedule();
+
+ // Should vote for itself and need one more vote (in 3-node cluster)
+ // Send vote response from node 0
+ VSMessage voteResponse1 = mock(VSMessage.class);
+ when(voteResponse1.getString("type")).thenReturn("VOTE_RESPONSE");
+ when(voteResponse1.getInteger("term")).thenReturn(1);
+ when(voteResponse1.getBoolean("voteGranted")).thenReturn(true);
+
+ // Mock sender process
+ VSInternalProcess mockSender1 = mock(VSInternalProcess.class);
+ when(mockSender1.getProcessNum()).thenReturn(0);
+ when(voteResponse1.getSendingProcess()).thenReturn(mockSender1);
+
+ protocol.onServerRecv(voteResponse1);
+
+ // Should become leader and highlight
+ verify(mockProcess).highlightOn();
+ verify(mockProcess, atLeastOnce()).log(contains("LEADER"));
+ }
+
+ @Test
+ void testHeartbeats() {
+ // Make node a leader
+ protocol.onServerInit();
+ protocol.isServer(true);
+
+ // Simulate becoming leader
+ when(mockProcess.getTime()).thenReturn(2000L);
+ protocol.onServerSchedule(); // Start election
+
+ // Get majority votes
+ VSMessage voteResponse = mock(VSMessage.class);
+ when(voteResponse.getString("type")).thenReturn("VOTE_RESPONSE");
+ when(voteResponse.getInteger("term")).thenReturn(1);
+ when(voteResponse.getBoolean("voteGranted")).thenReturn(true);
+
+ // Mock sender process
+ VSInternalProcess mockSender = mock(VSInternalProcess.class);
+ when(mockSender.getProcessNum()).thenReturn(0);
+ when(voteResponse.getSendingProcess()).thenReturn(mockSender);
+ protocol.onServerRecv(voteResponse);
+
+ // Clear previous invocations
+ clearInvocations(mockProcess);
+
+ // Trigger heartbeat
+ protocol.onServerSchedule();
+
+ // Should send append entries (heartbeats) to other nodes
+ ArgumentCaptor<VSMessage> heartbeatCaptor = ArgumentCaptor.forClass(VSMessage.class);
+ verify(mockProcess, atLeast(2)).sendMessage(heartbeatCaptor.capture());
+
+ boolean foundAppendEntries = false;
+ for (VSMessage msg : heartbeatCaptor.getAllValues()) {
+ if ("APPEND_ENTRIES".equals(msg.getString("type"))) {
+ foundAppendEntries = true;
+ assertEquals(1, msg.getInteger("term"));
+ assertEquals(1, msg.getInteger("leaderId"));
+ }
+ }
+ assertTrue(foundAppendEntries);
+ }
+
+ @Test
+ void testLogReplication() {
+ // Initialize as leader
+ protocol.onServerInit();
+ protocol.isServer(true);
+
+ // Become leader (simplified)
+ when(mockProcess.getTime()).thenReturn(2000L);
+ protocol.onServerSchedule();
+ VSMessage voteResponse = mock(VSMessage.class);
+ when(voteResponse.getString("type")).thenReturn("VOTE_RESPONSE");
+ when(voteResponse.getInteger("term")).thenReturn(1);
+ when(voteResponse.getBoolean("voteGranted")).thenReturn(true);
+
+ // Mock sender process
+ VSInternalProcess mockSender = mock(VSInternalProcess.class);
+ when(mockSender.getProcessNum()).thenReturn(0);
+ when(voteResponse.getSendingProcess()).thenReturn(mockSender);
+ protocol.onServerRecv(voteResponse);
+
+ // Client request
+ VSMessage clientRequest = mock(VSMessage.class);
+ when(clientRequest.getString("type")).thenReturn("CLIENT_REQUEST");
+ when(clientRequest.getString("command")).thenReturn("SET x=42");
+
+ // Mock sender process (client)
+ VSInternalProcess mockClient = mock(VSInternalProcess.class);
+ when(mockClient.getProcessNum()).thenReturn(2);
+ when(clientRequest.getSendingProcess()).thenReturn(mockClient);
+
+ protocol.onServerRecv(clientRequest);
+
+ // Should log the command
+ verify(mockProcess, atLeastOnce()).log(contains("SET x=42"));
+ }
+
+ @Test
+ void testFollowerRejectsClientRequests() {
+ // Initialize as follower
+ protocol.onServerInit();
+ protocol.isServer(true);
+
+ // Client request to follower
+ VSMessage clientRequest = mock(VSMessage.class);
+ when(clientRequest.getString("type")).thenReturn("CLIENT_REQUEST");
+ when(clientRequest.getString("command")).thenReturn("SET x=42");
+
+ // Mock sender process
+ VSInternalProcess mockClient = mock(VSInternalProcess.class);
+ when(mockClient.getProcessNum()).thenReturn(2);
+ when(clientRequest.getSendingProcess()).thenReturn(mockClient);
+
+ protocol.onServerRecv(clientRequest);
+
+ // Should send rejection response
+ ArgumentCaptor<VSMessage> responseCaptor = ArgumentCaptor.forClass(VSMessage.class);
+ verify(mockProcess).sendMessage(responseCaptor.capture());
+
+ VSMessage response = responseCaptor.getValue();
+ assertEquals("CLIENT_RESPONSE", response.getString("type"));
+ assertFalse(response.getBoolean("success"));
+ }
+
+ @Test
+ void testClientBehavior() {
+ // Test client side
+ protocol.isClient(true);
+ protocol.onClientInit();
+
+ // Mock scheduled task addition
+ doNothing().when(mockTaskManager).addTask(any());
+
+ protocol.onClientStart();
+
+ // Should schedule client requests
+ verify(mockProcess).getTime();
+
+ // Simulate scheduled client request
+ protocol.onClientSchedule();
+
+ // Should send client request
+ ArgumentCaptor<VSMessage> requestCaptor = ArgumentCaptor.forClass(VSMessage.class);
+ verify(mockProcess).sendMessage(requestCaptor.capture());
+
+ VSMessage request = requestCaptor.getValue();
+ assertEquals("CLIENT_REQUEST", request.getString("type"));
+ assertNotNull(request.getString("command"));
+ }
+
+ @Test
+ void testTermUpdate() {
+ // Initialize protocol
+ protocol.onServerInit();
+ protocol.isServer(true);
+
+ // Receive message with higher term
+ VSMessage higherTermMsg = mock(VSMessage.class);
+ when(higherTermMsg.getString("type")).thenReturn("APPEND_ENTRIES");
+ when(higherTermMsg.getInteger("term")).thenReturn(5);
+ when(higherTermMsg.getInteger("leaderId")).thenReturn(2);
+ when(higherTermMsg.getInteger("prevLogIndex")).thenReturn(0);
+ when(higherTermMsg.getInteger("prevLogTerm")).thenReturn(0);
+ when(higherTermMsg.getInteger("leaderCommit")).thenReturn(0);
+ when(higherTermMsg.getInteger("entryCount")).thenReturn(0);
+
+ // Mock sender process
+ VSInternalProcess mockLeader = mock(VSInternalProcess.class);
+ when(mockLeader.getProcessNum()).thenReturn(2);
+ when(higherTermMsg.getSendingProcess()).thenReturn(mockLeader);
+
+ protocol.onServerRecv(higherTermMsg);
+
+ // Should become follower (no longer logs in onServerRecv)
+ // Just verify the message was processed correctly by checking response
+ ArgumentCaptor<VSMessage> responseCaptor = ArgumentCaptor.forClass(VSMessage.class);
+ verify(mockProcess).sendMessage(responseCaptor.capture());
+
+ VSMessage response = responseCaptor.getValue();
+ assertEquals("APPEND_RESPONSE", response.getString("type"));
+ }
+} \ No newline at end of file