Skip to content

Commit 0b9a695

Browse files
Add test for unchecked setVotedFor() return value in handleRequestVoteRequest
1 parent eccaec1 commit 0b9a695

1 file changed

Lines changed: 226 additions & 0 deletions

File tree

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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

Comments
 (0)