lucasbru commented on code in PR #23566:
URL: https://github.com/apache/kafka/pull/23566#discussion_r4094305006


##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/streams/StreamsGroupTopologyDescriptionManager.java:
##########
@@ -275,6 +458,33 @@ public StreamsGroupHeartbeatResult 
maybeSetTopologyDescriptionRequired(
         return result;
     }
 
+    /**
+     * Decorates a streams group heartbeat's result with {@link 
#maybeSetTopologyDescriptionRequired},
+     * once the underlying write completes. No-ops when no plugin is 
configured, without
+     * scheduling the extra stage. If decoration itself fails (e.g. because of 
an unexpected
+     * response shape), the heartbeat has already committed durably, so the 
failure is logged and
+     * the committed result is returned unmodified rather than translated into 
an error; that
+     * would mask a successful broker-side state change behind a 
client-visible failure.
+     */
+    public CompletableFuture<StreamsGroupHeartbeatResult> 
decorateHeartbeatResult(
+        CompletableFuture<StreamsGroupHeartbeatResult> heartbeat,
+        String groupId,
+        int apiVersion,
+        int memberEpoch
+    ) {
+        if (!isPluginConfigured()) return heartbeat;
+        return heartbeat.thenApply(result -> {
+            try {
+                return maybeSetTopologyDescriptionRequired(result, groupId, 
apiVersion, memberEpoch);

Review Comment:
   Now that this wraps it, GroupCoordinatorService doesn't call 
maybeSetTopologyDescriptionRequired directly anymore, so it could drop to 
private. Same for invokeSetTopology and completeEpochWrite in pushTopology 
above - both were public building blocks for the service to compose before this 
PR, but now that pushTopology composes them itself, nothing outside this class 
calls them either.



##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/streams/StreamsGroupTopologyDescriptionManager.java:
##########
@@ -134,22 +212,25 @@ public void startCleanupCycle(
         scheduleNextTick(timer, cleanupCheckIntervalMs, cycleSupplier);
     }
 
-    /**
-     * Stop the cleanup cycle and release plugin-side resources. Flips {@code 
running}
-     * false (so the next timer tick refuses to fire) and cancels the 
currently-scheduled
-     * tick, then closes the plugin. Called by {@code 
GroupCoordinatorService.shutdown}
-     * before the runtime is closed, so writes already scheduled by the 
previous tick
-     * drain through their own futures rather than racing the runtime 
tear-down.
-     */
-    @Override
-    public void close() throws Exception {
+    /** Stops the cleanup cycle without touching the plugin; see {@link 
#close}. */
+    public void stopCleanupCycle() {
         if (running.compareAndSet(true, false)) {
             TimerTask snapshot = scheduledTask;
             if (snapshot != null) {
                 snapshot.cancel();
             }
         }
-        if (plugin.isPresent()) {
+    }
+
+    /**
+     * Stops the cleanup cycle and closes the plugin. Called by
+     * {@code GroupCoordinatorService.shutdown} after {@code isActive} flips 
false, since request
+     * handlers gate on {@code isActive} once and don't re-check it before 
reaching the plugin.
+     */
+    @Override
+    public void close() throws Exception {
+        stopCleanupCycle();
+        if (closed.compareAndSet(false, true) && plugin.isPresent()) {

Review Comment:
   Two gaps that are actually new with this PR and have no coverage anywhere: 
(1) the closed guard here - no test calls close()/shutdown() twice to check 
plugin.close() only fires once; before this PR there was no guard to test, now 
there's a real branch. (2) stopCleanupCycle() itself - it's only ever exercised 
indirectly through shutdown()'s end-to-end timer-size check, never called 
directly to verify its own no-op-when-not-running behavior. Both are cheap to 
add now that StreamsGroupTopologyDescriptionManagerTest exists - e.g. build a 
manager, call close() twice, assert plugin.close() invoked once; and call 
stopCleanupCycle() before/after startCleanupCycle() and check the scheduled 
task.



##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/GroupCoordinatorService.java:
##########
@@ -1355,7 +1089,7 @@ private CompletableFuture<Void> 
cleanupTopologyBeforeConversion(
             failJoinRetriably(request, responseFuture);
             return CompletableFuture.completedFuture(null);
         }
-        return markTopologyUncertainAsync(tp, request.groupId(), false)
+        return 
streamsGroupTopologyDescriptionManager.markTopologyUncertain(request.groupId(), 
false)

Review Comment:
   This whole method already routes every topology-specific step through 
`streamsGroupTopologyDescriptionManager` (isConversionDeleteThrottled, 
markTopologyUncertain, invokeDeleteTopologies, recordPluginDeleteOutcome, 
throttleConversionDelete, clearBackoffGroup, finalizeAfterDelete). Could the 
throttle -> mark -> delete -> record -> throttle-or-clear-backoff sequence move 
onto the manager as one method returning a boolean (cleaned up or not)? All 
three failure branches here do the same thing (failJoinRetriably), so this 
would collapse to something like:
   
   ```java
   
streamsGroupTopologyDescriptionManager.cleanupTopologyBeforeConversion(request.groupId())
       .thenCompose(cleaned -> {
           if (!cleaned) {
               failJoinRetriably(request, responseFuture);
               return CompletableFuture.completedFuture(null);
           }
           return runClassicGroupJoin(context, request, responseFuture, tp, 
true)
               .thenCompose(__ -> 
streamsGroupTopologyDescriptionManager.finalizeAfterDelete(request.groupId()));
       });
   ```
   
   `runClassicGroupJoin`/`failJoinRetriably` would stay here since they need 
the raw runtime and the JoinGroup wire types, but the rest looks like it 
belongs with the orchestration that already moved. Would also let 
`isConversionDeleteThrottled`, `throttleConversionDelete`, 
`markTopologyUncertain`, and `clearBackoffGroup` drop to private on the 
manager, since nothing outside it would call them anymore.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to