mjsax commented on code in PR #23794:
URL: https://github.com/apache/kafka/pull/23794#discussion_r4407518108


##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/streams/AssignmentRefinerImpl.java:
##########
@@ -344,6 +335,40 @@ static TaskDecisions analyzeTasks(
         return new TaskDecisions(List.copyOf(stagedMigrations), 
List.copyOf(grantedTasks));
     }
 
+    /**
+     * Which member the task keeps running on until the target owner is warm. 
'Empty' hands the task to the target
+     * owner in this step instead, cold unless its own process still has the 
state on disk.
+     */
+    private static Optional<String> keepRunningOn(
+        final CurrentAssignmentIndex currentAssignment,
+        final TaskId task,
+        final ActiveHolder holder,
+        final String targetProcessId,
+        final Map<TaskId, Set<String>> targetStandbyHolders,
+        final Map<String, Integer> heartbeatVersions,
+        final Map<String, ProcessLoad> processLoad
+    ) {
+        if (holder != null && holder.hot()) {
+            return Optional.of(holder.memberId());
+        }
+        if (holder == null && onDisk(currentAssignment, targetProcessId, 
task)) {
+            // The target owner's process still has this task's state on disk 
and reopens it, so the task goes
+            // there rather than onto a copy holder: the usual way into this 
branch is a member that restarted
+            // inside the session timeout and got its own tasks back, where 
that state is exact. A stale disk
+            // state is not told apart from a fresh one -- a process reports 
an end offset only for a task it
+            // is restoring -- and the copy-holder detour would cost a second 
restore, a slot and a hand-over.
+            return Optional.empty();
+        }
+        final Optional<String> copy = bestCopyToPromote(currentAssignment, 
task, targetStandbyHolders, processLoad);
+        // Only for an orphan whose target owner's process holds no copy of 
the task. A copy there is warmer than one
+        // of unknown lag elsewhere, and is what a hand-over that is already 
under way completes with.
+        if (copy.isEmpty() && holder == null && 
findCopyOnProcess(currentAssignment, task, targetProcessId).isEmpty()) {
+            return unobservableStandbyToPromote(
+                currentAssignment, task, targetProcessId, 
targetStandbyHolders, heartbeatVersions, processLoad);
+        }
+        return copy;

Review Comment:
   ```suggestion
           if (copy.isPresent()) {
               return copy;
           }
   
           // Only for an orphan whose target owner's process holds no copy of 
the task. A copy there is what the
           // grant recycles, or what a hand-over already under way completes 
with, so the alternative is not a                                               
                                                 
           // restore from scratch
           if (holder == null && findCopyOnProcess(currentAssignment, task, 
targetProcessId).isEmpty()) {
               return unobservableStandbyToPromote(
                   currentAssignment, task, targetProcessId, 
targetStandbyHolders, heartbeatVersions, processLoad);
           }
   
           return Optional.empty();
   ```



##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/streams/AssignmentRefinerImpl.java:
##########
@@ -371,6 +396,42 @@ private static Optional<String> bestCopyToPromote(
             .map(TaskCopy::memberId);
     }
 
+    /**
+     * The standby task of an orphan that is promoted to run the task although 
the coordinator cannot tell how far
+     * behind it is, because its holder does not report its restore progress: 
a member that is not
+     * {@link WarmupSupport#SUPPORTED}. Only a standby task that the target 
assignment also gives to that member, and
+     * that is not on the target owner's process, is promoted. The target 
owner's process is {@code null} if the
+     * target owner has left the group.
+     *
+     * <p>This is the one place where a copy of unknown lag is acted on. 
Elsewhere, the alternative to promoting a
+     * lagging copy is keeping a running owner, here it is a restore from 
scratch on the target owner next to a copy
+     * that would have to restore anyway: a stale copy is restored as an 
active task by the temporary owner before it
+     * processes anything, which is the restore the target assignment demands 
of it as a standby holder, and a copy
+     * that is close resumes processing at once. When the target owner's 
warm-up task has caught up, the hand-over
+     * demotes the temporary owner to its standby task in place.
+     *
+     * <p>Among several candidates, the least loaded process wins, without 
comparing lags.
+     */
+    private static Optional<String> unobservableStandbyToPromote(
+        final CurrentAssignmentIndex currentAssignment,
+        final TaskId task,
+        final String targetProcessId,
+        final Map<TaskId, Set<String>> targetStandbyHolders,
+        final Map<String, Integer> heartbeatVersions,
+        final Map<String, ProcessLoad> processLoad
+    ) {
+        final Set<String> designatedStandbyHolders = 
targetStandbyHolders.getOrDefault(task, Set.of());
+        return currentAssignment.taskCopies().getOrDefault(task, 
List.of()).stream()
+            .filter(copy -> copy.role() == TaskRole.STANDBY
+                && !copy.processId().equals(targetProcessId)

Review Comment:
   Why do we need this filter? If I understand the code correctly, this filter 
could only be false, if the assignor puts an active and standby on the same 
client, which would be an invalid assignment.
   
   From Claude:
   > This filter guards only part of what the exception needs. The rest, an 
orphan whose target owner's process holds no copy of any role, is checked by 
the caller in keepRunningOn. Could we move both checks into this method as an 
early return?  Then the method is correct on its own, this filter becomes 
unnecessary, and both orphan paths end in the same 
bestCopyToPromote(...).or(...).



##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/streams/AssignmentRefinerImpl.java:
##########
@@ -371,6 +396,42 @@ private static Optional<String> bestCopyToPromote(
             .map(TaskCopy::memberId);
     }
 
+    /**
+     * The standby task of an orphan that is promoted to run the task although 
the coordinator cannot tell how far
+     * behind it is, because its holder does not report its restore progress: 
a member that is not
+     * {@link WarmupSupport#SUPPORTED}. Only a standby task that the target 
assignment also gives to that member, and
+     * that is not on the target owner's process, is promoted. The target 
owner's process is {@code null} if the
+     * target owner has left the group.
+     *
+     * <p>This is the one place where a copy of unknown lag is acted on. 
Elsewhere, the alternative to promoting a
+     * lagging copy is keeping a running owner, here it is a restore from 
scratch on the target owner next to a copy
+     * that would have to restore anyway: a stale copy is restored as an 
active task by the temporary owner before it
+     * processes anything, which is the restore the target assignment demands 
of it as a standby holder, and a copy
+     * that is close resumes processing at once. When the target owner's 
warm-up task has caught up, the hand-over
+     * demotes the temporary owner to its standby task in place.
+     *
+     * <p>Among several candidates, the least loaded process wins, without 
comparing lags.
+     */
+    private static Optional<String> unobservableStandbyToPromote(

Review Comment:
   Reviewing this with the help from Claude reveals that this method does 
depend on condition the callee checks first. Might be worth to pull in this 
logic into the method to make it self-contained (also related to my comment 
below about the filter).
   
   ```
         private static Optional<String> unobservableStandbyToPromote(...) {
             if (currentAssignment.activeHolder().containsKey(task)
                 || targetProcessId != null && 
findCopyOnProcess(currentAssignment, task, targetProcessId).isPresent()) {
                 return Optional.empty();
             }
             final Set<String> designatedStandbyHolders = 
targetStandbyHolders.getOrDefault(task, Set.of());
             return currentAssignment.taskCopies().getOrDefault(task, 
List.of()).stream()
                 .filter(copy -> copy.role() == TaskRole.STANDBY
                     && designatedStandbyHolders.contains(copy.memberId())
                     && 
WarmupSupport.of(heartbeatVersions.get(copy.memberId())) != 
WarmupSupport.SUPPORTED)
                 ...
         }
   ```
   
   And
   
   >   Then keepRunningOn ends the same way as the target-owner-left branch:
   
   ```
             return bestCopyToPromote(currentAssignment, task, 
targetStandbyHolders, processLoad)
                 .or(() -> unobservableStandbyToPromote(
                     currentAssignment, task, targetProcessId, 
targetStandbyHolders, heartbeatVersions, processLoad));
   ```



##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/streams/AssignmentRefinerImpl.java:
##########
@@ -371,6 +396,42 @@ private static Optional<String> bestCopyToPromote(
             .map(TaskCopy::memberId);
     }
 
+    /**
+     * The standby task of an orphan that is promoted to run the task although 
the coordinator cannot tell how far
+     * behind it is, because its holder does not report its restore progress: 
a member that is not
+     * {@link WarmupSupport#SUPPORTED}. Only a standby task that the target 
assignment also gives to that member, and
+     * that is not on the target owner's process, is promoted. The target 
owner's process is {@code null} if the
+     * target owner has left the group.
+     *
+     * <p>This is the one place where a copy of unknown lag is acted on. 
Elsewhere, the alternative to promoting a
+     * lagging copy is keeping a running owner, here it is a restore from 
scratch on the target owner next to a copy
+     * that would have to restore anyway: a stale copy is restored as an 
active task by the temporary owner before it
+     * processes anything, which is the restore the target assignment demands 
of it as a standby holder, and a copy
+     * that is close resumes processing at once. When the target owner's 
warm-up task has caught up, the hand-over
+     * demotes the temporary owner to its standby task in place.

Review Comment:
   That's a classic Claude comment. Can you rephrase it to make it easier to 
understand. I grok it now, because I work on the subject for months. But if I 
cycle back in the future, or somebody less familiar reads this, I am sure it's 
hard to impossible to understand.



-- 
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