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

mjsax 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 25c099e351c KAFKA-20806:  Align failed-push expiration test with 
UNCERTAIN reclaim (#22840)
25c099e351c is described below

commit 25c099e351c23acd8d8fed526dc43c93d1c15423
Author: TengYao Chi <[email protected]>
AuthorDate: Wed Jul 15 20:23:37 2026 +0100

    KAFKA-20806:  Align failed-push expiration test with UNCERTAIN reclaim 
(#22840)
    
    The test shouldExpireGroupWithoutDeleteTopologyWhenPushPermanentlyFailed
    started failing because of commit f8c9566af1 (KAFKA-20748), which
    deliberately changed the post-failure state of a Streams group.
    
    Before KAFKA-20748:
    - A permanent `setTopology` failure left
    `StoredDescriptionTopologyEpoch` at `-1` (NONE).
    - The cleanup scan filtered out groups with `stored == NONE`, so the
    group was tombstoned directly without any `plugin.deleteTopology` call.
    - The test's assertion `assertEquals(0, deleteTopologyCalls(appId))`
    matched that invariant.
    
    After KAFKA-20748:
    1. The push path now writes an `UNCERTAIN(-2)` barrier to
    `StoredDescriptionTopologyEpoch` before calling `plugin.setTopology` —
    this is the durable marker that says "the plugin call is in progress; a
    residual entry may exist" (GroupCoordinatorService.java:748).
    2. When the plugin returns a permanent failure,
    `setFailedDescriptionTopologyEpoch` only advances
    `FailedDescriptionTopologyEpoch`; it deliberately leaves stored
    untouched (GroupMetadataManager.java:8600-8612). So the group ends up
    parked at `UNCERTAIN(-2)` rather than `NONE(-1)`.
    3. The eligibility scan (GroupCoordinatorShard.java:1013-1032) now skips
    only groups whose `storedEpoch == NONE`, so `UNCERTAIN` groups are
    picked up.
    4. The cleanup cycle therefore issues a defensive
    `plugin.deleteTopology` on the group to reclaim any potentially leaked
    plugin entry, then tombstones the group.
    
    The single defensive `deleteTopology` call is the observed expected: <0>
    but was: <1>. This is the intended new behavior — the plugin call and
    the metadata write are non-atomic, so the broker cannot rule out a
    residual entry after a permanent failure and must reclaim it
    defensively.
    
    Reviewers: Alieh Saeedi <[email protected]>, Matthias J. Sax
     <[email protected]>
---
 ...DescriptionPluginExpirationIntegrationTest.java | 22 ++++++++++++++--------
 1 file changed, 14 insertions(+), 8 deletions(-)

diff --git 
a/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/TopologyDescriptionPluginExpirationIntegrationTest.java
 
b/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/TopologyDescriptionPluginExpirationIntegrationTest.java
index 06762f1c3fa..5a6dfd67fd4 100644
--- 
a/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/TopologyDescriptionPluginExpirationIntegrationTest.java
+++ 
b/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/TopologyDescriptionPluginExpirationIntegrationTest.java
@@ -49,14 +49,15 @@ import java.util.Properties;
 import java.util.concurrent.ExecutionException;
 
 import static 
org.apache.kafka.streams.integration.utils.IntegrationTestUtils.startApplicationAndWaitUntilRunning;
-import static org.junit.jupiter.api.Assertions.assertEquals;
 
 /**
  * Tests of the topology description plugin behavior when a streams group 
expires naturally
  * (KIP-1331): the periodic broker-side cleanup cycle calls {@code 
plugin.deleteTopology} and
  * clears the stored topology epoch for empty groups without offsets, after 
which the regular
- * expiration sweep tombstones the group. Groups whose push permanently failed 
have no stored
- * plugin state, so they are tombstoned directly without a {@code 
deleteTopology} call.
+ * expiration sweep tombstones the group. Groups whose push permanently failed 
are left at
+ * UNCERTAIN(-2) — the non-atomic plugin call and epoch write mean the broker 
cannot rule out a
+ * residual plugin entry — so the cleanup cycle still issues {@code 
plugin.deleteTopology}
+ * defensively before the tombstone.
  *
  * <p>The tests avoid producing any input data, so the groups never commit 
offsets and become
  * eligible for expiration as soon as their last member leaves.
@@ -119,7 +120,7 @@ public class 
TopologyDescriptionPluginExpirationIntegrationTest {
     }
 
     @Test
-    public void 
shouldExpireGroupWithoutDeleteTopologyWhenPushPermanentlyFailed() throws 
Exception {
+    public void shouldReclaimUncertainPluginStateWhenPushPermanentlyFailed() 
throws Exception {
         final String appId = "topology-description-failed-expiration-app";
         final String inputTopic = 
"topology-description-failed-expiration-input";
         cluster.createTopic(inputTopic, 1, 1);
@@ -133,11 +134,16 @@ public class 
TopologyDescriptionPluginExpirationIntegrationTest {
                     "Expected a setTopology call for group " + appId);
             }
 
-            // The push permanently failed, so no topology description is 
stored for the
-            // group: the tombstone fires directly, without any deleteTopology 
call.
+            // The permanent push failure leaves the group at UNCERTAIN(-2): 
the barrier written
+            // before the plugin call remains, because the failed-epoch write 
only advances the
+            // failed epoch. UNCERTAIN is delete-eligible, so the cleanup 
cycle must call
+            // plugin.deleteTopology defensively to reclaim any residual entry 
before the
+            // expiration sweep tombstones the group.
+            TestUtils.waitForCondition(
+                () -> 
TrackingTopologyDescriptionPlugin.deleteTopologyCalls(appId) >= 1,
+                "Expected the cleanup cycle to invoke deleteTopology for group 
" + appId
+                    + " left at UNCERTAIN by the permanent push failure");
             waitForGroupToBeDeleted(admin, appId);
-            assertEquals(0, 
TrackingTopologyDescriptionPlugin.deleteTopologyCalls(appId),
-                "Expected no deleteTopology call for group " + appId + " whose 
push permanently failed");
         }
     }
 

Reply via email to