|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one or more |
| 3 | + * contributor license agreements. See the NOTICE file distributed with |
| 4 | + * this work for additional information regarding copyright ownership. |
| 5 | + * The ASF licenses this file to You under the Apache License, Version 2.0 |
| 6 | + * (the "License"); you may not use this file except in compliance with |
| 7 | + * the License. You may obtain a copy of the License at |
| 8 | + * |
| 9 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | + * |
| 11 | + * Unless required by applicable law or agreed to in writing, software |
| 12 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | + * See the License for the specific language governing permissions and |
| 15 | + * limitations under the License. |
| 16 | + */ |
| 17 | +package com.alipay.sofa.jraft.core; |
| 18 | + |
| 19 | +import java.io.File; |
| 20 | +import java.util.LinkedHashSet; |
| 21 | +import java.util.List; |
| 22 | + |
| 23 | +import org.apache.commons.io.FileUtils; |
| 24 | +import org.junit.After; |
| 25 | +import org.junit.Before; |
| 26 | +import org.junit.Rule; |
| 27 | +import org.junit.Test; |
| 28 | +import org.junit.rules.TestName; |
| 29 | + |
| 30 | +import com.alipay.sofa.jraft.NodeManager; |
| 31 | +import com.alipay.sofa.jraft.entity.PeerId; |
| 32 | +import com.alipay.sofa.jraft.option.RaftMetaStorageOptions; |
| 33 | +import com.alipay.sofa.jraft.option.RaftOptions; |
| 34 | +import com.alipay.sofa.jraft.rpc.RpcRequests.RequestVoteRequest; |
| 35 | +import com.alipay.sofa.jraft.rpc.RpcRequests.RequestVoteResponse; |
| 36 | +import com.alipay.sofa.jraft.storage.RaftMetaStorage; |
| 37 | +import com.alipay.sofa.jraft.storage.impl.LocalRaftMetaStorage; |
| 38 | +import com.alipay.sofa.jraft.test.TestUtils; |
| 39 | + |
| 40 | +import static org.junit.Assert.*; |
| 41 | + |
| 42 | +/** |
| 43 | + * Test for unchecked setVotedFor() return value in handleRequestVoteRequest(). |
| 44 | + * |
| 45 | + * handleRequestVoteRequest() does not check the return value of |
| 46 | + * metaStorage.setVotedFor(). If the disk write fails (I/O error) but the |
| 47 | + * process survives, the response is still granted=true because the in-memory |
| 48 | + * votedId was already set before the disk write. After restart the vote is |
| 49 | + * lost, allowing a different candidate to be granted in the same term. |
| 50 | + */ |
| 51 | +public class VotePersistenceBugTest { |
| 52 | + |
| 53 | + @Rule |
| 54 | + public TestName testName = new TestName(); |
| 55 | + |
| 56 | + private String dataPath; |
| 57 | + |
| 58 | + @Before |
| 59 | + public void setup() throws Exception { |
| 60 | + this.dataPath = TestUtils.mkTempDir(); |
| 61 | + FileUtils.forceMkdir(new File(this.dataPath)); |
| 62 | + assertEquals(NodeImpl.GLOBAL_NUM_NODES.get(), 0); |
| 63 | + } |
| 64 | + |
| 65 | + @After |
| 66 | + public void teardown() throws Exception { |
| 67 | + if (!TestCluster.CLUSTERS.isEmpty()) { |
| 68 | + for (final TestCluster c : TestCluster.CLUSTERS.removeAll()) { |
| 69 | + c.stopAll(); |
| 70 | + } |
| 71 | + } |
| 72 | + FileUtils.deleteDirectory(new File(this.dataPath)); |
| 73 | + NodeManager.getInstance().clear(); |
| 74 | + } |
| 75 | + |
| 76 | + // Wraps LocalRaftMetaStorage. When failNextSetVotedFor is true, makes the |
| 77 | + // meta directory read-only before delegating setVotedFor() so that the real |
| 78 | + // save() fails with an I/O error, triggering the full reportIOError() -> |
| 79 | + // node.onError() path. |
| 80 | + static class DiskFailureMetaStorage implements RaftMetaStorage { |
| 81 | + private final LocalRaftMetaStorage delegate; |
| 82 | + private final String metaDir; |
| 83 | + volatile boolean failNextSetVotedFor = false; |
| 84 | + |
| 85 | + DiskFailureMetaStorage(final String uri, final RaftOptions raftOptions) { |
| 86 | + this.delegate = new LocalRaftMetaStorage(uri, raftOptions); |
| 87 | + this.metaDir = uri; |
| 88 | + } |
| 89 | + |
| 90 | + @Override |
| 91 | + public boolean init(final RaftMetaStorageOptions opts) { |
| 92 | + return delegate.init(opts); |
| 93 | + } |
| 94 | + |
| 95 | + @Override |
| 96 | + public void shutdown() { |
| 97 | + delegate.shutdown(); |
| 98 | + } |
| 99 | + |
| 100 | + @Override |
| 101 | + public boolean setTerm(final long term) { |
| 102 | + return delegate.setTerm(term); |
| 103 | + } |
| 104 | + |
| 105 | + @Override |
| 106 | + public long getTerm() { |
| 107 | + return delegate.getTerm(); |
| 108 | + } |
| 109 | + |
| 110 | + @Override |
| 111 | + public PeerId getVotedFor() { |
| 112 | + return delegate.getVotedFor(); |
| 113 | + } |
| 114 | + |
| 115 | + @Override |
| 116 | + public boolean setTermAndVotedFor(final long term, final PeerId peerId) { |
| 117 | + return delegate.setTermAndVotedFor(term, peerId); |
| 118 | + } |
| 119 | + |
| 120 | + @Override |
| 121 | + public boolean setVotedFor(final PeerId peerId) { |
| 122 | + if (failNextSetVotedFor) { |
| 123 | + final File dir = new File(metaDir); |
| 124 | + dir.setWritable(false); |
| 125 | + try { |
| 126 | + return delegate.setVotedFor(peerId); |
| 127 | + } finally { |
| 128 | + dir.setWritable(true); |
| 129 | + } |
| 130 | + } |
| 131 | + return delegate.setVotedFor(peerId); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + static class DiskFailureServiceFactory extends TestJRaftServiceFactory { |
| 136 | + volatile DiskFailureMetaStorage storage; |
| 137 | + |
| 138 | + @Override |
| 139 | + public RaftMetaStorage createRaftMetaStorage(final String uri, final RaftOptions raftOptions) { |
| 140 | + storage = new DiskFailureMetaStorage(uri, raftOptions); |
| 141 | + return storage; |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + private RequestVoteRequest buildVoteRequest(final PeerId candidate, final PeerId voter, final long term) { |
| 146 | + return RequestVoteRequest.newBuilder().setGroupId("unittest").setServerId(candidate.toString()) |
| 147 | + .setPeerId(voter.toString()).setTerm(term).setLastLogIndex(999_999).setLastLogTerm(999_999) |
| 148 | + .setPreVote(false).build(); |
| 149 | + } |
| 150 | + |
| 151 | + // Control: normal restart preserves vote, second candidate rejected. |
| 152 | + @Test |
| 153 | + public void testVoteRejectedAfterNormalRestart() throws Exception { |
| 154 | + final List<PeerId> peers = TestUtils.generatePeers(3); |
| 155 | + final PeerId voter = peers.get(0); |
| 156 | + final PeerId candidate1 = peers.get(1); |
| 157 | + final PeerId candidate2 = peers.get(2); |
| 158 | + |
| 159 | + final TestCluster cluster = new TestCluster("unittest", this.dataPath, peers, new LinkedHashSet<>(), 600_000); |
| 160 | + assertTrue(cluster.start(voter.getEndpoint())); |
| 161 | + Thread.sleep(1000); |
| 162 | + |
| 163 | + final NodeImpl node1 = (NodeImpl) cluster.getNodes().get(0); |
| 164 | + final RequestVoteResponse resp1 = (RequestVoteResponse) node1.handleRequestVoteRequest(buildVoteRequest( |
| 165 | + candidate1, voter, 100)); |
| 166 | + assertTrue(resp1.getGranted()); |
| 167 | + |
| 168 | + cluster.stop(voter.getEndpoint()); |
| 169 | + Thread.sleep(200); |
| 170 | + assertTrue(cluster.start(voter.getEndpoint())); |
| 171 | + Thread.sleep(1000); |
| 172 | + |
| 173 | + // after normal restart, vote for candidate2 in the same term should be rejected |
| 174 | + final NodeImpl node2 = (NodeImpl) cluster.getNodes().get(0); |
| 175 | + final RequestVoteResponse resp2 = (RequestVoteResponse) node2.handleRequestVoteRequest(buildVoteRequest( |
| 176 | + candidate2, voter, 100)); |
| 177 | + assertFalse(resp2.getGranted()); |
| 178 | + |
| 179 | + cluster.stopAll(); |
| 180 | + } |
| 181 | + |
| 182 | + // setVotedFor() I/O failure: the real LocalRaftMetaStorage.save() fails |
| 183 | + // because the meta directory is made read-only. This triggers the full |
| 184 | + // reportIOError() -> node.onError() -> STATE_ERROR path. Despite this, |
| 185 | + // handleRequestVoteRequest() still returns granted=true because the |
| 186 | + // return value of setVotedFor() is not checked. |
| 187 | + @Test |
| 188 | + public void testDoubleVoteAfterSetVotedForIOFailure() throws Exception { |
| 189 | + final List<PeerId> peers = TestUtils.generatePeers(3); |
| 190 | + final PeerId voter = peers.get(0); |
| 191 | + final PeerId candidate1 = peers.get(1); |
| 192 | + final PeerId candidate2 = peers.get(2); |
| 193 | + |
| 194 | + final DiskFailureServiceFactory factory = new DiskFailureServiceFactory(); |
| 195 | + final TestCluster cluster = new TestCluster("unittest", this.dataPath, peers, new LinkedHashSet<>(), 600_000); |
| 196 | + cluster.setRaftServiceFactory(factory); |
| 197 | + assertTrue(cluster.start(voter.getEndpoint())); |
| 198 | + Thread.sleep(1000); |
| 199 | + |
| 200 | + // enable I/O failure for the next setVotedFor() call |
| 201 | + factory.storage.failNextSetVotedFor = true; |
| 202 | + |
| 203 | + // vote for candidate1; setVotedFor() will hit a real I/O error |
| 204 | + final NodeImpl node1 = (NodeImpl) cluster.getNodes().get(0); |
| 205 | + final RequestVoteResponse resp1 = (RequestVoteResponse) node1.handleRequestVoteRequest(buildVoteRequest( |
| 206 | + candidate1, voter, 100)); |
| 207 | + // response is granted=true despite the disk write failure |
| 208 | + assertTrue(resp1.getGranted()); |
| 209 | + |
| 210 | + // stop and restart with normal storage |
| 211 | + cluster.stop(voter.getEndpoint()); |
| 212 | + Thread.sleep(200); |
| 213 | + cluster.setRaftServiceFactory(new TestJRaftServiceFactory()); |
| 214 | + assertTrue(cluster.start(voter.getEndpoint())); |
| 215 | + Thread.sleep(1000); |
| 216 | + |
| 217 | + // vote for candidate2 in the same term — should be rejected |
| 218 | + final NodeImpl node2 = (NodeImpl) cluster.getNodes().get(0); |
| 219 | + final RequestVoteResponse resp2 = (RequestVoteResponse) node2.handleRequestVoteRequest(buildVoteRequest( |
| 220 | + candidate2, voter, 100)); |
| 221 | + assertFalse("double-vote: node voted for both " + candidate1 + " and " + candidate2 + " in term 100", |
| 222 | + resp2.getGranted()); |
| 223 | + |
| 224 | + cluster.stopAll(); |
| 225 | + } |
| 226 | +} |
0 commit comments