-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-16530: Fix high-watermark calculation to not assume the leader is in the voter set #16079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
372e02c
e7e067b
c3a00c0
93fd95d
66e27d7
06bf0f8
9acdb02
d33f923
4eb7961
5d73624
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,7 @@ | |
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.Iterator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
@@ -243,8 +244,9 @@ private boolean maybeUpdateHighWatermark() { | |
| ); | ||
| return true; | ||
| } else if (highWatermarkUpdateOffset < currentHighWatermarkMetadata.offset) { | ||
| log.error("The latest computed high watermark {} is smaller than the current " + | ||
| "value {}, which suggests that one of the voters has lost committed data. " + | ||
| log.info("The latest computed high watermark {} is smaller than the current " + | ||
| "value {}, which should only happen when voter set membership changes. If the voter " + | ||
| "set has not changed this suggests that one of the voters has lost committed data. " + | ||
| "Full voter replication state: {}", highWatermarkUpdateOffset, | ||
| currentHighWatermarkMetadata.offset, voterStates.values()); | ||
| return false; | ||
|
|
@@ -296,10 +298,12 @@ private void logHighWatermarkUpdate( | |
| * Update the local replica state. | ||
| * | ||
| * @param endOffsetMetadata updated log end offset of local replica | ||
| * @param lastVoterSet the up-to-date voter set | ||
| * @return true if the high watermark is updated as a result of this call | ||
| */ | ||
| public boolean updateLocalState( | ||
| LogOffsetMetadata endOffsetMetadata | ||
| LogOffsetMetadata endOffsetMetadata, | ||
| Set<Integer> lastVoterSet | ||
| ) { | ||
| ReplicaState state = getOrCreateReplicaState(localId); | ||
| state.endOffset.ifPresent(currentEndOffset -> { | ||
|
|
@@ -308,7 +312,8 @@ public boolean updateLocalState( | |
| "end offset: " + currentEndOffset.offset + " -> " + endOffsetMetadata.offset); | ||
| } | ||
| }); | ||
| state.updateLeaderState(endOffsetMetadata); | ||
| state.updateLeaderEndOffset(endOffsetMetadata); | ||
| updateVoterSet(lastVoterSet); | ||
| return maybeUpdateHighWatermark(); | ||
| } | ||
|
|
||
|
|
@@ -341,9 +346,18 @@ public boolean updateReplicaState( | |
| state.nodeId, currentEndOffset.offset, fetchOffsetMetadata.offset); | ||
| } | ||
| }); | ||
|
|
||
| Optional<LogOffsetMetadata> leaderEndOffsetOpt = | ||
| voterStates.get(localId).endOffset; | ||
| Optional<LogOffsetMetadata> leaderEndOffsetOpt; | ||
| ReplicaState leaderVoterState = voterStates.get(localId); | ||
| ReplicaState leaderObserverState = observerStates.get(localId); | ||
| if (leaderVoterState != null) { | ||
| leaderEndOffsetOpt = leaderVoterState.endOffset; | ||
| } else if (leaderObserverState != null) { | ||
| // The leader is not guaranteed to be in the voter set when in the process of being removed from the quorum. | ||
| log.info("Updating end offset for leader {} which is also an observer.", localId); | ||
| leaderEndOffsetOpt = leaderObserverState.endOffset; | ||
| } else { | ||
| throw new IllegalStateException("Leader state not found for localId " + localId); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In practice isn't this the same as |
||
|
|
||
| state.updateFollowerState( | ||
| currentTimeMs, | ||
|
|
@@ -387,12 +401,16 @@ public long epochStartOffset() { | |
| private ReplicaState getOrCreateReplicaState(int remoteNodeId) { | ||
| ReplicaState state = voterStates.get(remoteNodeId); | ||
| if (state == null) { | ||
| observerStates.putIfAbsent(remoteNodeId, new ReplicaState(remoteNodeId, false)); | ||
| return observerStates.get(remoteNodeId); | ||
| return createObserverState(remoteNodeId); | ||
| } | ||
| return state; | ||
| } | ||
|
|
||
| private ReplicaState createObserverState(int remoteNodeId) { | ||
| observerStates.putIfAbsent(remoteNodeId, new ReplicaState(remoteNodeId, false)); | ||
| return observerStates.get(remoteNodeId); | ||
| } | ||
|
|
||
| public DescribeQuorumResponseData.PartitionData describeQuorum(long currentTimeMs) { | ||
| clearInactiveObservers(currentTimeMs); | ||
|
|
||
|
|
@@ -445,6 +463,27 @@ private boolean isVoter(int remoteNodeId) { | |
| return voterStates.containsKey(remoteNodeId); | ||
| } | ||
|
|
||
| // with Jose's changes this will probably make more sense as VoterSet | ||
| private void updateVoterSet(Set<Integer> lastVoterSet) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the comment is basically saying |
||
| // Remove any voter state that is not in the last voter set. They become observers. | ||
| for (Iterator<Map.Entry<Integer, ReplicaState>> iter = voterStates.entrySet().iterator(); iter.hasNext(); ) { | ||
| Integer nodeId = iter.next().getKey(); | ||
| if (!lastVoterSet.contains(nodeId)) { | ||
| createObserverState(nodeId); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code in |
||
| iter.remove(); | ||
| } | ||
| } | ||
|
|
||
| // Add any voter state that is in the last voter set but not in the current voter set. They are removed from | ||
| // the observerStates if they exist. | ||
| for (int voterId : lastVoterSet) { | ||
| if (!voterStates.containsKey(voterId)) { | ||
| voterStates.put(voterId, new ReplicaState(voterId, false)); | ||
| observerStates.remove(voterId); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment applies to this code block and the one above. When moving a replica state from voter to observer and vice versa, we shouldn't create a new replica state but instead reuse the replica state for the previous hash map. |
||
| } | ||
| } | ||
| } | ||
|
|
||
| private static class ReplicaState implements Comparable<ReplicaState> { | ||
| final int nodeId; | ||
| Optional<LogOffsetMetadata> endOffset; | ||
|
|
@@ -462,7 +501,7 @@ public ReplicaState(int nodeId, boolean hasAcknowledgedLeader) { | |
| this.hasAcknowledgedLeader = hasAcknowledgedLeader; | ||
| } | ||
|
|
||
| void updateLeaderState( | ||
| void updateLeaderEndOffset( | ||
| LogOffsetMetadata endOffsetMetadata | ||
| ) { | ||
| // For the leader, we only update the end offset. The remaining fields | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you move this right before the
ifstatement and mark it as final it makes it more obvious that this variable can take two different values.