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

apoorvmittal10 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 28ec585cb3c KAFKA-20723: Add fail fast check for share group dlq(...). 
[1/N] (#22635)
28ec585cb3c is described below

commit 28ec585cb3cf4a6510a8c118464da4934049273c
Author: Sushant Mahajan <[email protected]>
AuthorDate: Wed Jun 24 18:40:25 2026 +0530

    KAFKA-20723: Add fail fast check for share group dlq(...). [1/N] (#22635)
    
    * Add check whether the DLQ state manager is started, before enqueuing
    the request in the `Sender`.
    * Change sequence of validations in `DefaultShareGroupDLQManager`.
    * Add tests to verify the `dlq()` method changes.
    
    Reviewers: Apoorv Mittal <[email protected]>, Andrew Schofield
     <[email protected]>
---
 .../src/main/scala/kafka/server/BrokerServer.scala |  7 ++++
 .../share/dlq/DefaultShareGroupDLQManager.java     | 16 ++++-----
 .../share/dlq/ShareGroupDLQStateManager.java       |  3 ++
 .../share/dlq/ShareGroupDLQStateManagerTest.java   | 40 ++++++++++++++++++++--
 4 files changed, 55 insertions(+), 11 deletions(-)

diff --git a/core/src/main/scala/kafka/server/BrokerServer.scala 
b/core/src/main/scala/kafka/server/BrokerServer.scala
index 7af4c50da83..d7502aaa039 100644
--- a/core/src/main/scala/kafka/server/BrokerServer.scala
+++ b/core/src/main/scala/kafka/server/BrokerServer.scala
@@ -929,9 +929,16 @@ class BrokerServer(
       Utils.closeQuietly(brokerTopicStats, "broker topic stats")
       Utils.closeQuietly(sharePartitionManager, "share partition manager")
 
+      // The order of closing sharePartitionManager, groupCoordinator and 
persister matters.
+      // groupCoordinator, sharePartitionManager must be closed before the 
persister so that
+      // new requests from sharePartitionManager, groupCoordinator do not 
encounter a stopped
+      // persister.
       if (persister != null)
         Utils.swallow(this.logger.underlying, () => persister.stop())
 
+      // The order of closing sharePartitionManager and shareGroupDLQManager 
matters.
+      // sharePartitionManager must be closed before the shareGroupDLQManager 
so any new
+      // requests from sharePartitionManager do not encounter a stopped 
shareGroupDLQManager.
       if (shareGroupDLQManager != null)
         Utils.swallow(this.logger.underlying, () => 
shareGroupDLQManager.stop())
 
diff --git 
a/server/src/main/java/org/apache/kafka/server/share/dlq/DefaultShareGroupDLQManager.java
 
b/server/src/main/java/org/apache/kafka/server/share/dlq/DefaultShareGroupDLQManager.java
index 73559b3cebf..1c78a33ae72 100644
--- 
a/server/src/main/java/org/apache/kafka/server/share/dlq/DefaultShareGroupDLQManager.java
+++ 
b/server/src/main/java/org/apache/kafka/server/share/dlq/DefaultShareGroupDLQManager.java
@@ -62,22 +62,22 @@ public class DefaultShareGroupDLQManager implements 
ShareGroupDLQManager {
         ShareGroupMetrics shareGroupMetrics,
         LogReader logReader
     ) {
-        this.stateManager = new ShareGroupDLQStateManager(client, cacheHelper, 
time, timer, shareGroupMetrics, logReader);
+        stateManager = new ShareGroupDLQStateManager(client, cacheHelper, 
time, timer, shareGroupMetrics, logReader);
     }
 
     private void start() {
-        this.stateManager.start();
+        stateManager.start();
     }
 
     @Override
     public CompletableFuture<Void> enqueue(ShareGroupDLQRecordParameter param) 
{
         try {
             validate(param);
+            return stateManager.dlq(param);
         } catch (Exception e) {
-            log.error("Unable to validate dlq record parameters", e);
+            log.error("Unable to enqueue DLQ request", e);
             return CompletableFuture.failedFuture(e);
         }
-        return stateManager.dlq(param);
     }
 
     @Override
@@ -111,10 +111,6 @@ public class DefaultShareGroupDLQManager implements 
ShareGroupDLQManager {
             throw new IllegalArgumentException(prefix + " partition cannot be 
negative.");
         }
 
-        if (param.lastOffset() < param.firstOffset()) {
-            throw new IllegalArgumentException(prefix + " last offset cannot 
be less than first offset.");
-        }
-
         if (param.firstOffset() < 0) {
             throw new IllegalArgumentException(prefix + " first offset cannot 
be negative.");
         }
@@ -122,5 +118,9 @@ public class DefaultShareGroupDLQManager implements 
ShareGroupDLQManager {
         if (param.lastOffset() < 0) {
             throw new IllegalArgumentException(prefix + " last offset cannot 
be negative.");
         }
+
+        if (param.lastOffset() < param.firstOffset()) {
+            throw new IllegalArgumentException(prefix + " last offset cannot 
be less than first offset.");
+        }
     }
 }
diff --git 
a/server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManager.java
 
b/server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManager.java
index 6bd1863aca8..8a0b3e04d26 100644
--- 
a/server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManager.java
+++ 
b/server/src/main/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManager.java
@@ -185,6 +185,9 @@ public class ShareGroupDLQStateManager {
 
     // Visibility for tests
     CompletableFuture<Void> dlq(ShareGroupDLQRecordParameter param, long 
requestBackoffMs, long requestBackoffMaxMs, int maxRequestAttempts) {
+        if (!this.isStarted.get()) {
+            return CompletableFuture.failedFuture(new 
IllegalStateException("ShareGroupDLQStateManager is not started."));
+        }
         CompletableFuture<Void> future = new CompletableFuture<>();
         ProduceRequestHandler requestHandler = new 
ProduceRequestHandler(param, future, requestBackoffMs, requestBackoffMaxMs, 
maxRequestAttempts);
         enqueue(requestHandler);
diff --git 
a/server/src/test/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManagerTest.java
 
b/server/src/test/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManagerTest.java
index 2da51e250d3..a1767ab634b 100644
--- 
a/server/src/test/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManagerTest.java
+++ 
b/server/src/test/java/org/apache/kafka/server/share/dlq/ShareGroupDLQStateManagerTest.java
@@ -382,7 +382,33 @@ class ShareGroupDLQStateManagerTest {
         verifyNoInteractions(mockMetrics);
     }
 
-    // ---- DLQ topic validation tests (no thread start required) ----
+    @Test
+    public void testDlqBeforeStartFailsWithIllegalState() {
+        stateManager = builder().build();
+        // dlq() is invoked without a prior start(); the lifecycle guard must 
reject it with a
+        // failed future rather than enqueueing onto a sender thread that is 
not running.
+        CompletableFuture<Void> result = stateManager.dlq(param());
+        assertTrue(result.isDone());
+        assertTrue(result.isCompletedExceptionally());
+        assertInstanceOf(IllegalStateException.class, getCause(result));
+        verifyNoInteractions(mockMetrics);
+    }
+
+    @Test
+    public void testDlqAfterStopFailsWithIllegalState() throws Exception {
+        stateManager = builder().build();
+        stateManager.start();
+        stateManager.stop();
+        // Once stopped, dlq() must fail fast rather than enqueueing onto a 
shut-down sender thread
+        // where the future would never complete.
+        CompletableFuture<Void> result = stateManager.dlq(param());
+        assertTrue(result.isDone());
+        assertTrue(result.isCompletedExceptionally());
+        assertInstanceOf(IllegalStateException.class, getCause(result));
+        verifyNoInteractions(mockMetrics);
+    }
+
+    // ---- DLQ topic validation tests ----
 
     @Test
     public void testDlqEmptyTopicNameFailsValidation() throws Exception {
@@ -391,6 +417,7 @@ class ShareGroupDLQStateManagerTest {
         
when(cacheHelper.shareGroupDlqTopicPrefix()).thenReturn(Optional.empty());
 
         stateManager = builder().withCacheHelper(cacheHelper).build();
+        stateManager.start();
         Throwable cause = getCause(stateManager.dlq(param()));
         assertInstanceOf(ConfigException.class, cause);
         assertTrue(cause.getMessage().contains("empty"));
@@ -404,6 +431,7 @@ class ShareGroupDLQStateManagerTest {
         
when(cacheHelper.shareGroupDlqTopicPrefix()).thenReturn(Optional.empty());
 
         stateManager = builder().withCacheHelper(cacheHelper).build();
+        stateManager.start();
         Throwable cause = getCause(stateManager.dlq(param()));
         assertInstanceOf(ConfigException.class, cause);
         assertTrue(cause.getMessage().contains("__"));
@@ -419,6 +447,7 @@ class ShareGroupDLQStateManagerTest {
         when(cacheHelper.isDlqEnabledOnTopic(DLQ_TOPIC)).thenReturn(false);
 
         stateManager = builder().withCacheHelper(cacheHelper).build();
+        stateManager.start();
         Throwable cause = getCause(stateManager.dlq(param()));
         assertInstanceOf(ConfigException.class, cause);
         assertTrue(cause.getMessage().contains("DLQ is not enabled"));
@@ -434,6 +463,7 @@ class ShareGroupDLQStateManagerTest {
         when(cacheHelper.isDlqAutoTopicCreateEnabled()).thenReturn(false);
 
         stateManager = builder().withCacheHelper(cacheHelper).build();
+        stateManager.start();
         Throwable cause = getCause(stateManager.dlq(param()));
         assertInstanceOf(ConfigException.class, cause);
         assertTrue(cause.getMessage().contains("auto create is disabled"));
@@ -449,6 +479,7 @@ class ShareGroupDLQStateManagerTest {
         when(cacheHelper.isDlqEnabledOnTopic(DLQ_TOPIC)).thenReturn(true);
 
         stateManager = builder().withCacheHelper(cacheHelper).build();
+        stateManager.start();
         Throwable cause = getCause(stateManager.dlq(param()));
         assertInstanceOf(ConfigException.class, cause);
         assertTrue(cause.getMessage().contains("does not comply with the DLQ 
topic prefix"));
@@ -456,17 +487,20 @@ class ShareGroupDLQStateManagerTest {
     }
 
     @Test
-    public void testDlqValidationFailureCompletesFutureBeforeStart() throws 
Exception {
+    public void testDlqValidationFailureCompletesFutureSynchronously() throws 
Exception {
         ShareGroupDLQMetadataCacheHelper cacheHelper = 
mock(ShareGroupDLQMetadataCacheHelper.class);
         
when(cacheHelper.shareGroupDlqTopic(GROUP_ID)).thenReturn(Optional.empty());
         
when(cacheHelper.shareGroupDlqTopicPrefix()).thenReturn(Optional.empty());
 
-        // validateDlqTopic runs synchronously inside dlq(), so it should fail 
without the sender thread.
+        // validateDlqTopic runs synchronously inside dlq() on the calling 
thread, so a validation
+        // failure completes the returned future before dlq() returns - no 
sender-thread round trip.
         stateManager = builder().withCacheHelper(cacheHelper).build();
+        stateManager.start();
         CompletableFuture<Void> result = stateManager.dlq(param());
         assertTrue(result.isDone());
         assertTrue(result.isCompletedExceptionally());
         assertFalse(result.isCancelled());
+        assertInstanceOf(ConfigException.class, getCause(result));
         verifyNoInteractions(mockMetrics);
     }
 

Reply via email to