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 ff1f4674063 KAFKA-20215: Add integration test for log directory 
cordoning (#22815)
ff1f4674063 is described below

commit ff1f46740636a32d7d2155ea84c410b92ca07318
Author: Chang-Chi Hsu <[email protected]>
AuthorDate: Tue Aug 4 00:12:26 2026 +0800

    KAFKA-20215: Add integration test for log directory cordoning (#22815)
    
    - Added an integration test for log directory decommissioning.
    - The test verifies that a broker can restart successfully after a log
    directory has been decommissioned.
    
    Reviewers: Ken Huang <[email protected]>, Mickael Maison 
<[email protected]>
---
 .../server/CordonedLogDirsIntegrationTest.java     | 61 ++++++++++++++++++++++
 .../apache/kafka/common/test/ClusterInstance.java  |  2 +
 .../kafka/common/test/KafkaClusterTestKit.java     | 44 ++++++++++++++++
 .../test/junit/RaftClusterInvocationContext.java   |  5 ++
 4 files changed, 112 insertions(+)

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 495678d746c..c7a67051496 100644
--- 
a/server/src/test/java/org/apache/kafka/server/CordonedLogDirsIntegrationTest.java
+++ 
b/server/src/test/java/org/apache/kafka/server/CordonedLogDirsIntegrationTest.java
@@ -58,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.apache.kafka.server.config.ServerLogConfigs.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;
@@ -400,6 +401,66 @@ public class CordonedLogDirsIntegrationTest {
         }
     }
 
+    @ClusterTest(
+            brokers = 2,
+            controllers = 1
+    )
+    public void testDecommissionLogDir() throws ExecutionException, 
InterruptedException {
+        // Select the target broker
+        int brokerId = clusterInstance.brokerIds().stream().filter(id -> 
!clusterInstance.controllerIds().contains(id)).findFirst().get();
+        try (var admin = clusterInstance.admin()) {
+            List<String> logDirs = 
clusterInstance.brokers().get(brokerId).config().logDirs();
+            assertTrue(logDirs.size() > 1, "Test requires more than one log 
dir per broker");
+            String logDirToRemove = logDirs.get(logDirs.size() - 1);
+            List<String> remainingLogDirs = logDirs.subList(0, logDirs.size() 
- 1);
+
+            // Create 10 topics, replicated to every broker so brokerId is 
guaranteed to host some
+            // replicas on the log dir we're about to decommission
+            for (int i = 0; i < 10; i++) {
+                admin.createTopics(newTopic("topic" + i)).all().get();
+            }
+            TestUtils.waitForCondition(() -> 
admin.listTopics().names().get().size() == 10, 10_000, "Topics were not 
created");
+
+            ConfigResource brokerResource = new 
ConfigResource(ConfigResource.Type.BROKER, String.valueOf(brokerId));
+
+            // Cordon the log dir we're going to decommission and move any 
replicas hosted on it elsewhere
+            setCordonedLogDirs(admin, List.of(logDirToRemove), brokerResource);
+            Set<TopicPartition> partitionsToMove = new HashSet<>();
+            LogDirDescription description = 
admin.describeLogDirs(List.of(brokerId)).allDescriptions().get().get(brokerId).get(logDirToRemove);
+            partitionsToMove.addAll(description.replicaInfos().keySet());
+            assertFalse(partitionsToMove.isEmpty());
+            int target = clusterInstance.brokerIds().stream().filter(id -> id 
!= brokerId).findFirst().get();
+            movePartitions(admin, partitionsToMove, brokerId, 
Optional.of(logDirToRemove), target);
+
+            // Shut down the broker so the log dir can be physically 
decommissioned while it's offline
+            clusterInstance.brokers().get(brokerId).shutdown();
+            clusterInstance.brokers().get(brokerId).awaitShutdown();
+
+            // Uncordon the log dir via the controller while the broker is 
shut down
+            try (Admin controllerAdmin = clusterInstance.admin(Map.of(), 
true)) {
+                controllerAdmin.incrementalAlterConfigs(cordonedDirsConfig("", 
brokerResource)).all().get();
+            }
+
+            // Physically decommission the log dir: restart the
+            // broker without it in its static log.dirs config
+            Map<String, Object> propOverrides = Map.of(LOG_DIRS_CONFIG, 
String.join(",", remainingLogDirs), CORDONED_LOG_DIRS_CONFIG, "");
+            clusterInstance.restartBroker(brokerId, propOverrides);
+            clusterInstance.waitForReadyBrokers();
+
+            // The broker restarted with the reduced set of log dirs, and no 
longer reports the removed one
+            assertEquals(remainingLogDirs, 
clusterInstance.brokers().get(brokerId).config().logDirs());
+            TestUtils.waitForCondition(() ->
+                
!admin.describeLogDirs(List.of(brokerId)).allDescriptions().get().get(brokerId).containsKey(logDirToRemove),
+                10_000, "Broker " + brokerId + " is still reporting the 
removed log dir " + logDirToRemove);
+
+            // The broker is still fully functional after losing a log dir
+            
admin.createTopics(newTopic("topic-after-decommission")).all().get();
+            TestUtils.waitForCondition(() ->
+                admin.listTopics().names().get().size() == 11,
+                10_000, "Topic was not created after decommissioning a log 
dir");
+        }
+    }
+
     private void movePartitions(Admin admin, Set<TopicPartition> partitions, 
int source, Optional<String> logDir, int target) throws ExecutionException, 
InterruptedException {
         Map<TopicPartition, Optional<NewPartitionReassignment>> reassignments 
= new HashMap<>();
         for (TopicPartition partition : partitions) {
diff --git 
a/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/ClusterInstance.java
 
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/ClusterInstance.java
index cdf911f5198..dacf3f27241 100644
--- 
a/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/ClusterInstance.java
+++ 
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/ClusterInstance.java
@@ -259,6 +259,8 @@ public interface ClusterInstance {
 
     void startBroker(int brokerId);
 
+    void restartBroker(int brokerId, Map<String, Object> propOverrides);
+
     void restartBrokersWithSwappedClientListenerPorts(int brokerId1, int 
brokerId2);
 
     //---------------------------[wait]---------------------------//
diff --git 
a/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/KafkaClusterTestKit.java
 
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/KafkaClusterTestKit.java
index eec40e2bb82..ec2fa992d9c 100644
--- 
a/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/KafkaClusterTestKit.java
+++ 
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/KafkaClusterTestKit.java
@@ -647,6 +647,50 @@ public class KafkaClusterTestKit implements AutoCloseable {
         broker2.startup();
     }
 
+    /**
+     * Shuts down the given broker (if it isn't already) and starts it back up 
with a possibly
+     * modified static configuration. This allows tests to change read-only 
configs, such as
+     * {@code log.dirs}, which can only be applied when the broker process 
(re)starts.
+     * <p>
+     * The broker keeps its identity (node ID, cluster metadata, bound ports): 
a new
+     * {@link SharedServer}/{@link BrokerServer} pair is created from the 
previous broker's
+     * {@link MetaPropertiesEnsemble} and socket factory, but with a {@link 
KafkaConfig} derived
+     * from the previous one with {@code propOverrides} applied on top.
+     *
+     * @param nodeId         The ID of the broker to restart.
+     * @param propOverrides  Configs to override in the broker's static 
configuration.
+     */
+    public void restartBroker(int nodeId, Map<String, Object> propOverrides) {
+        BrokerServer broker = brokers.get(nodeId);
+        if (broker == null) {
+            throw new IllegalArgumentException("Unknown broker ID " + nodeId);
+        }
+        if (!broker.isShutdown()) {
+            broker.shutdown();
+        }
+        broker.awaitShutdown();
+
+        Map<String, Object> props = new HashMap<>(broker.config().originals());
+        props.putAll(propOverrides);
+        KafkaConfig newConfig = new KafkaConfig(props, false);
+
+        SharedServer sharedServer = new SharedServer(
+            newConfig,
+            broker.sharedServer().metaPropsEnsemble(),
+            Time.SYSTEM,
+            new Metrics(),
+            CompletableFuture.completedFuture(
+                
QuorumConfig.parseVoterConnections(newConfig.quorumConfig().voters())),
+            
QuorumConfig.parseBootstrapServers(newConfig.quorumConfig().bootstrapServers()),
+            faultHandlerFactory,
+            socketFactoryManager.getOrCreateSocketFactory(nodeId)
+        );
+        broker = new BrokerServer(sharedServer);
+        brokers.put(nodeId, broker);
+
+        broker.startup();
+    }
+
     /**
      * Wait for a controller to mark all the brokers as ready (registered and 
unfenced).
      * And also wait for the metadata cache up-to-date in each broker server.
diff --git 
a/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/RaftClusterInvocationContext.java
 
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/RaftClusterInvocationContext.java
index 699adaa3ec7..75757b9f4e1 100644
--- 
a/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/RaftClusterInvocationContext.java
+++ 
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/RaftClusterInvocationContext.java
@@ -271,6 +271,11 @@ public class RaftClusterInvocationContext implements 
TestTemplateInvocationConte
             findBrokerOrThrow(brokerId).startup();
         }
 
+        @Override
+        public void restartBroker(int brokerId, Map<String, Object> 
propOverrides) {
+            clusterTestKit.restartBroker(brokerId, propOverrides);
+        }
+
         @Override
         public void restartBrokersWithSwappedClientListenerPorts(int 
brokerId1, int brokerId2) {
             try {

Reply via email to