This is an automated email from the ASF dual-hosted git repository.
chia7712 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git
The following commit(s) were added to refs/heads/trunk by this push:
new 30c937d350a KAFKA-20746 NPE in MetadataCache#toCluster (#22723)
30c937d350a is described below
commit 30c937d350a8ab3a867f965d609e549885e0320c
Author: Kuan-Po Tseng <[email protected]>
AuthorDate: Thu Jul 9 23:49:27 2026 +0800
KAFKA-20746 NPE in MetadataCache#toCluster (#22723)
MetadataCache#toCluster builds a Cluster view for quota callbacks.
Previously, it filtered fenced brokers out of the broker-to-node map,
but partition metadata can still reference fenced brokers as replicas.
When such replicas were converted to PartitionInfo, the missing broker
entry could cause a NullPointerException.
Include all registered brokers in the broker-to-node map so fenced
replicas can be represented as offline replicas in the generated
Cluster.
Reviewers: Luke Chen <[email protected]>, Chia-Ping Tsai
<[email protected]>
---
.../org/apache/kafka/metadata/MetadataCache.java | 10 ++--
.../apache/kafka/metadata/MetadataCacheTest.java | 56 ++++++++++++++++++++++
2 files changed, 61 insertions(+), 5 deletions(-)
diff --git
a/metadata/src/main/java/org/apache/kafka/metadata/MetadataCache.java
b/metadata/src/main/java/org/apache/kafka/metadata/MetadataCache.java
index 555f1866bd8..481ab4ff2f8 100644
--- a/metadata/src/main/java/org/apache/kafka/metadata/MetadataCache.java
+++ b/metadata/src/main/java/org/apache/kafka/metadata/MetadataCache.java
@@ -36,11 +36,11 @@ import org.apache.kafka.server.common.MetadataVersion;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
@@ -147,10 +147,8 @@ public interface MetadataCache extends ConfigRepository {
boolean ignoreTopicsWithExceptions);
static Cluster toCluster(String clusterId, MetadataImage image) {
- Map<Integer, List<Node>> brokerToNodes = new HashMap<>();
- image.cluster().brokers().values().stream()
- .filter(broker -> !broker.fenced())
- .forEach(broker -> brokerToNodes.put(broker.id(), broker.nodes()));
+ Map<Integer, List<Node>> brokerToNodes =
image.cluster().brokers().values().stream()
+ .collect(Collectors.toMap(BrokerRegistration::id,
BrokerRegistration::nodes));
List<PartitionInfo> partitionInfos = new ArrayList<>();
Set<String> internalTopics = new HashSet<>();
@@ -168,6 +166,7 @@ public interface MetadataCache extends ConfigRepository {
toArray(partition.isr, brokerToNodes),
getOfflineReplicas(image, partition).stream()
.map(brokerToNodes::get)
+ .filter(Objects::nonNull)
.flatMap(Collection::stream)
.toArray(Node[]::new)
));
@@ -196,6 +195,7 @@ public interface MetadataCache extends ConfigRepository {
private static Node[] toArray(int[] replicas, Map<Integer, List<Node>>
brokerToNodes) {
return Arrays.stream(replicas)
.mapToObj(brokerToNodes::get)
+ .filter(Objects::nonNull)
.flatMap(Collection::stream)
.toArray(Node[]::new);
}
diff --git
a/metadata/src/test/java/org/apache/kafka/metadata/MetadataCacheTest.java
b/metadata/src/test/java/org/apache/kafka/metadata/MetadataCacheTest.java
index 7df67f23670..c193e48b58a 100644
--- a/metadata/src/test/java/org/apache/kafka/metadata/MetadataCacheTest.java
+++ b/metadata/src/test/java/org/apache/kafka/metadata/MetadataCacheTest.java
@@ -16,8 +16,10 @@
*/
package org.apache.kafka.metadata;
+import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.DirectoryId;
import org.apache.kafka.common.Node;
+import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.Uuid;
import org.apache.kafka.common.message.DescribeTopicPartitionsResponseData;
@@ -42,6 +44,7 @@ import org.apache.kafka.server.common.KRaftVersion;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
@@ -50,6 +53,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1008,6 +1012,58 @@ public class MetadataCacheTest {
private record Broker(int id, List<Uuid> dirs) {
}
+ @Test
+ public void testToClusterIncludesFencedReplicasAsOffline() {
+ MetadataDelta delta = new MetadataDelta.Builder().build();
+ RegisterBrokerRecord broker0RegisterRecord = new RegisterBrokerRecord()
+ .setBrokerId(0)
+ .setFenced(false)
+ .setEndPoints(new BrokerEndpointCollection(List.of(
+ new BrokerEndpoint()
+ .setSecurityProtocol(SecurityProtocol.PLAINTEXT.id)
+ .setPort((short) 9092)
+ .setName("PLAINTEXT")
+ .setHost("broker-0")
+ )));
+ // Register broker 1 as fenced
+ RegisterBrokerRecord broker1RegisterRecord = new RegisterBrokerRecord()
+ .setBrokerId(1)
+ .setFenced(true)
+ .setEndPoints(new BrokerEndpointCollection(List.of(
+ new BrokerEndpoint()
+ .setSecurityProtocol(SecurityProtocol.PLAINTEXT.id)
+ .setPort((short) 9092)
+ .setName("PLAINTEXT")
+ .setHost("broker-1")
+ )));
+ delta.replay(broker0RegisterRecord);
+ delta.replay(broker1RegisterRecord);
+
+ String topicName = "foo";
+ Uuid topicId = Uuid.randomUuid();
+ int partitionId = 0;
+ int broker0 = broker0RegisterRecord.brokerId();
+ int broker1 = broker1RegisterRecord.brokerId();
+ delta.replay(new TopicRecord().setTopicId(topicId).setName(topicName));
+ // Broker 1 is not in the ISR, but it remains in the replica set.
+ // This used to trigger a NullPointerException when fenced brokers were
+ // omitted from the broker map used to build replica metadata.
+ delta.replay(new PartitionRecord()
+ .setTopicId(topicId)
+ .setPartitionId(partitionId)
+ .setReplicas(List.of(broker0, broker1))
+ .setLeader(broker0)
+ .setIsr(List.of(broker0)));
+
+ Cluster cluster = assertDoesNotThrow(() ->
+ MetadataCache.toCluster("cluster-id",
delta.apply(MetadataProvenance.EMPTY)));
+ PartitionInfo partition = cluster.partition(new
TopicPartition(topicName, partitionId));
+
+ assertEquals(List.of(broker0, broker1),
Arrays.stream(partition.replicas()).map(Node::id).toList());
+ assertEquals(List.of(broker1),
Arrays.stream(partition.offlineReplicas()).map(Node::id).toList());
+ assertTrue(cluster.nodeById(broker1).isFenced());
+ }
+
private record Partition(int id, List<Integer> replicas, List<Uuid> dirs) {
}