This is an automated email from the ASF dual-hosted git repository.

mimaison 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 226ce2d8354 KAFKA-20832: Fix cordon logic with user specified 
assignments (#22926)
226ce2d8354 is described below

commit 226ce2d8354694625ce98c7213df724f6fead22e
Author: Mickael Maison <[email protected]>
AuthorDate: Mon Jul 27 14:16:02 2026 +0200

    KAFKA-20832: Fix cordon logic with user specified assignments (#22926)
    
    Fix the createTopics, createPartitions and alterPartitions code paths
    
    Reviewers: Paolo Patierno <[email protected]>, Gaurav Narula
     <[email protected]>, Luke Chen <[email protected]>
---
 .../controller/ReplicationControlManager.java      |  4 ++
 .../controller/ReplicationControlManagerTest.java  | 16 +++++-
 .../server/CordonedLogDirsIntegrationTest.java     | 57 +++++++++++++++++++++-
 3 files changed, 74 insertions(+), 3 deletions(-)

diff --git 
a/metadata/src/main/java/org/apache/kafka/controller/ReplicationControlManager.java
 
b/metadata/src/main/java/org/apache/kafka/controller/ReplicationControlManager.java
index 4c8a24f0faa..c915615e1ff 100644
--- 
a/metadata/src/main/java/org/apache/kafka/controller/ReplicationControlManager.java
+++ 
b/metadata/src/main/java/org/apache/kafka/controller/ReplicationControlManager.java
@@ -2015,6 +2015,10 @@ public class ReplicationControlManager {
                     "assignment includes broker " + brokerId + ", but no such 
broker is " +
                     "registered.");
             }
+            if 
(!clusterControl.brokerRegistrations().get(brokerId).hasUncordonedDirs()) {
+                throw new InvalidReplicaAssignmentException("The manual 
partition " +
+                    "assignment includes broker " + brokerId + ", but all its 
log directories are cordoned.");
+            }
             if (brokerId.equals(prevBrokerId)) {
                 throw new InvalidReplicaAssignmentException("The manual 
partition " +
                     "assignment includes the broker " + prevBrokerId + " more 
than " +
diff --git 
a/metadata/src/test/java/org/apache/kafka/controller/ReplicationControlManagerTest.java
 
b/metadata/src/test/java/org/apache/kafka/controller/ReplicationControlManagerTest.java
index b6b66a906e8..71dc3c00cfe 100644
--- 
a/metadata/src/test/java/org/apache/kafka/controller/ReplicationControlManagerTest.java
+++ 
b/metadata/src/test/java/org/apache/kafka/controller/ReplicationControlManagerTest.java
@@ -543,6 +543,15 @@ public class ReplicationControlManagerTest {
                     .collect(Collectors.toSet());
         }
 
+        void cordonBroker(int brokerId) {
+            List<Uuid> dirs = 
clusterControl.brokerRegistrations().get(brokerId).directories();
+            BrokerRegistrationChangeRecord record = new 
BrokerRegistrationChangeRecord()
+                .setBrokerId(brokerId)
+                .setBrokerEpoch(defaultBrokerEpoch(brokerId))
+                .setCordonedLogDirs(dirs);
+            clusterControl.replay(record);
+        }
+
     }
 
     static CreateTopicsResponseData withoutConfigs(CreateTopicsResponseData 
data) {
@@ -1818,7 +1827,8 @@ public class ReplicationControlManagerTest {
     @Test
     public void testValidateBadManualPartitionAssignments() {
         ReplicationControlTestContext ctx = new 
ReplicationControlTestContext.Builder().build();
-        ctx.registerBrokers(1, 2);
+        ctx.registerBrokers(0, 1, 2);
+        ctx.cordonBroker(0);
         assertEquals("The manual partition assignment includes an empty 
replica list.",
             assertThrows(InvalidReplicaAssignmentException.class, () ->
                 
ctx.replicationControl.validateManualPartitionAssignment(partitionAssignment(List.of()),
@@ -1827,6 +1837,10 @@ public class ReplicationControlManagerTest {
             "broker is registered.", 
assertThrows(InvalidReplicaAssignmentException.class, () ->
                 
ctx.replicationControl.validateManualPartitionAssignment(partitionAssignment(List.of(1,
 2, 3)),
                     OptionalInt.empty())).getMessage());
+        assertEquals("The manual partition assignment includes broker 0, but 
all its log " +
+            "directories are cordoned.", 
assertThrows(InvalidReplicaAssignmentException.class, () ->
+                
ctx.replicationControl.validateManualPartitionAssignment(partitionAssignment(List.of(0,
 1, 2)),
+                    OptionalInt.empty())).getMessage());
         assertEquals("The manual partition assignment includes the broker 2 
more than " +
             "once.", assertThrows(InvalidReplicaAssignmentException.class, () 
->
                 
ctx.replicationControl.validateManualPartitionAssignment(partitionAssignment(List.of(1,
 2, 2)),
diff --git 
a/server/src/test/java/org/apache/kafka/server/CordonedLogDirsIntegrationTest.java
 
b/server/src/test/java/org/apache/kafka/server/CordonedLogDirsIntegrationTest.java
index 558c7324d4e..495678d746c 100644
--- 
a/server/src/test/java/org/apache/kafka/server/CordonedLogDirsIntegrationTest.java
+++ 
b/server/src/test/java/org/apache/kafka/server/CordonedLogDirsIntegrationTest.java
@@ -27,6 +27,7 @@ import org.apache.kafka.clients.admin.LogDirDescription;
 import org.apache.kafka.clients.admin.NewPartitionReassignment;
 import org.apache.kafka.clients.admin.NewPartitions;
 import org.apache.kafka.clients.admin.NewTopic;
+import org.apache.kafka.clients.admin.TopicDescription;
 import org.apache.kafka.clients.admin.UpdateFeaturesOptions;
 import org.apache.kafka.clients.admin.UpdateFeaturesResult;
 import org.apache.kafka.common.TopicPartition;
@@ -57,6 +58,7 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static 
org.apache.kafka.server.config.ServerLogConfigs.CORDONED_LOG_DIRS_CONFIG;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -162,18 +164,32 @@ public class CordonedLogDirsIntegrationTest {
             setCordonedLogDirs(admin, logDirsBroker0, BROKER_0);
             assertCordonedLogDirs(admin, logDirsBroker0);
 
-            // We can't create new topics or partitions
+            // We can't create new topics
             Set<NewTopic> newTopics = newTopic(TOPIC2);
             Throwable ee = assertThrows(ExecutionException.class, () ->
                 admin.createTopics(newTopics).all().get()
             );
             assertInstanceOf(InvalidReplicationFactorException.class, 
ee.getCause());
+
+            // We can't create new topics specifying the replica assignment
+            ee = assertThrows(ExecutionException.class, () ->
+                admin.createTopics(Set.of(new NewTopic(TOPIC2, Map.of(0, 
List.of(0))))).all().get()
+            );
+            assertInstanceOf(InvalidReplicaAssignmentException.class, 
ee.getCause());
+
+            // We can't create new partitions
             Map<String, NewPartitions> newPartitions = Map.of(TOPIC1, 
NewPartitions.increaseTo(2));
             ee = assertThrows(ExecutionException.class, () ->
                 admin.createPartitions(newPartitions).all().get()
             );
             assertInstanceOf(InvalidReplicationFactorException.class, 
ee.getCause());
 
+            // We can't create new partitions specifying the replica assignment
+            ee = assertThrows(ExecutionException.class, () ->
+                    admin.createPartitions(Map.of(TOPIC1, 
NewPartitions.increaseTo(2, List.of(List.of(0))))).all().get()
+            );
+            assertInstanceOf(InvalidReplicaAssignmentException.class, 
ee.getCause());
+
             // Uncordon all log dirs
             setCordonedLogDirs(admin, List.of(logDirsBroker0.get(0)), 
BROKER_0);
             assertCordonedLogDirs(admin, List.of(logDirsBroker0.get(0)));
@@ -207,7 +223,7 @@ public class CordonedLogDirsIntegrationTest {
     }
 
     @ClusterTest
-    public void testReassignWithCordonedLogDirs() throws Exception {
+    public void testAlterReplicaWithCordonedLogDirs() throws Exception {
         TopicPartitionReplica replica = new TopicPartitionReplica(TOPIC1, 0, 
0);
         try (Admin admin = clusterInstance.admin()) {
             admin.createTopics(newTopic(TOPIC1)).all().get();
@@ -235,6 +251,43 @@ public class CordonedLogDirsIntegrationTest {
         }
     }
 
+    @ClusterTest(
+            brokers = 2
+    )
+    public void testAlterPartitionWithCordonedLogDirs() throws Exception {
+        Set<Integer> allBrokers = new HashSet<>(clusterInstance.brokerIds());
+        try (Admin admin = clusterInstance.admin()) {
+            admin.createTopics(newTopic(TOPIC1)).all().get();
+
+            // Find the broker that hosts the partition and cordon the other 
broker
+            AtomicReference<Integer> partitionBroker = new AtomicReference<>();
+            TestUtils.waitForCondition(() -> {
+                TopicDescription td = 
admin.describeTopics(List.of(TOPIC1)).allTopicNames().get().get(TOPIC1);
+                if (td == null) return false;
+                
partitionBroker.set(td.partitions().get(0).replicas().get(0).id());
+                return true;
+            }, 10_000, "Unable to find broker hosting topic " + TOPIC1);
+            allBrokers.remove(partitionBroker.get());
+            assertEquals(1, allBrokers.size());
+            int otherBroker = allBrokers.iterator().next();
+            setCordonedLogDirs(admin, List.of("*"), new 
ConfigResource(ConfigResource.Type.BROKER, String.valueOf(otherBroker)));
+
+            // We can't move a partition to the other broker
+            Throwable ee = assertThrows(ExecutionException.class, () ->
+                    admin.alterPartitionReassignments(Map.of(
+                            new TopicPartition(TOPIC1, 0),
+                            Optional.of(new 
NewPartitionReassignment(List.of(otherBroker))))).all().get()
+            );
+            assertInstanceOf(InvalidReplicaAssignmentException.class, 
ee.getCause());
+
+            // After uncordoning the other broker, we can move the replica on 
it
+            setCordonedLogDirs(admin, List.of(), new 
ConfigResource(ConfigResource.Type.BROKER, String.valueOf(otherBroker)));
+            admin.alterPartitionReassignments(Map.of(
+                    new TopicPartition(TOPIC1, 0),
+                    Optional.of(new 
NewPartitionReassignment(List.of(otherBroker))))).all().get();
+        }
+    }
+
     @ClusterTest()
     public void testCordonUnknownLogDirs() {
         try (Admin admin = clusterInstance.admin()) {

Reply via email to