-
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 1 commit
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,52 @@ 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(0, quorumInfo.observers.size) | ||
| assertEquals(3, quorumInfo.voters.size) | ||
|
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. A more straightforward way to assert the voter/observer sets is in val voterData = partitionData.currentVoters.asScala
assertEquals(cluster.controllerIds().asScala, voterData.map(_.replicaId).toSet);
val observerData = partitionData.observers.asScala
assertEquals(cluster.brokerIds().asScala, observerData.map(_.replicaId).toSet);
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. That check already exists in the
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 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.
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. 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. |
||
| 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.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 |
|---|---|---|
|
|
@@ -1016,7 +1016,7 @@ private FetchResponseData tryCompleteFetchRequest( | |
| if (validOffsetAndEpoch.kind() == ValidOffsetAndEpoch.Kind.VALID) { | ||
| LogFetchInfo info = log.read(fetchOffset, Isolation.UNCOMMITTED); | ||
|
|
||
| if (state.updateReplicaState(replicaId, currentTimeMs, info.startOffsetMetadata)) { | ||
| if (state.updateReplicaState(replicaId, currentTimeMs, info.startOffsetMetadata, log.endOffset().offset)) { | ||
| onUpdateLeaderHighWatermark(state, currentTimeMs); | ||
| } | ||
|
|
||
|
|
@@ -1179,8 +1179,12 @@ private DescribeQuorumResponseData handleDescribeQuorumRequest( | |
| leaderState.localId(), | ||
| leaderState.epoch(), | ||
| leaderState.highWatermark().isPresent() ? leaderState.highWatermark().get().offset : -1, | ||
| convertToReplicaStates(leaderState.getVoterEndOffsets()), | ||
| convertToReplicaStates(leaderState.getObserverStates(currentTimeMs)) | ||
| convertToReplicaStates(leaderState.getVoterEndOffsets(), | ||
| leaderState.getVoterLastFetchTimes(), | ||
|
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. I think it would be a bit simpler to push creation of the class LeaderState {
...
List<ReplicaState> voterStates(long currentTimeMs);
List<ReplicaState> observerStates(long currentTimeMs);
}Then we wouldn't need all the boilerplate methods to create all the different Map variations. Also, then the sanity checks in |
||
| leaderState.getVoterLastCaughUpTimes(currentTimeMs)), | ||
| convertToReplicaStates(leaderState.getObserverEndOffsets(currentTimeMs), | ||
| leaderState.getObserverLastFetchTimes(currentTimeMs), | ||
| leaderState.getObserverLastCaughUpTimes(currentTimeMs)) | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -1418,11 +1422,25 @@ private boolean handleFetchSnapshotResponse( | |
| return true; | ||
| } | ||
|
|
||
| List<ReplicaState> convertToReplicaStates(Map<Integer, Long> replicaEndOffsets) { | ||
| List<ReplicaState> convertToReplicaStates( | ||
| Map<Integer, Long> replicaEndOffsets, | ||
| Map<Integer, Long> replicaLastFetchTimes, | ||
| Map<Integer, Long> replicaLastCaughtUpTimes | ||
| ) { | ||
| if (replicaEndOffsets.size() != replicaLastFetchTimes.size() | ||
| || replicaEndOffsets.size() != replicaLastCaughtUpTimes.size()) { | ||
| throw new IllegalStateException("The replica states were not coherent." + | ||
| "End Offsets: " + replicaEndOffsets.toString() + | ||
| "Last Fetch Times: " + replicaLastFetchTimes.toString() + | ||
| "Last CaughtUp times: " + replicaLastCaughtUpTimes.toString() | ||
| ); | ||
| } | ||
| return replicaEndOffsets.entrySet().stream() | ||
| .map(entry -> new ReplicaState() | ||
| .setReplicaId(entry.getKey()) | ||
| .setLogEndOffset(entry.getValue())) | ||
| .setLogEndOffset(entry.getValue()) | ||
| .setLastFetchTimestamp(replicaLastFetchTimes.get(entry.getKey())) | ||
| .setLastCaughtUpTimestamp(replicaLastCaughtUpTimes.get(entry.getKey()))) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,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 +206,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,19 +218,30 @@ 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); | ||
| // 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); | ||
| } | ||
|
|
||
|
|
@@ -294,11 +306,29 @@ Map<Integer, Long> getVoterEndOffsets() { | |
| return getReplicaEndOffsets(voterStates); | ||
| } | ||
|
|
||
| Map<Integer, Long> getObserverStates(final long currentTimeMs) { | ||
| Map<Integer, Long> getVoterLastFetchTimes() { | ||
| return getReplicaLastFetchTimes(voterStates); | ||
| } | ||
|
|
||
| Map<Integer, Long> getVoterLastCaughUpTimes(final long currentTimeMs) { | ||
| return getReplicaLastCaughtUpTimes(voterStates, OptionalInt.of(localId), currentTimeMs); | ||
| } | ||
|
|
||
| Map<Integer, Long> getObserverEndOffsets(final long currentTimeMs) { | ||
| clearInactiveObservers(currentTimeMs); | ||
| return getReplicaEndOffsets(observerStates); | ||
| } | ||
|
|
||
| Map<Integer, Long> getObserverLastFetchTimes(final long currentTimeMs) { | ||
| clearInactiveObservers(currentTimeMs); | ||
| return getReplicaLastFetchTimes(observerStates); | ||
| } | ||
|
|
||
| Map<Integer, Long> getObserverLastCaughUpTimes(final long currentTimeMs) { | ||
| clearInactiveObservers(currentTimeMs); | ||
| return getReplicaLastCaughtUpTimes(observerStates, OptionalInt.empty(), currentTimeMs); | ||
| } | ||
|
|
||
| private static <R extends ReplicaState> Map<Integer, Long> getReplicaEndOffsets( | ||
| Map<Integer, R> replicaStates) { | ||
| return replicaStates.entrySet().stream() | ||
|
|
@@ -308,6 +338,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, | ||
|
niket-goel marked this conversation as resolved.
Outdated
|
||
| 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 +372,30 @@ private static class ReplicaState implements Comparable<ReplicaState> { | |
| final int nodeId; | ||
| Optional<LogOffsetMetadata> endOffset; | ||
| OptionalLong lastFetchTimestamp; | ||
| OptionalLong lastfetchLeaderLogEndOffset; | ||
|
niket-goel marked this conversation as resolved.
Outdated
|
||
| 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 +413,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 | ||
| ); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.