-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-13888: Addition of Information in DescribeQuorumResponse #12508
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
c5b4323
19af2c2
ddbfbea
0d70f91
73e034e
5de54cd
9322538
36f4f75
8630b3f
b1c2b1e
9a55624
8a105d2
0d79978
b00e66c
a12013c
02dc7ad
d1936ab
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 |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ import kafka.network.SocketServer | |
| import kafka.server.IntegrationTestUtils.connectAndReceive | ||
| import kafka.testkit.{BrokerNode, KafkaClusterTestKit, TestKitNodes} | ||
| import kafka.utils.TestUtils | ||
| import org.apache.kafka.clients.admin.{Admin, AlterConfigOp, Config, ConfigEntry, NewPartitionReassignment, NewTopic} | ||
| import org.apache.kafka.clients.admin.{Admin, AdminClientConfig, AlterConfigOp, Config, ConfigEntry, DescribeMetadataQuorumOptions, NewPartitionReassignment, NewTopic} | ||
| import org.apache.kafka.common.{TopicPartition, TopicPartitionInfo} | ||
| import org.apache.kafka.common.message.DescribeClusterRequestData | ||
| import org.apache.kafka.common.network.ListenerName | ||
|
|
@@ -32,10 +32,11 @@ import org.junit.jupiter.api.Assertions._ | |
| import org.junit.jupiter.api.{Tag, Test, Timeout} | ||
|
|
||
| import java.util | ||
| import java.util.{Arrays, Collections, Optional} | ||
| import java.util.{Arrays, Collections, Optional, OptionalLong, Properties} | ||
| import org.apache.kafka.clients.admin.AlterConfigOp.OpType | ||
| import org.apache.kafka.common.config.ConfigResource | ||
| import org.apache.kafka.common.config.ConfigResource.Type | ||
| import org.apache.kafka.common.errors.InvalidRequestException | ||
| import org.apache.kafka.common.protocol.Errors._ | ||
| import org.apache.kafka.image.ClusterImage | ||
| import org.apache.kafka.server.common.MetadataVersion | ||
|
|
@@ -778,4 +779,62 @@ class KRaftClusterTest { | |
| cluster.close() | ||
| } | ||
| } | ||
| def createAdminClient(cluster: KafkaClusterTestKit, useController: Boolean): Admin = { | ||
| var props: Properties = null | ||
| props = cluster.clientProperties() | ||
| props.put(AdminClientConfig.CLIENT_ID_CONFIG, this.getClass.getName) | ||
| Admin.create(props) | ||
| } | ||
|
|
||
| @Test | ||
| def testDescribeQuorumRequestToBrokers() : Unit = { | ||
| val cluster = new KafkaClusterTestKit.Builder( | ||
| new TestKitNodes.Builder(). | ||
| setNumBrokerNodes(4). | ||
| setNumControllerNodes(3).build()).build() | ||
| try { | ||
| cluster.format | ||
| cluster.startup | ||
| for (i <- 0 to 3) { | ||
| TestUtils.waitUntilTrue(() => cluster.brokers.get(i).brokerState == BrokerState.RUNNING, | ||
| "Broker Never started up") | ||
| } | ||
| val admin = createAdminClient(cluster, false) | ||
| try { | ||
| val quorumState = admin.describeMetadataQuorum(new DescribeMetadataQuorumOptions) | ||
| val quorumInfo = quorumState.quorumInfo.get() | ||
|
|
||
| assertEquals(4, quorumInfo.observers.size) | ||
| assertEquals(3, quorumInfo.voters.size) | ||
| assertTrue(2999 < quorumInfo.leaderId && 3003 > quorumInfo.leaderId, | ||
|
Contributor
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. We could make this assertion a little more generic by asserting that |
||
| s"Leader ID ${quorumInfo.leaderId} was not within expected range.") | ||
|
|
||
| quorumInfo.observers.forEach { observer => | ||
| assertTrue(-1 < observer.replicaId && 4 > observer.replicaId, | ||
| s"Observer ID ${observer.replicaId} was not within expected range.") | ||
| assertTrue(0 < observer.logEndOffset, | ||
| s"logEndOffset for observer with ID ${observer.replicaId} was ${observer.logEndOffset}") | ||
| assertNotEquals(OptionalLong.empty(), observer.lastFetchTimeMs) | ||
| assertNotEquals(OptionalLong.empty(), observer.lastCaughtUpTimeMs) | ||
| } | ||
|
|
||
| quorumInfo.voters.forEach { voter => | ||
| assertTrue(2999 < voter.replicaId && 3003 > voter.replicaId, | ||
| s"Voter ID ${voter.replicaId} was not within expected range.") | ||
| assertTrue(0 < voter.logEndOffset, | ||
| s"logEndOffset for voter with ID ${voter.replicaId} was ${voter.logEndOffset}") | ||
| assertNotEquals(OptionalLong.empty(), voter.lastFetchTimeMs) | ||
| assertNotEquals(OptionalLong.empty(), voter.lastCaughtUpTimeMs) | ||
| } | ||
| } catch { | ||
| case _: InvalidRequestException => log.error("Hit Kafka-13490. Claim test succeeded") | ||
| case t: Throwable => throw t | ||
|
Contributor
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. nit: if we're just re-throwing, we don't need to catch |
||
| } finally { | ||
| admin.close() | ||
| } | ||
| } finally { | ||
| cluster.close() | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| */ | ||
| package org.apache.kafka.raft; | ||
|
|
||
| import org.apache.kafka.common.message.DescribeQuorumResponseData; | ||
| import org.apache.kafka.common.utils.LogContext; | ||
| import org.apache.kafka.raft.internals.BatchAccumulator; | ||
| import org.slf4j.Logger; | ||
|
|
@@ -31,6 +32,7 @@ | |
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.OptionalInt; | ||
| import java.util.OptionalLong; | ||
| import java.util.Set; | ||
| import java.util.stream.Collectors; | ||
|
|
@@ -205,10 +207,10 @@ private boolean updateHighWatermark() { | |
| /** | ||
| * Update the local replica state. | ||
| * | ||
| * See {@link #updateReplicaState(int, long, LogOffsetMetadata)} | ||
| * See {@link #updateReplicaState(int, long, LogOffsetMetadata, Long)} | ||
| */ | ||
| public boolean updateLocalState(long fetchTimestamp, LogOffsetMetadata logOffsetMetadata) { | ||
| return updateReplicaState(localId, fetchTimestamp, logOffsetMetadata); | ||
| return updateReplicaState(localId, fetchTimestamp, logOffsetMetadata, logOffsetMetadata.offset); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -217,20 +219,36 @@ public boolean updateLocalState(long fetchTimestamp, LogOffsetMetadata logOffset | |
| * @param replicaId replica id | ||
| * @param fetchTimestamp fetch timestamp | ||
| * @param logOffsetMetadata new log offset and metadata | ||
| * @param leaderLogEndOffset current log end offset of the leader | ||
| * @return true if the high watermark is updated too | ||
| */ | ||
| public boolean updateReplicaState(int replicaId, | ||
| long fetchTimestamp, | ||
| LogOffsetMetadata logOffsetMetadata) { | ||
| public boolean updateReplicaState( | ||
| int replicaId, | ||
| long fetchTimestamp, | ||
| LogOffsetMetadata logOffsetMetadata, | ||
| Long leaderLogEndOffset | ||
|
niket-goel marked this conversation as resolved.
Outdated
|
||
| ) { | ||
| // Ignore fetches from negative replica id, as it indicates | ||
| // the fetch is from non-replica. For example, a consumer. | ||
| if (replicaId < 0) { | ||
| return false; | ||
| } | ||
|
|
||
| ReplicaState state = getReplicaState(replicaId); | ||
| state.updateFetchTimestamp(fetchTimestamp); | ||
| return updateEndOffset(state, logOffsetMetadata); | ||
|
|
||
| // Only proceed with updating the states if the offset update is valid | ||
| verifyEndOffsetUpdate(state, logOffsetMetadata); | ||
|
|
||
| // Update the Last CaughtUp Time | ||
| if (logOffsetMetadata.offset >= leaderLogEndOffset) { | ||
| state.updateLastCaughtUpTimestamp(fetchTimestamp); | ||
|
Contributor
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. We do these updates before the call to |
||
| } else if (logOffsetMetadata.offset >= state.lastFetchLeaderLogEndOffset.orElse(-1L)) { | ||
| state.updateLastCaughtUpTimestamp(state.lastFetchTimestamp.orElse(-1L)); | ||
| } | ||
|
|
||
| state.updateFetchTimestamp(fetchTimestamp, leaderLogEndOffset); | ||
| return updateEndOffset(state, logOffsetMetadata, false); | ||
|
|
||
|
niket-goel marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| public List<Integer> nonLeaderVotersByDescendingFetchOffset() { | ||
|
|
@@ -246,20 +264,30 @@ private List<ReplicaState> followersByDescendingFetchOffset() { | |
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| private boolean updateEndOffset(ReplicaState state, | ||
| LogOffsetMetadata endOffsetMetadata) { | ||
| private void verifyEndOffsetUpdate( | ||
| ReplicaState state, | ||
| LogOffsetMetadata endOffsetMetadata | ||
| ) { | ||
| state.endOffset.ifPresent(currentEndOffset -> { | ||
| if (currentEndOffset.offset > endOffsetMetadata.offset) { | ||
| if (state.nodeId == localId) { | ||
| throw new IllegalStateException("Detected non-monotonic update of local " + | ||
| "end offset: " + currentEndOffset.offset + " -> " + endOffsetMetadata.offset); | ||
| "end offset: " + currentEndOffset.offset + " -> " + endOffsetMetadata.offset); | ||
|
niket-goel marked this conversation as resolved.
Outdated
|
||
| } else { | ||
| log.warn("Detected non-monotonic update of fetch offset from nodeId {}: {} -> {}", | ||
| state.nodeId, currentEndOffset.offset, endOffsetMetadata.offset); | ||
| state.nodeId, currentEndOffset.offset, endOffsetMetadata.offset); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| } | ||
| private boolean updateEndOffset( | ||
| ReplicaState state, | ||
|
niket-goel marked this conversation as resolved.
Outdated
|
||
| LogOffsetMetadata endOffsetMetadata, | ||
| boolean verifyUpdate | ||
| ) { | ||
| if (verifyUpdate) { | ||
| verifyEndOffsetUpdate(state, endOffsetMetadata); | ||
| } | ||
| state.endOffset = Optional.of(endOffsetMetadata); | ||
| state.hasAcknowledgedLeader = true; | ||
| return isVoter(state.nodeId) && updateHighWatermark(); | ||
|
|
@@ -290,13 +318,30 @@ private ReplicaState getReplicaState(int remoteNodeId) { | |
| return state; | ||
| } | ||
|
|
||
| Map<Integer, Long> getVoterEndOffsets() { | ||
| return getReplicaEndOffsets(voterStates); | ||
| List<DescribeQuorumResponseData.ReplicaState> quorumResponseVoterStates(long currentTimeMs) { | ||
| return quorumResponseReplicaStates(voterStates, OptionalInt.of(localId), currentTimeMs); | ||
| } | ||
|
|
||
| Map<Integer, Long> getObserverStates(final long currentTimeMs) { | ||
| List<DescribeQuorumResponseData.ReplicaState> quorumResponseObserverStates(long currentTimeMs) { | ||
| clearInactiveObservers(currentTimeMs); | ||
| return getReplicaEndOffsets(observerStates); | ||
| return quorumResponseReplicaStates(observerStates, OptionalInt.empty(), currentTimeMs); | ||
| } | ||
|
|
||
| private static <R extends ReplicaState> List<DescribeQuorumResponseData.ReplicaState> quorumResponseReplicaStates( | ||
|
niket-goel marked this conversation as resolved.
Outdated
|
||
| Map<Integer, R> state, | ||
| OptionalInt leaderId, | ||
|
niket-goel marked this conversation as resolved.
Outdated
|
||
| long currentTimeMs) { | ||
| Map<Integer, Long> replicaEndOffsets = getReplicaEndOffsets(state); | ||
|
hachikuji marked this conversation as resolved.
Outdated
|
||
| Map<Integer, Long> replicaLastFetchTimes = getReplicaLastFetchTimes(state); | ||
| Map<Integer, Long> replicaLastCaughtUpTimes = getReplicaLastCaughtUpTimes(state, leaderId, currentTimeMs); | ||
|
|
||
| return replicaEndOffsets.entrySet().stream() | ||
| .map(entry -> new DescribeQuorumResponseData.ReplicaState() | ||
| .setReplicaId(entry.getKey()) | ||
| .setLogEndOffset(entry.getValue()) | ||
| .setLastFetchTimestamp(replicaLastFetchTimes.get(entry.getKey())) | ||
| .setLastCaughtUpTimestamp(replicaLastCaughtUpTimes.get(entry.getKey()))) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| private static <R extends ReplicaState> Map<Integer, Long> getReplicaEndOffsets( | ||
|
|
@@ -308,6 +353,25 @@ private static <R extends ReplicaState> Map<Integer, Long> getReplicaEndOffsets( | |
| ); | ||
| } | ||
|
|
||
| private static <R extends ReplicaState> Map<Integer, Long> getReplicaLastFetchTimes( | ||
| Map<Integer, R> replicaStates) { | ||
| return replicaStates.entrySet().stream() | ||
| .collect(Collectors.toMap(Map.Entry::getKey, | ||
| e -> e.getValue().lastFetchTimestamp.orElse(-1L)) | ||
| ); | ||
| } | ||
|
|
||
| private static <R extends ReplicaState> Map<Integer, Long> getReplicaLastCaughtUpTimes( | ||
| Map<Integer, R> replicaStates, | ||
| OptionalInt leaderId, | ||
| final long currentTimeMs | ||
| ) { | ||
| return replicaStates.entrySet().stream() | ||
| .collect(Collectors.toMap(Map.Entry::getKey, | ||
| e -> e.getKey() == leaderId.orElse(-1) ? currentTimeMs : e.getValue().lastCaughtUpTimestamp.orElse(-1L)) | ||
| ); | ||
| } | ||
|
|
||
| private void clearInactiveObservers(final long currentTimeMs) { | ||
| observerStates.entrySet().removeIf( | ||
| integerReplicaStateEntry -> | ||
|
|
@@ -323,19 +387,30 @@ private static class ReplicaState implements Comparable<ReplicaState> { | |
| final int nodeId; | ||
| Optional<LogOffsetMetadata> endOffset; | ||
| OptionalLong lastFetchTimestamp; | ||
| OptionalLong lastFetchLeaderLogEndOffset; | ||
| OptionalLong lastCaughtUpTimestamp; | ||
| boolean hasAcknowledgedLeader; | ||
|
|
||
| public ReplicaState(int nodeId, boolean hasAcknowledgedLeader) { | ||
| this.nodeId = nodeId; | ||
| this.endOffset = Optional.empty(); | ||
| this.lastFetchTimestamp = OptionalLong.empty(); | ||
| this.lastFetchLeaderLogEndOffset = OptionalLong.empty(); | ||
| this.lastCaughtUpTimestamp = OptionalLong.empty(); | ||
| this.hasAcknowledgedLeader = hasAcknowledgedLeader; | ||
| } | ||
|
|
||
| void updateFetchTimestamp(long currentFetchTimeMs) { | ||
| void updateFetchTimestamp(long currentFetchTimeMs, long leaderLogEndOffset) { | ||
| // To be resilient to system time shifts we do not strictly | ||
| // require the timestamp be monotonically increasing. | ||
| lastFetchTimestamp = OptionalLong.of(Math.max(lastFetchTimestamp.orElse(-1L), currentFetchTimeMs)); | ||
| lastFetchLeaderLogEndOffset = OptionalLong.of(leaderLogEndOffset); | ||
| } | ||
|
|
||
| void updateLastCaughtUpTimestamp(long lastCaughtUpTime) { | ||
| // This value relies on the fetch timestamp which does not | ||
| // require monotonicity | ||
| lastCaughtUpTimestamp = OptionalLong.of(Math.max(lastCaughtUpTimestamp.orElse(-1L), lastCaughtUpTime)); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -353,10 +428,12 @@ else if (!that.endOffset.isPresent()) | |
| @Override | ||
| public String toString() { | ||
| return String.format( | ||
| "ReplicaState(nodeId=%d, endOffset=%s, lastFetchTimestamp=%s, hasAcknowledgedLeader=%s)", | ||
| "ReplicaState(nodeId=%d, endOffset=%s, lastFetchTimestamp=%s, " + | ||
| " lastCaughtUpTimestamp=%s, hasAcknowledgedLeader=%s)", | ||
|
niket-goel marked this conversation as resolved.
Outdated
|
||
| nodeId, | ||
| endOffset, | ||
| lastFetchTimestamp, | ||
| lastCaughtUpTimestamp, | ||
| hasAcknowledgedLeader | ||
| ); | ||
| } | ||
|
|
||
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.
A more straightforward way to assert the voter/observer sets is in
DescribeQuorumRequestTest: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.
That check already exists in the
DescribeQuorumRequestTest. The count validation here is just a sanity check.Uh oh!
There was an error while loading. Please reload this page.
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.
We are checking the size here and all of the individual replicaIds below. For example:
I am just offering a more concise and complete way to do that.
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.
You may have missed my comment above. The nice thing about asserting the set directly is that it ensures that all expected voters/observers are present in the result.