Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions clients/src/main/java/org/apache/kafka/clients/Metadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,14 @@ private MetadataCache handleMetadataResponse(MetadataResponse metadataResponse,
internalTopics.add(metadata.topic());
for (MetadataResponse.PartitionMetadata partitionMetadata : metadata.partitionMetadata()) {

// Even if the partition's metadata includes an error, we need to handle the update to catch new epochs
updatePartitionInfo(metadata.topic(), partitionMetadata, partitionInfo -> {
Consumer<PartitionInfo> addToPartitions = partitionInfo -> {
int epoch = partitionMetadata.leaderEpoch().orElse(RecordBatch.NO_PARTITION_LEADER_EPOCH);

@hachikuji hachikuji Dec 7, 2019

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note this was a bug. We were using the leader epoch from the response even if it was stale and we had taken the PartitionInfo from the previous update.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we have a test that covers this bug too?

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.

I believe testStaleMetadata() does that

partitions.add(new MetadataCache.PartitionInfoAndEpoch(partitionInfo, epoch));
});
};

// Even if the partition's metadata includes an error, we need to handle the update to catch new epochs
updatePartitionInfo(metadata.topic(), partitionMetadata,
metadataResponse.hasReliableLeaderEpochs(), addToPartitions);

if (partitionMetadata.error().exception() instanceof InvalidMetadataException) {
log.debug("Requesting metadata update for partition {} due to error {}",
Expand All @@ -328,10 +331,10 @@ private MetadataCache handleMetadataResponse(MetadataResponse metadataResponse,
*/
private void updatePartitionInfo(String topic,
MetadataResponse.PartitionMetadata partitionMetadata,
boolean hasReliableLeaderEpoch,
Consumer<PartitionInfo> partitionInfoConsumer) {

TopicPartition tp = new TopicPartition(topic, partitionMetadata.partition());
if (partitionMetadata.leaderEpoch().isPresent()) {
if (hasReliableLeaderEpoch && partitionMetadata.leaderEpoch().isPresent()) {
int newEpoch = partitionMetadata.leaderEpoch().get();
// If the received leader epoch is at least the same as the previous one, update the metadata
if (updateLastSeenEpoch(tp, newEpoch, oldEpoch -> newEpoch >= oldEpoch, false)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,25 @@ public class MetadataResponse extends AbstractResponse {

private final MetadataResponseData data;
private volatile Holder holder;
private final boolean hasReliableLeaderEpochs;

public MetadataResponse(MetadataResponseData data) {
this.data = data;
this(data, true);
}

public MetadataResponse(Struct struct, short version) {
this(new MetadataResponseData(struct, version));
// Prior to Kafka version 2.4 (which coincides with Metadata version 9), the broker
// does not propagate leader epoch information accurately while a reassignment is in
// progress. Relying on a stale epoch can lead to FENCED_LEADER_EPOCH errors which
// can prevent consumption throughout the course of a reassignment. It is safer in
// this case to revert to the behavior in previous protocol versions which checks
// leader status only.
this(new MetadataResponseData(struct, version), version >= 9);
}

private MetadataResponse(MetadataResponseData data, boolean hasReliableLeaderEpochs) {
this.data = data;
this.hasReliableLeaderEpochs = hasReliableLeaderEpochs;
}

@Override
Expand Down Expand Up @@ -205,6 +217,18 @@ public String clusterId() {
return this.data.clusterId();
}

/**
* Check whether the leader epochs returned from the response can be relied on
* for epoch validation in Fetch, ListOffsets, and OffsetsForLeaderEpoch requests.
* If not, then the client will not retain the leader epochs and hence will not
* forward them in requests.
*
* @return true if the epoch can be used for validation
*/
public boolean hasReliableLeaderEpochs() {
return hasReliableLeaderEpochs;
}

public static MetadataResponse parse(ByteBuffer buffer, short version) {
return new MetadataResponse(ApiKeys.METADATA.responseSchema(version).read(buffer), version);
}
Expand Down
12 changes: 7 additions & 5 deletions core/src/main/scala/kafka/controller/KafkaController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1081,14 +1081,16 @@ class KafkaController(val config: KafkaConfig,
val UpdateLeaderAndIsrResult(finishedUpdates, _) =
zkClient.updateLeaderAndIsr(immutable.Map(partition -> newLeaderAndIsr), epoch, controllerContext.epochZkVersion)

finishedUpdates.headOption.map {
case (partition, Right(leaderAndIsr)) =>
finalLeaderIsrAndControllerEpoch = Some(LeaderIsrAndControllerEpoch(leaderAndIsr, epoch))
finishedUpdates.get(partition).exists {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: I think it would probably be clearer to just go with a Some/None pattern match given the side-effects we're doing below (throwing exceptions, mutations, etc).

case Right(leaderAndIsr) =>
val leaderIsrAndControllerEpoch = LeaderIsrAndControllerEpoch(leaderAndIsr, epoch)
controllerContext.partitionLeadershipInfo.put(partition, leaderIsrAndControllerEpoch)
finalLeaderIsrAndControllerEpoch = Some(leaderIsrAndControllerEpoch)
info(s"Updated leader epoch for partition $partition to ${leaderAndIsr.leaderEpoch}")
true
case (_, Left(e)) =>
case Left(e) =>
throw e
}.getOrElse(false)
}
case None =>
throw new IllegalStateException(s"Cannot update leader epoch for partition $partition as " +
"leaderAndIsr path is empty. This could mean we somehow tried to reassign a partition that doesn't exist")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,41 @@ class ReassignPartitionsClusterTest extends ZooKeeperTestHarness with Logging {
assertEquals(Seq(101), zkClient.getReplicasForPartition(tp0))
}

@Test
def testProduceAndConsumeWithReassignmentInProgress(): Unit = {

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.

nice test!

startBrokers(Seq(100, 101))
adminClient = createAdminClient(servers)
createTopic(zkClient, topicName, Map(tp0.partition() -> Seq(100)), servers = servers)

produceMessages(tp0.topic, 500, acks = -1, valueLength = 100 * 1000)

TestUtils.throttleAllBrokersReplication(adminClient, Seq(101), throttleBytes = 1)
TestUtils.assignThrottledPartitionReplicas(adminClient, Map(tp0 -> Seq(101)))

adminClient.alterPartitionReassignments(
Map(reassignmentEntry(tp0, Seq(100, 101))).asJava
).all().get()

awaitReassignmentInProgress(tp0)

produceMessages(tp0.topic, 500, acks = -1, valueLength = 64)
val consumer = TestUtils.createConsumer(TestUtils.getBrokerListStrFromServers(servers))
try {
consumer.assign(Seq(tp0).asJava)
pollUntilAtLeastNumRecords(consumer, numRecords = 1000)
} finally {
consumer.close()
}

assertTrue(isAssignmentInProgress(tp0))

TestUtils.resetBrokersThrottle(adminClient, Seq(101))
TestUtils.removePartitionReplicaThrottles(adminClient, Set(tp0))

waitForAllReassignmentsToComplete()
assertEquals(Seq(100, 101), zkClient.getReplicasForPartition(tp0))
}

@Test
def shouldListMovingPartitionsThroughApi(): Unit = {
startBrokers(Seq(100, 101))
Expand Down Expand Up @@ -1228,6 +1263,16 @@ class ReassignPartitionsClusterTest extends ZooKeeperTestHarness with Logging {
s"Znode ${ReassignPartitionsZNode.path} wasn't deleted", pause = pause)
}

def awaitReassignmentInProgress(topicPartition: TopicPartition): Unit = {
waitUntilTrue(() => isAssignmentInProgress(topicPartition),
"Timed out waiting for expected reassignment to begin")
}

def isAssignmentInProgress(topicPartition: TopicPartition): Boolean = {
val reassignments = adminClient.listPartitionReassignments().reassignments().get()
reassignments.asScala.get(topicPartition).isDefined
}

def waitForAllReassignmentsToComplete(pause: Long = 100L): Unit = {
waitUntilTrue(() => adminClient.listPartitionReassignments().reassignments().get().isEmpty,
s"There still are ongoing reassignments", pause = pause)
Expand Down