Skip to content

RATIS-1446. Avoid leader election for invalid conf - #560

Merged
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446
Dec 15, 2021
Merged

RATIS-1446. Avoid leader election for invalid conf#560
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446

Conversation

@Xushaohong

@XushaohongXushaohong commented Dec 9, 2021

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Candidate shall not start leader election in these cases in case of possible NPE caused by conf.getPeer().getPriority().
With this patch, the candidate will become a follower again when receiving the future appendEntries request.

Phenomenon:
The bootstrapped follower becomes the leader after the election timeout somehow, with an empty raft conf. Then in the process of yieldLeaderToHigherPriorityPeer, NPE happens as (null).getPriority().
Setconfiguration action takes place at the end of appendEntriesAsync. If we make it possible to raise electiontimeout before this action, we can replay the NPE case.

1private void yieldLeaderToHigherPriorityPeer() {
2 if (!server.getInfo().isLeader()) {
3 return;
4 }
5 final RaftConfigurationImpl conf = server.getRaftConf();
6 int leaderPriority = conf.getPeer(server.getId()).getPriority(); 

Possible reason:
Network delay/loss? The leader did not send entries in time or the follower did not receive entries.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/RATIS-1446

How was this patch tested?

Manual simulation in UT.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@szetszwo Actually, I am not sure whether we should make a specific check for each getPriority() use. Right now, I just prevent the NPE from the source. Currently, conf.getPeer().getPriority() only exists in leader election and leader state.

Comment on lines +586 to +591
// Candidate shall not start leader election in these cases in case of
// possible NPE caused by conf.getPeer().getPriority()
if (!getRaftConf().containsInBothConfs(getId())) {
LOG.warn("{} find invalid configuration {}, skip start leader election", this, getRaftConf());
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

 private Set<RaftPeerId> getHigherPriorityPeers(RaftConfiguration conf) {
final Optional<Integer> priority = Optional.ofNullable(conf.getPeer(server.getId())).map(RaftPeer::getPriority);
return conf.getAllPeers().stream()
.filter(peer -> priority.filter(p -> peer.getPriority() > p).isPresent())
.map(RaftPeer::getId)
.collect(Collectors.toSet());
}

@szetszwo

Copy link
Copy Markdown
Contributor

..., I am not sure whether we should make a specific check for each getPriority() use.

That's a good point. We should check. I just have walked through the code. There are two other cases needed to be fixed:

diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
index 03850114..d1413040 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
@@ -916,13 +916,13 @@ class LeaderStateImpl implements LeaderState {
for (LogAppender logAppender : senders.getSenders()) {
FollowerInfo followerInfo = logAppender.getFollower();
- RaftPeerId followerID = followerInfo.getPeer().getId();
- int followerPriority = conf.getPeer(followerID).getPriority();
-
+ final RaftPeer follower = followerInfo.getPeer();
+ final int followerPriority = follower.getPriority();
if (followerPriority <= leaderPriority) {
continue;
}
+ final RaftPeerId followerID = follower.getId();
final TermIndex leaderLastEntry = server.getState().getLastEntry();
if (leaderLastEntry == null) {
LOG.info("{} send StartLeaderElectionRequest to follower:{} on term:{} because follower's priority:{} " +
diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
index 1ef721aa..fde198fe 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
@@ -144,7 +144,11 @@ class VoteContext {
}
// Check priority
- final int priority = impl.getRaftConf().getPeer(impl.getId()).getPriority();
+ final RaftPeer peer = conf.getPeer(impl.getId());
+ if (peer == null) {
+ return reject("our server " + impl.getId() + " is not in the conf");
+ }
+ final int priority = peer.getPriority();
if (priority <= candidate.getPriority()) {
return log(true, "our priority " + priority + " <= candidate's priority " + candidate.getPriority());
} else {

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

The check here in the getHigherPriorityPeers is not enough. The possible case I tested is that bootstrapped follower changes role from follower to candidate and start leader election, as it has an empty conf, it will become leader directly due to if (others.isEmpty()) { r = new ResultAndTerm(Result.PASSED, electionTerm);. Once it becomes another leader, it won't receive any requests from the original leader as the term is increased. Meanwhile, the original leader stuck in sending appendEntries to 'follower'.

I sightly changed the check place. PTAL
@szetszwo

@szetszwo

Copy link
Copy Markdown
Contributor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

The bootstrapped follower could start with empty conf and set conf through leader's appendEntries request or Installsnapshot request. Somehow caused election timeout would let this follower start leader with empty conf.
@szetszwo

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional changes are good although the empty-conf test case is invalid. Some comments inlined; see also https://issues.apache.org/jira/secure/attachment/13037236/560_reivew.patch

Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
@Xushaohong

Xushaohong commented Dec 10, 2021

Copy link
Copy Markdown
ContributorAuthor

Thanks for all the comments above. I have fixed all. @szetszwo

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

@szetszwo

Copy link
Copy Markdown
Contributor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

Currently, the setConf request would not update the FollowerInfo in LogAppenderBase, so we have to get the latest followerInfo from conf like the previous version. I have reverted it back and added an NPE check.
@szetszwo Could you help confirm it?

@szetszwo

Copy link
Copy Markdown
Contributor

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

We should fix the RaftReconfigurationBaseTest but not having the phase check.


private ResultAndTerm submitRequestAndWaitResult(Phase phase, RaftConfigurationImpl conf, long electionTerm)
throws InterruptedException {
if (!conf.containsInConf(server.getId()) && phase == Phase.ELECTION) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

@XushaohongXushaohongDec 13, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

OK, let's find out what's wrong with UT. @szetszwo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help approve the workflow? : )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xushaohong , the build finished. Please take a look.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

I reviewed the failure UT of testRemovePeers. The fault is due to after removing the peers out of the conf, the division which should quit the raft group will become a candidate and start the election. During the prevote process, originally it will send out the request and then the other peers should send back should shutdown to turn this server down. But for our case, we just return new ResultAndTerm(Result.NOT_IN_CONF), and won't go to the SHUTDOWN branch.
@szetszwo Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

@szetszwo

Copy link
Copy Markdown
Contributor

... Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

This is a good idea. Let's shutdown the server when it is not in conf. Thanks.

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Xushaohong@szetszwo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
RATIS-1446. Avoid leader election for invalid conf by Xushaohong · Pull Request #560 · apache/ratis · GitHub
Skip to content

RATIS-1446. Avoid leader election for invalid conf - #560

Merged
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446
Dec 15, 2021
Merged

RATIS-1446. Avoid leader election for invalid conf#560
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446

Conversation

@Xushaohong

@XushaohongXushaohong commented Dec 9, 2021

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Candidate shall not start leader election in these cases in case of possible NPE caused by conf.getPeer().getPriority().
With this patch, the candidate will become a follower again when receiving the future appendEntries request.

Phenomenon:
The bootstrapped follower becomes the leader after the election timeout somehow, with an empty raft conf. Then in the process of yieldLeaderToHigherPriorityPeer, NPE happens as (null).getPriority().
Setconfiguration action takes place at the end of appendEntriesAsync. If we make it possible to raise electiontimeout before this action, we can replay the NPE case.

1private void yieldLeaderToHigherPriorityPeer() {
2 if (!server.getInfo().isLeader()) {
3 return;
4 }
5 final RaftConfigurationImpl conf = server.getRaftConf();
6 int leaderPriority = conf.getPeer(server.getId()).getPriority(); 

Possible reason:
Network delay/loss? The leader did not send entries in time or the follower did not receive entries.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/RATIS-1446

How was this patch tested?

Manual simulation in UT.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@szetszwo Actually, I am not sure whether we should make a specific check for each getPriority() use. Right now, I just prevent the NPE from the source. Currently, conf.getPeer().getPriority() only exists in leader election and leader state.

Comment on lines +586 to +591
// Candidate shall not start leader election in these cases in case of
// possible NPE caused by conf.getPeer().getPriority()
if (!getRaftConf().containsInBothConfs(getId())) {
LOG.warn("{} find invalid configuration {}, skip start leader election", this, getRaftConf());
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

 private Set<RaftPeerId> getHigherPriorityPeers(RaftConfiguration conf) {
final Optional<Integer> priority = Optional.ofNullable(conf.getPeer(server.getId())).map(RaftPeer::getPriority);
return conf.getAllPeers().stream()
.filter(peer -> priority.filter(p -> peer.getPriority() > p).isPresent())
.map(RaftPeer::getId)
.collect(Collectors.toSet());
}

@szetszwo

Copy link
Copy Markdown
Contributor

..., I am not sure whether we should make a specific check for each getPriority() use.

That's a good point. We should check. I just have walked through the code. There are two other cases needed to be fixed:

diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
index 03850114..d1413040 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
@@ -916,13 +916,13 @@ class LeaderStateImpl implements LeaderState {
for (LogAppender logAppender : senders.getSenders()) {
FollowerInfo followerInfo = logAppender.getFollower();
- RaftPeerId followerID = followerInfo.getPeer().getId();
- int followerPriority = conf.getPeer(followerID).getPriority();
-
+ final RaftPeer follower = followerInfo.getPeer();
+ final int followerPriority = follower.getPriority();
if (followerPriority <= leaderPriority) {
continue;
}
+ final RaftPeerId followerID = follower.getId();
final TermIndex leaderLastEntry = server.getState().getLastEntry();
if (leaderLastEntry == null) {
LOG.info("{} send StartLeaderElectionRequest to follower:{} on term:{} because follower's priority:{} " +
diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
index 1ef721aa..fde198fe 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
@@ -144,7 +144,11 @@ class VoteContext {
}
// Check priority
- final int priority = impl.getRaftConf().getPeer(impl.getId()).getPriority();
+ final RaftPeer peer = conf.getPeer(impl.getId());
+ if (peer == null) {
+ return reject("our server " + impl.getId() + " is not in the conf");
+ }
+ final int priority = peer.getPriority();
if (priority <= candidate.getPriority()) {
return log(true, "our priority " + priority + " <= candidate's priority " + candidate.getPriority());
} else {

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

The check here in the getHigherPriorityPeers is not enough. The possible case I tested is that bootstrapped follower changes role from follower to candidate and start leader election, as it has an empty conf, it will become leader directly due to if (others.isEmpty()) { r = new ResultAndTerm(Result.PASSED, electionTerm);. Once it becomes another leader, it won't receive any requests from the original leader as the term is increased. Meanwhile, the original leader stuck in sending appendEntries to 'follower'.

I sightly changed the check place. PTAL
@szetszwo

@szetszwo

Copy link
Copy Markdown
Contributor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

The bootstrapped follower could start with empty conf and set conf through leader's appendEntries request or Installsnapshot request. Somehow caused election timeout would let this follower start leader with empty conf.
@szetszwo

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional changes are good although the empty-conf test case is invalid. Some comments inlined; see also https://issues.apache.org/jira/secure/attachment/13037236/560_reivew.patch

Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
@Xushaohong

Xushaohong commented Dec 10, 2021

Copy link
Copy Markdown
ContributorAuthor

Thanks for all the comments above. I have fixed all. @szetszwo

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

@szetszwo

Copy link
Copy Markdown
Contributor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

Currently, the setConf request would not update the FollowerInfo in LogAppenderBase, so we have to get the latest followerInfo from conf like the previous version. I have reverted it back and added an NPE check.
@szetszwo Could you help confirm it?

@szetszwo

Copy link
Copy Markdown
Contributor

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

We should fix the RaftReconfigurationBaseTest but not having the phase check.


private ResultAndTerm submitRequestAndWaitResult(Phase phase, RaftConfigurationImpl conf, long electionTerm)
throws InterruptedException {
if (!conf.containsInConf(server.getId()) && phase == Phase.ELECTION) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

@XushaohongXushaohongDec 13, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

OK, let's find out what's wrong with UT. @szetszwo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help approve the workflow? : )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xushaohong , the build finished. Please take a look.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

I reviewed the failure UT of testRemovePeers. The fault is due to after removing the peers out of the conf, the division which should quit the raft group will become a candidate and start the election. During the prevote process, originally it will send out the request and then the other peers should send back should shutdown to turn this server down. But for our case, we just return new ResultAndTerm(Result.NOT_IN_CONF), and won't go to the SHUTDOWN branch.
@szetszwo Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

@szetszwo

Copy link
Copy Markdown
Contributor

... Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

This is a good idea. Let's shutdown the server when it is not in conf. Thanks.

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Xushaohong@szetszwo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' RATIS-1446. Avoid leader election for invalid conf by Xushaohong · Pull Request #560 · apache/ratis · GitHub
Skip to content

RATIS-1446. Avoid leader election for invalid conf - #560

Merged
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446
Dec 15, 2021
Merged

RATIS-1446. Avoid leader election for invalid conf#560
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446

Conversation

@Xushaohong

@XushaohongXushaohong commented Dec 9, 2021

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Candidate shall not start leader election in these cases in case of possible NPE caused by conf.getPeer().getPriority().
With this patch, the candidate will become a follower again when receiving the future appendEntries request.

Phenomenon:
The bootstrapped follower becomes the leader after the election timeout somehow, with an empty raft conf. Then in the process of yieldLeaderToHigherPriorityPeer, NPE happens as (null).getPriority().
Setconfiguration action takes place at the end of appendEntriesAsync. If we make it possible to raise electiontimeout before this action, we can replay the NPE case.

1private void yieldLeaderToHigherPriorityPeer() {
2 if (!server.getInfo().isLeader()) {
3 return;
4 }
5 final RaftConfigurationImpl conf = server.getRaftConf();
6 int leaderPriority = conf.getPeer(server.getId()).getPriority(); 

Possible reason:
Network delay/loss? The leader did not send entries in time or the follower did not receive entries.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/RATIS-1446

How was this patch tested?

Manual simulation in UT.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@szetszwo Actually, I am not sure whether we should make a specific check for each getPriority() use. Right now, I just prevent the NPE from the source. Currently, conf.getPeer().getPriority() only exists in leader election and leader state.

Comment on lines +586 to +591
// Candidate shall not start leader election in these cases in case of
// possible NPE caused by conf.getPeer().getPriority()
if (!getRaftConf().containsInBothConfs(getId())) {
LOG.warn("{} find invalid configuration {}, skip start leader election", this, getRaftConf());
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

 private Set<RaftPeerId> getHigherPriorityPeers(RaftConfiguration conf) {
final Optional<Integer> priority = Optional.ofNullable(conf.getPeer(server.getId())).map(RaftPeer::getPriority);
return conf.getAllPeers().stream()
.filter(peer -> priority.filter(p -> peer.getPriority() > p).isPresent())
.map(RaftPeer::getId)
.collect(Collectors.toSet());
}

@szetszwo

Copy link
Copy Markdown
Contributor

..., I am not sure whether we should make a specific check for each getPriority() use.

That's a good point. We should check. I just have walked through the code. There are two other cases needed to be fixed:

diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
index 03850114..d1413040 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
@@ -916,13 +916,13 @@ class LeaderStateImpl implements LeaderState {
for (LogAppender logAppender : senders.getSenders()) {
FollowerInfo followerInfo = logAppender.getFollower();
- RaftPeerId followerID = followerInfo.getPeer().getId();
- int followerPriority = conf.getPeer(followerID).getPriority();
-
+ final RaftPeer follower = followerInfo.getPeer();
+ final int followerPriority = follower.getPriority();
if (followerPriority <= leaderPriority) {
continue;
}
+ final RaftPeerId followerID = follower.getId();
final TermIndex leaderLastEntry = server.getState().getLastEntry();
if (leaderLastEntry == null) {
LOG.info("{} send StartLeaderElectionRequest to follower:{} on term:{} because follower's priority:{} " +
diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
index 1ef721aa..fde198fe 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
@@ -144,7 +144,11 @@ class VoteContext {
}
// Check priority
- final int priority = impl.getRaftConf().getPeer(impl.getId()).getPriority();
+ final RaftPeer peer = conf.getPeer(impl.getId());
+ if (peer == null) {
+ return reject("our server " + impl.getId() + " is not in the conf");
+ }
+ final int priority = peer.getPriority();
if (priority <= candidate.getPriority()) {
return log(true, "our priority " + priority + " <= candidate's priority " + candidate.getPriority());
} else {

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

The check here in the getHigherPriorityPeers is not enough. The possible case I tested is that bootstrapped follower changes role from follower to candidate and start leader election, as it has an empty conf, it will become leader directly due to if (others.isEmpty()) { r = new ResultAndTerm(Result.PASSED, electionTerm);. Once it becomes another leader, it won't receive any requests from the original leader as the term is increased. Meanwhile, the original leader stuck in sending appendEntries to 'follower'.

I sightly changed the check place. PTAL
@szetszwo

@szetszwo

Copy link
Copy Markdown
Contributor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

The bootstrapped follower could start with empty conf and set conf through leader's appendEntries request or Installsnapshot request. Somehow caused election timeout would let this follower start leader with empty conf.
@szetszwo

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional changes are good although the empty-conf test case is invalid. Some comments inlined; see also https://issues.apache.org/jira/secure/attachment/13037236/560_reivew.patch

Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
@Xushaohong

Xushaohong commented Dec 10, 2021

Copy link
Copy Markdown
ContributorAuthor

Thanks for all the comments above. I have fixed all. @szetszwo

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

@szetszwo

Copy link
Copy Markdown
Contributor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

Currently, the setConf request would not update the FollowerInfo in LogAppenderBase, so we have to get the latest followerInfo from conf like the previous version. I have reverted it back and added an NPE check.
@szetszwo Could you help confirm it?

@szetszwo

Copy link
Copy Markdown
Contributor

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

We should fix the RaftReconfigurationBaseTest but not having the phase check.


private ResultAndTerm submitRequestAndWaitResult(Phase phase, RaftConfigurationImpl conf, long electionTerm)
throws InterruptedException {
if (!conf.containsInConf(server.getId()) && phase == Phase.ELECTION) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

@XushaohongXushaohongDec 13, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

OK, let's find out what's wrong with UT. @szetszwo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help approve the workflow? : )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xushaohong , the build finished. Please take a look.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

I reviewed the failure UT of testRemovePeers. The fault is due to after removing the peers out of the conf, the division which should quit the raft group will become a candidate and start the election. During the prevote process, originally it will send out the request and then the other peers should send back should shutdown to turn this server down. But for our case, we just return new ResultAndTerm(Result.NOT_IN_CONF), and won't go to the SHUTDOWN branch.
@szetszwo Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

@szetszwo

Copy link
Copy Markdown
Contributor

... Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

This is a good idea. Let's shutdown the server when it is not in conf. Thanks.

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Xushaohong@szetszwo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' RATIS-1446. Avoid leader election for invalid conf by Xushaohong · Pull Request #560 · apache/ratis · GitHub
Skip to content

RATIS-1446. Avoid leader election for invalid conf - #560

Merged
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446
Dec 15, 2021
Merged

RATIS-1446. Avoid leader election for invalid conf#560
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446

Conversation

@Xushaohong

@XushaohongXushaohong commented Dec 9, 2021

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Candidate shall not start leader election in these cases in case of possible NPE caused by conf.getPeer().getPriority().
With this patch, the candidate will become a follower again when receiving the future appendEntries request.

Phenomenon:
The bootstrapped follower becomes the leader after the election timeout somehow, with an empty raft conf. Then in the process of yieldLeaderToHigherPriorityPeer, NPE happens as (null).getPriority().
Setconfiguration action takes place at the end of appendEntriesAsync. If we make it possible to raise electiontimeout before this action, we can replay the NPE case.

1private void yieldLeaderToHigherPriorityPeer() {
2 if (!server.getInfo().isLeader()) {
3 return;
4 }
5 final RaftConfigurationImpl conf = server.getRaftConf();
6 int leaderPriority = conf.getPeer(server.getId()).getPriority(); 

Possible reason:
Network delay/loss? The leader did not send entries in time or the follower did not receive entries.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/RATIS-1446

How was this patch tested?

Manual simulation in UT.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@szetszwo Actually, I am not sure whether we should make a specific check for each getPriority() use. Right now, I just prevent the NPE from the source. Currently, conf.getPeer().getPriority() only exists in leader election and leader state.

Comment on lines +586 to +591
// Candidate shall not start leader election in these cases in case of
// possible NPE caused by conf.getPeer().getPriority()
if (!getRaftConf().containsInBothConfs(getId())) {
LOG.warn("{} find invalid configuration {}, skip start leader election", this, getRaftConf());
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

 private Set<RaftPeerId> getHigherPriorityPeers(RaftConfiguration conf) {
final Optional<Integer> priority = Optional.ofNullable(conf.getPeer(server.getId())).map(RaftPeer::getPriority);
return conf.getAllPeers().stream()
.filter(peer -> priority.filter(p -> peer.getPriority() > p).isPresent())
.map(RaftPeer::getId)
.collect(Collectors.toSet());
}

@szetszwo

Copy link
Copy Markdown
Contributor

..., I am not sure whether we should make a specific check for each getPriority() use.

That's a good point. We should check. I just have walked through the code. There are two other cases needed to be fixed:

diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
index 03850114..d1413040 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
@@ -916,13 +916,13 @@ class LeaderStateImpl implements LeaderState {
for (LogAppender logAppender : senders.getSenders()) {
FollowerInfo followerInfo = logAppender.getFollower();
- RaftPeerId followerID = followerInfo.getPeer().getId();
- int followerPriority = conf.getPeer(followerID).getPriority();
-
+ final RaftPeer follower = followerInfo.getPeer();
+ final int followerPriority = follower.getPriority();
if (followerPriority <= leaderPriority) {
continue;
}
+ final RaftPeerId followerID = follower.getId();
final TermIndex leaderLastEntry = server.getState().getLastEntry();
if (leaderLastEntry == null) {
LOG.info("{} send StartLeaderElectionRequest to follower:{} on term:{} because follower's priority:{} " +
diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
index 1ef721aa..fde198fe 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
@@ -144,7 +144,11 @@ class VoteContext {
}
// Check priority
- final int priority = impl.getRaftConf().getPeer(impl.getId()).getPriority();
+ final RaftPeer peer = conf.getPeer(impl.getId());
+ if (peer == null) {
+ return reject("our server " + impl.getId() + " is not in the conf");
+ }
+ final int priority = peer.getPriority();
if (priority <= candidate.getPriority()) {
return log(true, "our priority " + priority + " <= candidate's priority " + candidate.getPriority());
} else {

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

The check here in the getHigherPriorityPeers is not enough. The possible case I tested is that bootstrapped follower changes role from follower to candidate and start leader election, as it has an empty conf, it will become leader directly due to if (others.isEmpty()) { r = new ResultAndTerm(Result.PASSED, electionTerm);. Once it becomes another leader, it won't receive any requests from the original leader as the term is increased. Meanwhile, the original leader stuck in sending appendEntries to 'follower'.

I sightly changed the check place. PTAL
@szetszwo

@szetszwo

Copy link
Copy Markdown
Contributor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

The bootstrapped follower could start with empty conf and set conf through leader's appendEntries request or Installsnapshot request. Somehow caused election timeout would let this follower start leader with empty conf.
@szetszwo

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional changes are good although the empty-conf test case is invalid. Some comments inlined; see also https://issues.apache.org/jira/secure/attachment/13037236/560_reivew.patch

Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
@Xushaohong

Xushaohong commented Dec 10, 2021

Copy link
Copy Markdown
ContributorAuthor

Thanks for all the comments above. I have fixed all. @szetszwo

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

@szetszwo

Copy link
Copy Markdown
Contributor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

Currently, the setConf request would not update the FollowerInfo in LogAppenderBase, so we have to get the latest followerInfo from conf like the previous version. I have reverted it back and added an NPE check.
@szetszwo Could you help confirm it?

@szetszwo

Copy link
Copy Markdown
Contributor

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

We should fix the RaftReconfigurationBaseTest but not having the phase check.


private ResultAndTerm submitRequestAndWaitResult(Phase phase, RaftConfigurationImpl conf, long electionTerm)
throws InterruptedException {
if (!conf.containsInConf(server.getId()) && phase == Phase.ELECTION) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

@XushaohongXushaohongDec 13, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

OK, let's find out what's wrong with UT. @szetszwo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help approve the workflow? : )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xushaohong , the build finished. Please take a look.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

I reviewed the failure UT of testRemovePeers. The fault is due to after removing the peers out of the conf, the division which should quit the raft group will become a candidate and start the election. During the prevote process, originally it will send out the request and then the other peers should send back should shutdown to turn this server down. But for our case, we just return new ResultAndTerm(Result.NOT_IN_CONF), and won't go to the SHUTDOWN branch.
@szetszwo Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

@szetszwo

Copy link
Copy Markdown
Contributor

... Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

This is a good idea. Let's shutdown the server when it is not in conf. Thanks.

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Xushaohong@szetszwo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' RATIS-1446. Avoid leader election for invalid conf by Xushaohong · Pull Request #560 · apache/ratis · GitHub
Skip to content

RATIS-1446. Avoid leader election for invalid conf - #560

Merged
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446
Dec 15, 2021
Merged

RATIS-1446. Avoid leader election for invalid conf#560
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446

Conversation

@Xushaohong

@XushaohongXushaohong commented Dec 9, 2021

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Candidate shall not start leader election in these cases in case of possible NPE caused by conf.getPeer().getPriority().
With this patch, the candidate will become a follower again when receiving the future appendEntries request.

Phenomenon:
The bootstrapped follower becomes the leader after the election timeout somehow, with an empty raft conf. Then in the process of yieldLeaderToHigherPriorityPeer, NPE happens as (null).getPriority().
Setconfiguration action takes place at the end of appendEntriesAsync. If we make it possible to raise electiontimeout before this action, we can replay the NPE case.

1private void yieldLeaderToHigherPriorityPeer() {
2 if (!server.getInfo().isLeader()) {
3 return;
4 }
5 final RaftConfigurationImpl conf = server.getRaftConf();
6 int leaderPriority = conf.getPeer(server.getId()).getPriority(); 

Possible reason:
Network delay/loss? The leader did not send entries in time or the follower did not receive entries.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/RATIS-1446

How was this patch tested?

Manual simulation in UT.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@szetszwo Actually, I am not sure whether we should make a specific check for each getPriority() use. Right now, I just prevent the NPE from the source. Currently, conf.getPeer().getPriority() only exists in leader election and leader state.

Comment on lines +586 to +591
// Candidate shall not start leader election in these cases in case of
// possible NPE caused by conf.getPeer().getPriority()
if (!getRaftConf().containsInBothConfs(getId())) {
LOG.warn("{} find invalid configuration {}, skip start leader election", this, getRaftConf());
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

 private Set<RaftPeerId> getHigherPriorityPeers(RaftConfiguration conf) {
final Optional<Integer> priority = Optional.ofNullable(conf.getPeer(server.getId())).map(RaftPeer::getPriority);
return conf.getAllPeers().stream()
.filter(peer -> priority.filter(p -> peer.getPriority() > p).isPresent())
.map(RaftPeer::getId)
.collect(Collectors.toSet());
}

@szetszwo

Copy link
Copy Markdown
Contributor

..., I am not sure whether we should make a specific check for each getPriority() use.

That's a good point. We should check. I just have walked through the code. There are two other cases needed to be fixed:

diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
index 03850114..d1413040 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
@@ -916,13 +916,13 @@ class LeaderStateImpl implements LeaderState {
for (LogAppender logAppender : senders.getSenders()) {
FollowerInfo followerInfo = logAppender.getFollower();
- RaftPeerId followerID = followerInfo.getPeer().getId();
- int followerPriority = conf.getPeer(followerID).getPriority();
-
+ final RaftPeer follower = followerInfo.getPeer();
+ final int followerPriority = follower.getPriority();
if (followerPriority <= leaderPriority) {
continue;
}
+ final RaftPeerId followerID = follower.getId();
final TermIndex leaderLastEntry = server.getState().getLastEntry();
if (leaderLastEntry == null) {
LOG.info("{} send StartLeaderElectionRequest to follower:{} on term:{} because follower's priority:{} " +
diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
index 1ef721aa..fde198fe 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
@@ -144,7 +144,11 @@ class VoteContext {
}
// Check priority
- final int priority = impl.getRaftConf().getPeer(impl.getId()).getPriority();
+ final RaftPeer peer = conf.getPeer(impl.getId());
+ if (peer == null) {
+ return reject("our server " + impl.getId() + " is not in the conf");
+ }
+ final int priority = peer.getPriority();
if (priority <= candidate.getPriority()) {
return log(true, "our priority " + priority + " <= candidate's priority " + candidate.getPriority());
} else {

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

The check here in the getHigherPriorityPeers is not enough. The possible case I tested is that bootstrapped follower changes role from follower to candidate and start leader election, as it has an empty conf, it will become leader directly due to if (others.isEmpty()) { r = new ResultAndTerm(Result.PASSED, electionTerm);. Once it becomes another leader, it won't receive any requests from the original leader as the term is increased. Meanwhile, the original leader stuck in sending appendEntries to 'follower'.

I sightly changed the check place. PTAL
@szetszwo

@szetszwo

Copy link
Copy Markdown
Contributor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

The bootstrapped follower could start with empty conf and set conf through leader's appendEntries request or Installsnapshot request. Somehow caused election timeout would let this follower start leader with empty conf.
@szetszwo

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional changes are good although the empty-conf test case is invalid. Some comments inlined; see also https://issues.apache.org/jira/secure/attachment/13037236/560_reivew.patch

Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
@Xushaohong

Xushaohong commented Dec 10, 2021

Copy link
Copy Markdown
ContributorAuthor

Thanks for all the comments above. I have fixed all. @szetszwo

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

@szetszwo

Copy link
Copy Markdown
Contributor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

Currently, the setConf request would not update the FollowerInfo in LogAppenderBase, so we have to get the latest followerInfo from conf like the previous version. I have reverted it back and added an NPE check.
@szetszwo Could you help confirm it?

@szetszwo

Copy link
Copy Markdown
Contributor

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

We should fix the RaftReconfigurationBaseTest but not having the phase check.


private ResultAndTerm submitRequestAndWaitResult(Phase phase, RaftConfigurationImpl conf, long electionTerm)
throws InterruptedException {
if (!conf.containsInConf(server.getId()) && phase == Phase.ELECTION) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

@XushaohongXushaohongDec 13, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

OK, let's find out what's wrong with UT. @szetszwo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help approve the workflow? : )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xushaohong , the build finished. Please take a look.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

I reviewed the failure UT of testRemovePeers. The fault is due to after removing the peers out of the conf, the division which should quit the raft group will become a candidate and start the election. During the prevote process, originally it will send out the request and then the other peers should send back should shutdown to turn this server down. But for our case, we just return new ResultAndTerm(Result.NOT_IN_CONF), and won't go to the SHUTDOWN branch.
@szetszwo Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

@szetszwo

Copy link
Copy Markdown
Contributor

... Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

This is a good idea. Let's shutdown the server when it is not in conf. Thanks.

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Xushaohong@szetszwo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' RATIS-1446. Avoid leader election for invalid conf by Xushaohong · Pull Request #560 · apache/ratis · GitHub
Skip to content

RATIS-1446. Avoid leader election for invalid conf - #560

Merged
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446
Dec 15, 2021
Merged

RATIS-1446. Avoid leader election for invalid conf#560
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446

Conversation

@Xushaohong

@XushaohongXushaohong commented Dec 9, 2021

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Candidate shall not start leader election in these cases in case of possible NPE caused by conf.getPeer().getPriority().
With this patch, the candidate will become a follower again when receiving the future appendEntries request.

Phenomenon:
The bootstrapped follower becomes the leader after the election timeout somehow, with an empty raft conf. Then in the process of yieldLeaderToHigherPriorityPeer, NPE happens as (null).getPriority().
Setconfiguration action takes place at the end of appendEntriesAsync. If we make it possible to raise electiontimeout before this action, we can replay the NPE case.

1private void yieldLeaderToHigherPriorityPeer() {
2 if (!server.getInfo().isLeader()) {
3 return;
4 }
5 final RaftConfigurationImpl conf = server.getRaftConf();
6 int leaderPriority = conf.getPeer(server.getId()).getPriority(); 

Possible reason:
Network delay/loss? The leader did not send entries in time or the follower did not receive entries.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/RATIS-1446

How was this patch tested?

Manual simulation in UT.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@szetszwo Actually, I am not sure whether we should make a specific check for each getPriority() use. Right now, I just prevent the NPE from the source. Currently, conf.getPeer().getPriority() only exists in leader election and leader state.

Comment on lines +586 to +591
// Candidate shall not start leader election in these cases in case of
// possible NPE caused by conf.getPeer().getPriority()
if (!getRaftConf().containsInBothConfs(getId())) {
LOG.warn("{} find invalid configuration {}, skip start leader election", this, getRaftConf());
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

 private Set<RaftPeerId> getHigherPriorityPeers(RaftConfiguration conf) {
final Optional<Integer> priority = Optional.ofNullable(conf.getPeer(server.getId())).map(RaftPeer::getPriority);
return conf.getAllPeers().stream()
.filter(peer -> priority.filter(p -> peer.getPriority() > p).isPresent())
.map(RaftPeer::getId)
.collect(Collectors.toSet());
}

@szetszwo

Copy link
Copy Markdown
Contributor

..., I am not sure whether we should make a specific check for each getPriority() use.

That's a good point. We should check. I just have walked through the code. There are two other cases needed to be fixed:

diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
index 03850114..d1413040 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
@@ -916,13 +916,13 @@ class LeaderStateImpl implements LeaderState {
for (LogAppender logAppender : senders.getSenders()) {
FollowerInfo followerInfo = logAppender.getFollower();
- RaftPeerId followerID = followerInfo.getPeer().getId();
- int followerPriority = conf.getPeer(followerID).getPriority();
-
+ final RaftPeer follower = followerInfo.getPeer();
+ final int followerPriority = follower.getPriority();
if (followerPriority <= leaderPriority) {
continue;
}
+ final RaftPeerId followerID = follower.getId();
final TermIndex leaderLastEntry = server.getState().getLastEntry();
if (leaderLastEntry == null) {
LOG.info("{} send StartLeaderElectionRequest to follower:{} on term:{} because follower's priority:{} " +
diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
index 1ef721aa..fde198fe 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
@@ -144,7 +144,11 @@ class VoteContext {
}
// Check priority
- final int priority = impl.getRaftConf().getPeer(impl.getId()).getPriority();
+ final RaftPeer peer = conf.getPeer(impl.getId());
+ if (peer == null) {
+ return reject("our server " + impl.getId() + " is not in the conf");
+ }
+ final int priority = peer.getPriority();
if (priority <= candidate.getPriority()) {
return log(true, "our priority " + priority + " <= candidate's priority " + candidate.getPriority());
} else {

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

The check here in the getHigherPriorityPeers is not enough. The possible case I tested is that bootstrapped follower changes role from follower to candidate and start leader election, as it has an empty conf, it will become leader directly due to if (others.isEmpty()) { r = new ResultAndTerm(Result.PASSED, electionTerm);. Once it becomes another leader, it won't receive any requests from the original leader as the term is increased. Meanwhile, the original leader stuck in sending appendEntries to 'follower'.

I sightly changed the check place. PTAL
@szetszwo

@szetszwo

Copy link
Copy Markdown
Contributor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

The bootstrapped follower could start with empty conf and set conf through leader's appendEntries request or Installsnapshot request. Somehow caused election timeout would let this follower start leader with empty conf.
@szetszwo

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional changes are good although the empty-conf test case is invalid. Some comments inlined; see also https://issues.apache.org/jira/secure/attachment/13037236/560_reivew.patch

Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
@Xushaohong

Xushaohong commented Dec 10, 2021

Copy link
Copy Markdown
ContributorAuthor

Thanks for all the comments above. I have fixed all. @szetszwo

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

@szetszwo

Copy link
Copy Markdown
Contributor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

Currently, the setConf request would not update the FollowerInfo in LogAppenderBase, so we have to get the latest followerInfo from conf like the previous version. I have reverted it back and added an NPE check.
@szetszwo Could you help confirm it?

@szetszwo

Copy link
Copy Markdown
Contributor

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

We should fix the RaftReconfigurationBaseTest but not having the phase check.


private ResultAndTerm submitRequestAndWaitResult(Phase phase, RaftConfigurationImpl conf, long electionTerm)
throws InterruptedException {
if (!conf.containsInConf(server.getId()) && phase == Phase.ELECTION) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

@XushaohongXushaohongDec 13, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

OK, let's find out what's wrong with UT. @szetszwo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help approve the workflow? : )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xushaohong , the build finished. Please take a look.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

I reviewed the failure UT of testRemovePeers. The fault is due to after removing the peers out of the conf, the division which should quit the raft group will become a candidate and start the election. During the prevote process, originally it will send out the request and then the other peers should send back should shutdown to turn this server down. But for our case, we just return new ResultAndTerm(Result.NOT_IN_CONF), and won't go to the SHUTDOWN branch.
@szetszwo Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

@szetszwo

Copy link
Copy Markdown
Contributor

... Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

This is a good idea. Let's shutdown the server when it is not in conf. Thanks.

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Xushaohong@szetszwo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' RATIS-1446. Avoid leader election for invalid conf by Xushaohong · Pull Request #560 · apache/ratis · GitHub
Skip to content

RATIS-1446. Avoid leader election for invalid conf - #560

Merged
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446
Dec 15, 2021
Merged

RATIS-1446. Avoid leader election for invalid conf#560
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446

Conversation

@Xushaohong

@XushaohongXushaohong commented Dec 9, 2021

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Candidate shall not start leader election in these cases in case of possible NPE caused by conf.getPeer().getPriority().
With this patch, the candidate will become a follower again when receiving the future appendEntries request.

Phenomenon:
The bootstrapped follower becomes the leader after the election timeout somehow, with an empty raft conf. Then in the process of yieldLeaderToHigherPriorityPeer, NPE happens as (null).getPriority().
Setconfiguration action takes place at the end of appendEntriesAsync. If we make it possible to raise electiontimeout before this action, we can replay the NPE case.

1private void yieldLeaderToHigherPriorityPeer() {
2 if (!server.getInfo().isLeader()) {
3 return;
4 }
5 final RaftConfigurationImpl conf = server.getRaftConf();
6 int leaderPriority = conf.getPeer(server.getId()).getPriority(); 

Possible reason:
Network delay/loss? The leader did not send entries in time or the follower did not receive entries.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/RATIS-1446

How was this patch tested?

Manual simulation in UT.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@szetszwo Actually, I am not sure whether we should make a specific check for each getPriority() use. Right now, I just prevent the NPE from the source. Currently, conf.getPeer().getPriority() only exists in leader election and leader state.

Comment on lines +586 to +591
// Candidate shall not start leader election in these cases in case of
// possible NPE caused by conf.getPeer().getPriority()
if (!getRaftConf().containsInBothConfs(getId())) {
LOG.warn("{} find invalid configuration {}, skip start leader election", this, getRaftConf());
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

 private Set<RaftPeerId> getHigherPriorityPeers(RaftConfiguration conf) {
final Optional<Integer> priority = Optional.ofNullable(conf.getPeer(server.getId())).map(RaftPeer::getPriority);
return conf.getAllPeers().stream()
.filter(peer -> priority.filter(p -> peer.getPriority() > p).isPresent())
.map(RaftPeer::getId)
.collect(Collectors.toSet());
}

@szetszwo

Copy link
Copy Markdown
Contributor

..., I am not sure whether we should make a specific check for each getPriority() use.

That's a good point. We should check. I just have walked through the code. There are two other cases needed to be fixed:

diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
index 03850114..d1413040 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
@@ -916,13 +916,13 @@ class LeaderStateImpl implements LeaderState {
for (LogAppender logAppender : senders.getSenders()) {
FollowerInfo followerInfo = logAppender.getFollower();
- RaftPeerId followerID = followerInfo.getPeer().getId();
- int followerPriority = conf.getPeer(followerID).getPriority();
-
+ final RaftPeer follower = followerInfo.getPeer();
+ final int followerPriority = follower.getPriority();
if (followerPriority <= leaderPriority) {
continue;
}
+ final RaftPeerId followerID = follower.getId();
final TermIndex leaderLastEntry = server.getState().getLastEntry();
if (leaderLastEntry == null) {
LOG.info("{} send StartLeaderElectionRequest to follower:{} on term:{} because follower's priority:{} " +
diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
index 1ef721aa..fde198fe 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
@@ -144,7 +144,11 @@ class VoteContext {
}
// Check priority
- final int priority = impl.getRaftConf().getPeer(impl.getId()).getPriority();
+ final RaftPeer peer = conf.getPeer(impl.getId());
+ if (peer == null) {
+ return reject("our server " + impl.getId() + " is not in the conf");
+ }
+ final int priority = peer.getPriority();
if (priority <= candidate.getPriority()) {
return log(true, "our priority " + priority + " <= candidate's priority " + candidate.getPriority());
} else {

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

The check here in the getHigherPriorityPeers is not enough. The possible case I tested is that bootstrapped follower changes role from follower to candidate and start leader election, as it has an empty conf, it will become leader directly due to if (others.isEmpty()) { r = new ResultAndTerm(Result.PASSED, electionTerm);. Once it becomes another leader, it won't receive any requests from the original leader as the term is increased. Meanwhile, the original leader stuck in sending appendEntries to 'follower'.

I sightly changed the check place. PTAL
@szetszwo

@szetszwo

Copy link
Copy Markdown
Contributor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

The bootstrapped follower could start with empty conf and set conf through leader's appendEntries request or Installsnapshot request. Somehow caused election timeout would let this follower start leader with empty conf.
@szetszwo

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional changes are good although the empty-conf test case is invalid. Some comments inlined; see also https://issues.apache.org/jira/secure/attachment/13037236/560_reivew.patch

Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
@Xushaohong

Xushaohong commented Dec 10, 2021

Copy link
Copy Markdown
ContributorAuthor

Thanks for all the comments above. I have fixed all. @szetszwo

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

@szetszwo

Copy link
Copy Markdown
Contributor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

Currently, the setConf request would not update the FollowerInfo in LogAppenderBase, so we have to get the latest followerInfo from conf like the previous version. I have reverted it back and added an NPE check.
@szetszwo Could you help confirm it?

@szetszwo

Copy link
Copy Markdown
Contributor

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

We should fix the RaftReconfigurationBaseTest but not having the phase check.


private ResultAndTerm submitRequestAndWaitResult(Phase phase, RaftConfigurationImpl conf, long electionTerm)
throws InterruptedException {
if (!conf.containsInConf(server.getId()) && phase == Phase.ELECTION) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

@XushaohongXushaohongDec 13, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

OK, let's find out what's wrong with UT. @szetszwo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help approve the workflow? : )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xushaohong , the build finished. Please take a look.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

I reviewed the failure UT of testRemovePeers. The fault is due to after removing the peers out of the conf, the division which should quit the raft group will become a candidate and start the election. During the prevote process, originally it will send out the request and then the other peers should send back should shutdown to turn this server down. But for our case, we just return new ResultAndTerm(Result.NOT_IN_CONF), and won't go to the SHUTDOWN branch.
@szetszwo Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

@szetszwo

Copy link
Copy Markdown
Contributor

... Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

This is a good idea. Let's shutdown the server when it is not in conf. Thanks.

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Xushaohong@szetszwo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); RATIS-1446. Avoid leader election for invalid conf by Xushaohong · Pull Request #560 · apache/ratis · GitHub
Skip to content

RATIS-1446. Avoid leader election for invalid conf - #560

Merged
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446
Dec 15, 2021
Merged

RATIS-1446. Avoid leader election for invalid conf#560
szetszwo merged 6 commits into
apache:masterfrom
Xushaohong:RATIS-1446

Conversation

@Xushaohong

@XushaohongXushaohong commented Dec 9, 2021

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Candidate shall not start leader election in these cases in case of possible NPE caused by conf.getPeer().getPriority().
With this patch, the candidate will become a follower again when receiving the future appendEntries request.

Phenomenon:
The bootstrapped follower becomes the leader after the election timeout somehow, with an empty raft conf. Then in the process of yieldLeaderToHigherPriorityPeer, NPE happens as (null).getPriority().
Setconfiguration action takes place at the end of appendEntriesAsync. If we make it possible to raise electiontimeout before this action, we can replay the NPE case.

1private void yieldLeaderToHigherPriorityPeer() {
2 if (!server.getInfo().isLeader()) {
3 return;
4 }
5 final RaftConfigurationImpl conf = server.getRaftConf();
6 int leaderPriority = conf.getPeer(server.getId()).getPriority(); 

Possible reason:
Network delay/loss? The leader did not send entries in time or the follower did not receive entries.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/RATIS-1446

How was this patch tested?

Manual simulation in UT.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@szetszwo Actually, I am not sure whether we should make a specific check for each getPriority() use. Right now, I just prevent the NPE from the source. Currently, conf.getPeer().getPriority() only exists in leader election and leader state.

Comment on lines +586 to +591
// Candidate shall not start leader election in these cases in case of
// possible NPE caused by conf.getPeer().getPriority()
if (!getRaftConf().containsInBothConfs(getId())) {
LOG.warn("{} find invalid configuration {}, skip start leader election", this, getRaftConf());
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

 private Set<RaftPeerId> getHigherPriorityPeers(RaftConfiguration conf) {
final Optional<Integer> priority = Optional.ofNullable(conf.getPeer(server.getId())).map(RaftPeer::getPriority);
return conf.getAllPeers().stream()
.filter(peer -> priority.filter(p -> peer.getPriority() > p).isPresent())
.map(RaftPeer::getId)
.collect(Collectors.toSet());
}

@szetszwo

Copy link
Copy Markdown
Contributor

..., I am not sure whether we should make a specific check for each getPriority() use.

That's a good point. We should check. I just have walked through the code. There are two other cases needed to be fixed:

diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
index 03850114..d1413040 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/LeaderStateImpl.java
@@ -916,13 +916,13 @@ class LeaderStateImpl implements LeaderState {
for (LogAppender logAppender : senders.getSenders()) {
FollowerInfo followerInfo = logAppender.getFollower();
- RaftPeerId followerID = followerInfo.getPeer().getId();
- int followerPriority = conf.getPeer(followerID).getPriority();
-
+ final RaftPeer follower = followerInfo.getPeer();
+ final int followerPriority = follower.getPriority();
if (followerPriority <= leaderPriority) {
continue;
}
+ final RaftPeerId followerID = follower.getId();
final TermIndex leaderLastEntry = server.getState().getLastEntry();
if (leaderLastEntry == null) {
LOG.info("{} send StartLeaderElectionRequest to follower:{} on term:{} because follower's priority:{} " +
diff --git a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
index 1ef721aa..fde198fe 100644
--- a/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
+++ b/ratis-server/src/main/java/org/apache/ratis/server/impl/VoteContext.java
@@ -144,7 +144,11 @@ class VoteContext {
}
// Check priority
- final int priority = impl.getRaftConf().getPeer(impl.getId()).getPriority();
+ final RaftPeer peer = conf.getPeer(impl.getId());
+ if (peer == null) {
+ return reject("our server " + impl.getId() + " is not in the conf");
+ }
+ final int priority = peer.getPriority();
if (priority <= candidate.getPriority()) {
return log(true, "our priority " + priority + " <= candidate's priority " + candidate.getPriority());
} else {

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

The conf may be changed later on. Therefore, let's don't check it here and check null in LeaderElection.getHigherPriorityPeers(..) as below.

The check here in the getHigherPriorityPeers is not enough. The possible case I tested is that bootstrapped follower changes role from follower to candidate and start leader election, as it has an empty conf, it will become leader directly due to if (others.isEmpty()) { r = new ResultAndTerm(Result.PASSED, electionTerm);. Once it becomes another leader, it won't receive any requests from the original leader as the term is increased. Meanwhile, the original leader stuck in sending appendEntries to 'follower'.

I sightly changed the check place. PTAL
@szetszwo

@szetszwo

Copy link
Copy Markdown
Contributor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

..., as it has an empty conf ...

This is an invalid test case since the conf (i.e. the group) is incorrect in the beginning. We must either set the group correct before startup or use AdminApi.setConfiguration to change the conf after startup.

The bootstrapped follower could start with empty conf and set conf through leader's appendEntries request or Installsnapshot request. Somehow caused election timeout would let this follower start leader with empty conf.
@szetszwo

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional changes are good although the empty-conf test case is invalid. Some comments inlined; see also https://issues.apache.org/jira/secure/attachment/13037236/560_reivew.patch

Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
Comment threadratis-server/src/main/java/org/apache/ratis/server/impl/LeaderElection.java Outdated
@Xushaohong

Xushaohong commented Dec 10, 2021

Copy link
Copy Markdown
ContributorAuthor

Thanks for all the comments above. I have fixed all. @szetszwo

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

@szetszwo

Copy link
Copy Markdown
Contributor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

@Xushaohong , thanks for the update.

TestLeaderElectionWithNetty.testTransferLeader failed. I also can reproduce it locally. Could you take a look?

Currently, the setConf request would not update the FollowerInfo in LogAppenderBase, so we have to get the latest followerInfo from conf like the previous version. I have reverted it back and added an NPE check.
@szetszwo Could you help confirm it?

@szetszwo

Copy link
Copy Markdown
Contributor

With NOT_IN_CONF check added in submitRequestAndWaitResult, RaftReconfigurationBaseTest would have some UT failed. I checked it and found it clashes with PRE_VOTE and thus added the phase check.

We should fix the RaftReconfigurationBaseTest but not having the phase check.


private ResultAndTerm submitRequestAndWaitResult(Phase phase, RaftConfigurationImpl conf, long electionTerm)
throws InterruptedException {
if (!conf.containsInConf(server.getId()) && phase == Phase.ELECTION) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

@XushaohongXushaohongDec 13, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix the unit tests but not adding the phase check here.

OK, let's find out what's wrong with UT. @szetszwo

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help approve the workflow? : )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xushaohong , the build finished. Please take a look.

@Xushaohong

Copy link
Copy Markdown
ContributorAuthor

I reviewed the failure UT of testRemovePeers. The fault is due to after removing the peers out of the conf, the division which should quit the raft group will become a candidate and start the election. During the prevote process, originally it will send out the request and then the other peers should send back should shutdown to turn this server down. But for our case, we just return new ResultAndTerm(Result.NOT_IN_CONF), and won't go to the SHUTDOWN branch.
@szetszwo Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

@szetszwo

Copy link
Copy Markdown
Contributor

... Shall we move up the line case NOT_IN_CONF: above case SHUTDOWN or still use my original phase check?

This is a good idea. Let's shutdown the server when it is not in conf. Thanks.

@szetszwoszetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the change looks good.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Xushaohong@szetszwo