diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-27 13:30:14 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-27 13:30:14 +0200 |
| commit | 0bebec08cd89039c32bd9b9e73d80d573b6bf0b3 (patch) | |
| tree | 429222b73e259a3f1e3f2d9d229164a4841b41f3 | |
| parent | 35def2831acd67ace6943e06f502a356529c3357 (diff) | |
sr: fix Raft replay leader election
| -rw-r--r-- | saved-simulations/raft.dat | bin | 14533 -> 14533 bytes | |||
| -rw-r--r-- | src/main/java/protocols/VSAbstractProtocol.java | 31 | ||||
| -rw-r--r-- | src/main/java/protocols/implementations/VSRaftProtocol.java | 14 | ||||
| -rw-r--r-- | src/main/java/simulator/builder/SimulationFactory.java | 8 | ||||
| -rw-r--r-- | src/test/java/protocols/implementations/VSRaftProtocolTest.java | 41 |
5 files changed, 89 insertions, 5 deletions
diff --git a/saved-simulations/raft.dat b/saved-simulations/raft.dat Binary files differindex 5738148..262809a 100644 --- a/saved-simulations/raft.dat +++ b/saved-simulations/raft.dat diff --git a/src/main/java/protocols/VSAbstractProtocol.java b/src/main/java/protocols/VSAbstractProtocol.java index da12d31..ee0d6c2 100644 --- a/src/main/java/protocols/VSAbstractProtocol.java +++ b/src/main/java/protocols/VSAbstractProtocol.java @@ -180,8 +180,8 @@ abstract public class VSAbstractProtocol extends VSAbstractEvent { * This method: * <ul> * <li>Filters out messages for other protocols</li> - * <li>Ensures server/client initialization</li> - * <li>Delegates to onServerRecv() or onClientRecv() based on context</li> + * <li>Routes messages to the active role(s) without double-delivering + * to dual-role peers</li> * </ul> * * @param message the received message @@ -192,6 +192,21 @@ abstract public class VSAbstractProtocol extends VSAbstractEvent { if (isIncorrectProtocol(message)) return; + if (isServer && isClient) { + if (message.isServerMessage()) { + currentContextIsServer(false); + if (!isClientInitialized) + onInit(); + onClientRecv(message); + } else { + currentContextIsServer(true); + if (!isServerInitialized) + onInit(); + onServerRecv(message); + } + return; + } + if (isServer) { currentContextIsServer(true); if (!isServerInitialized) @@ -226,6 +241,18 @@ abstract public class VSAbstractProtocol extends VSAbstractEvent { if (isIncorrectProtocol(message)) return false; + return isRelevantMessageForContext(message); + } + + /** + * Checks whether a message is relevant for this protocol instance. + * Subclasses can relax or specialize the default server/client routing + * rules while keeping the protocol-name filter intact. + * + * @param message the message to check + * @return true if the message should be processed by this protocol instance + */ + protected boolean isRelevantMessageForContext(VSMessage message) { if (message.isServerMessage()) { if (!isClient) return false; diff --git a/src/main/java/protocols/implementations/VSRaftProtocol.java b/src/main/java/protocols/implementations/VSRaftProtocol.java index c75628d..bad893c 100644 --- a/src/main/java/protocols/implementations/VSRaftProtocol.java +++ b/src/main/java/protocols/implementations/VSRaftProtocol.java @@ -103,6 +103,11 @@ public class VSRaftProtocol extends VSAbstractProtocol { handleMessage(recvMessage); } + @Override + protected boolean isRelevantMessageForContext(VSMessage message) { + return isServer() || isClient(); + } + /* (non-Javadoc) * @see protocols.VSAbstractProtocol#onServerSchedule() */ @@ -366,7 +371,10 @@ public class VSRaftProtocol extends VSAbstractProtocol { heartbeatAck.setInteger("term", currentTerm); heartbeatAck.setInteger("pid", process.getProcessID()); heartbeatAck.setInteger("targetPid", messageLeaderId); + boolean previousContextIsServer = currentContextIsServer(); + currentContextIsServer(true); sendMessage(heartbeatAck); + currentContextIsServer(previousContextIsServer); } /** @@ -417,7 +425,10 @@ public class VSRaftProtocol extends VSAbstractProtocol { voteResponse.setInteger("pid", process.getProcessID()); voteResponse.setBoolean("voteGranted", voteGranted); voteResponse.setInteger("targetPid", candidateId); + boolean previousContextIsServer = currentContextIsServer(); + currentContextIsServer(true); sendMessage(voteResponse); + currentContextIsServer(previousContextIsServer); } /** @@ -487,7 +498,10 @@ public class VSRaftProtocol extends VSAbstractProtocol { appendAck.setInteger("pid", process.getProcessID()); appendAck.setInteger("logIndex", messageLogIndex); appendAck.setInteger("targetPid", messageLeaderId); + boolean previousContextIsServer = currentContextIsServer(); + currentContextIsServer(true); sendMessage(appendAck); + currentContextIsServer(previousContextIsServer); } /** diff --git a/src/main/java/simulator/builder/SimulationFactory.java b/src/main/java/simulator/builder/SimulationFactory.java index 48ec638..165e42d 100644 --- a/src/main/java/simulator/builder/SimulationFactory.java +++ b/src/main/java/simulator/builder/SimulationFactory.java @@ -96,10 +96,12 @@ public class SimulationFactory { .activateClientsAt(100, 1) .activateClientsAt(1700, 2) // Bias process 1 toward a fast, clean post-crash election while - // keeping process 2's timeout comfortably behind it. - .setProtocolLong(1, "electionTimeout", 4000) + // keeping process 2's timeout comfortably behind it. The shorter + // timeout also keeps the headless replay active long enough to + // reach the first post-crash election deterministically. + .setProtocolLong(1, "electionTimeout", 2500) .setProtocolLong(1, "electionJitter", 0) - .setProtocolLong(2, "electionTimeout", 9000) + .setProtocolLong(2, "electionTimeout", 12000) .setProtocolLong(2, "electionJitter", 0) .addCrashEvent(0, 3500); } diff --git a/src/test/java/protocols/implementations/VSRaftProtocolTest.java b/src/test/java/protocols/implementations/VSRaftProtocolTest.java index 40dfd10..802c80f 100644 --- a/src/test/java/protocols/implementations/VSRaftProtocolTest.java +++ b/src/test/java/protocols/implementations/VSRaftProtocolTest.java @@ -390,6 +390,33 @@ class VSRaftProtocolTest { } @Test + void testDualRoleVoteRequestIsHandledOnce() throws Exception { + protocol.currentContextIsServer(false); + protocol.isClient(true); + protocol.isServer(true); + protocol.onInit(); + clearInvocations(mockProcess, mockTaskManager); + when(mockProcess.getTime()).thenReturn(200L, 200L); + + VSMessage voteRequest = new VSMessage(); + setMessageProtocolClassname(voteRequest, VSRaftProtocol.class.getName()); + setMessageServerFlag(voteRequest, true); + voteRequest.setString("type", "voteRequest"); + voteRequest.setInteger("term", 2); + voteRequest.setInteger("candidateId", 11); + + ArgumentCaptor<VSMessage> messageCaptor = + ArgumentCaptor.forClass(VSMessage.class); + + protocol.onMessageRecvStart(voteRequest); + + verify(mockProcess).sendMessage(messageCaptor.capture()); + assertEquals("voteResponse", messageCaptor.getValue().getString("type")); + assertEquals(2, messageCaptor.getValue().getInteger("term")); + assertTrue(messageCaptor.getValue().getBoolean("voteGranted")); + } + + @Test void testClientReceiveHeartbeatBecomesFollowerResetsTimeoutAndSendsAck() throws Exception { protocol.currentContextIsServer(false); @@ -764,6 +791,20 @@ class VSRaftProtocolTest { field.setBoolean(protocol, value); } + private void setMessageProtocolClassname(VSMessage message, String classname) + throws Exception { + Field field = VSMessage.class.getDeclaredField("protocolClassname"); + field.setAccessible(true); + field.set(message, classname); + } + + private void setMessageServerFlag(VSMessage message, boolean isServerMessage) + throws Exception { + Field field = VSMessage.class.getDeclaredField("isServerMessage"); + field.setAccessible(true); + field.setBoolean(message, isServerMessage); + } + private int getIntField(String fieldName) throws Exception { Field field = VSRaftProtocol.class.getDeclaredField(fieldName); field.setAccessible(true); |
