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


##########
clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManager.java:
##########
@@ -147,19 +151,87 @@ public StreamsGroupHeartbeatRequestData 
buildRequestData() {
                 data.setActiveTasks(fromStreamsToHeartbeatRequest(Set.of()));
                 data.setStandbyTasks(fromStreamsToHeartbeatRequest(Set.of()));
                 data.setWarmupTasks(fromStreamsToHeartbeatRequest(Set.of()));
+                
data.setTaskOffsets(convertToList(streamsRebalanceData.taskOffsetSum()));
+                
data.setTaskEndOffsets(convertToList(streamsRebalanceData.taskEndOffsetSum()));
             } else {
-                StreamsRebalanceData.Assignment reconciledAssignment = 
streamsRebalanceData.reconciledAssignment();
-                if (!reconciledAssignment.equals(lastSentFields.assignment)) {
+                final StreamsRebalanceData.Assignment reconciledAssignment = 
streamsRebalanceData.reconciledAssignment();
+                final boolean assignmentChanged = 
!reconciledAssignment.equals(lastSentFields.assignment);
+
+                if (assignmentChanged) {
                     
data.setActiveTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.activeTasks()));
                     
data.setStandbyTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.standbyTasks()));
                     
data.setWarmupTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.warmupTasks()));
                     lastSentFields.assignment = reconciledAssignment;
                 }
+
+                // call both method only once, as they invoke an expensive 
`supplier`
+                final Map<StreamsRebalanceData.TaskId, Long> taskOffsetSum = 
streamsRebalanceData.taskOffsetSum();
+                final Map<StreamsRebalanceData.TaskId, Long> taskEndOffsetSum 
= streamsRebalanceData.taskEndOffsetSum();
+
+                if (assignmentChanged || taskOffsetIntervalPassed() || 
hasAtLeastOneHotWarmupTask(taskOffsetSum, taskEndOffsetSum)) {
+
+                    // TODO: send only if changed this last time
+                    data.setTaskOffsets(convertToList(taskOffsetSum));
+                    data.setTaskEndOffsets(convertToList(taskEndOffsetSum));
+
+                    lastTaskOffsetIntervalTs = time.milliseconds();
+                }
             }
             
data.setShutdownApplication(streamsRebalanceData.shutdownRequested());
             return data;
         }
 
+        private List<StreamsGroupHeartbeatRequestData.TaskOffset> 
convertToList(Map<StreamsRebalanceData.TaskId, Long> offsetsMap) {
+            return offsetsMap.entrySet().stream().map(
+                    entry -> new StreamsGroupHeartbeatRequestData.TaskOffset()
+                        .setSubtopologyId(entry.getKey().subtopologyId())
+                        .setPartition(entry.getKey().partitionId())
+                        .setOffset(entry.getValue()))
+                .collect(Collectors.toList());
+        }
+
+        private boolean taskOffsetIntervalPassed() {
+            return lastTaskOffsetIntervalTs + 
streamsRebalanceData.taskOffsetIntervalMs() <= time.milliseconds();

Review Comment:
   When `taskOffsetIntervalMs` is still -1 (not yet received from broker), this 
evaluates to `-1 + (-1) = -2 <= currentTimeMs`, which is always true. After the 
first send sets `lastTaskOffsetIntervalTs = currentTimeMs`, it becomes 
`currentTimeMs - 1 <= currentTimeMs`, still always true. So task offsets are 
sent on every heartbeat until the broker sets a real interval.
   
   `hasAtLeastOneHotWarmupTask` handles the analogous case correctly (`if 
(acceptableRecoveryLag < 0) return false`), but `taskOffsetIntervalPassed()` 
has no equivalent guard. Shouldn't this return `false` when 
`taskOffsetIntervalMs() < 0`?



##########
streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java:
##########
@@ -520,7 +520,12 @@ public static StreamThread create(final TopologyMetadata 
topologyMetadata,
             clientSupplier,
             processId,
             consumerConfigs,
-            taskManager::taskOffsetSumSnapshot
+            taskManager::taskOffsetSumSnapshot,
+            // TODO (KAFKA-20116): wire a real per-task changelog end-offset 
supplier here.
+            // Empty placeholder for now lets us thread setTaskEndOffsets 
through the heartbeat builder
+            // and exercise the lag-comparison code path; with no end-offset 
entries, hasHotWarmupTask
+            // safely returns false (the broker simply can't compute lag yet).

Review Comment:
   With `Map::of` as the supplier, `taskEndOffsetSum.get(taskId)` always 
returns `null`, so `hasAtLeastOneHotWarmupTask` always short-circuits at the 
`endOffset == null` guard — the actual `endOffset - offset <= 
acceptableRecoveryLag` comparison is never reached. The comment that this 
"exercises the lag-comparison code path" isn't quite accurate.



##########
clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManager.java:
##########
@@ -147,19 +151,87 @@ public StreamsGroupHeartbeatRequestData 
buildRequestData() {
                 data.setActiveTasks(fromStreamsToHeartbeatRequest(Set.of()));
                 data.setStandbyTasks(fromStreamsToHeartbeatRequest(Set.of()));
                 data.setWarmupTasks(fromStreamsToHeartbeatRequest(Set.of()));
+                
data.setTaskOffsets(convertToList(streamsRebalanceData.taskOffsetSum()));
+                
data.setTaskEndOffsets(convertToList(streamsRebalanceData.taskEndOffsetSum()));
             } else {
-                StreamsRebalanceData.Assignment reconciledAssignment = 
streamsRebalanceData.reconciledAssignment();
-                if (!reconciledAssignment.equals(lastSentFields.assignment)) {
+                final StreamsRebalanceData.Assignment reconciledAssignment = 
streamsRebalanceData.reconciledAssignment();
+                final boolean assignmentChanged = 
!reconciledAssignment.equals(lastSentFields.assignment);
+
+                if (assignmentChanged) {
                     
data.setActiveTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.activeTasks()));
                     
data.setStandbyTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.standbyTasks()));
                     
data.setWarmupTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.warmupTasks()));
                     lastSentFields.assignment = reconciledAssignment;
                 }
+
+                // call both method only once, as they invoke an expensive 
`supplier`
+                final Map<StreamsRebalanceData.TaskId, Long> taskOffsetSum = 
streamsRebalanceData.taskOffsetSum();
+                final Map<StreamsRebalanceData.TaskId, Long> taskEndOffsetSum 
= streamsRebalanceData.taskEndOffsetSum();
+
+                if (assignmentChanged || taskOffsetIntervalPassed() || 
hasAtLeastOneHotWarmupTask(taskOffsetSum, taskEndOffsetSum)) {
+
+                    // TODO: send only if changed this last time
+                    data.setTaskOffsets(convertToList(taskOffsetSum));
+                    data.setTaskEndOffsets(convertToList(taskEndOffsetSum));
+
+                    lastTaskOffsetIntervalTs = time.milliseconds();
+                }
             }
             
data.setShutdownApplication(streamsRebalanceData.shutdownRequested());
             return data;
         }
 
+        private List<StreamsGroupHeartbeatRequestData.TaskOffset> 
convertToList(Map<StreamsRebalanceData.TaskId, Long> offsetsMap) {
+            return offsetsMap.entrySet().stream().map(
+                    entry -> new StreamsGroupHeartbeatRequestData.TaskOffset()
+                        .setSubtopologyId(entry.getKey().subtopologyId())
+                        .setPartition(entry.getKey().partitionId())
+                        .setOffset(entry.getValue()))
+                .collect(Collectors.toList());
+        }
+
+        private boolean taskOffsetIntervalPassed() {
+            return lastTaskOffsetIntervalTs + 
streamsRebalanceData.taskOffsetIntervalMs() <= time.milliseconds();
+        }
+
+        private boolean hasAtLeastOneHotWarmupTask(
+            final Map<StreamsRebalanceData.TaskId, Long> taskOffsetSum,
+            final Map<StreamsRebalanceData.TaskId, Long> taskEndOffsetSum
+        ) {
+            final long acceptableRecoveryLag = 
streamsRebalanceData.acceptableRecoveryLag();
+
+            // -1 means "unknown" (can happen when talking to older brokers)
+            // we must be conservative and assume that no warmup might be hot 
already
+            //
+            // technically, we should never get warmup tasks assigned when 
talking to older brokers,
+            // so this is just another safeguard, which should actually be 
redundant:
+            // the code futher below should automatically return false if 
there are no warmup tasks;
+            // checking `acceptableRecoveryLag` is cheaper though, so it's 
also a small micro optimization
+            if (acceptableRecoveryLag < 0) {
+                return false;
+            }
+
+            final Set<StreamsRebalanceData.TaskId> warmupTasks = 
streamsRebalanceData.reconciledAssignment().warmupTasks();
+            if (warmupTasks.isEmpty()) {
+                return false;
+            }
+
+            return warmupTasks.stream()
+                .anyMatch(taskId -> {
+                    final Long offset = taskOffsetSum.get(taskId);
+                    final Long endOffset = taskEndOffsetSum.get(taskId);
+
+                    // offset and endOffset might not be known,
+                    // or be capped at MAX_VALUE due to overflow
+                    if (offset == null || offset == Long.MAX_VALUE
+                        || endOffset == null || endOffset == Long.MAX_VALUE) {
+                        return false;
+                    }
+
+                    return endOffset - offset <= acceptableRecoveryLag;

Review Comment:
   If `endOffset < offset` (possible due to measurement timing skew, leadership 
change, or compaction), the subtraction yields a negative long. Since 
`acceptableRecoveryLag >= 0`, the comparison would be trivially true, 
incorrectly flagging the task as hot.
   
   `ClientState.java` already explicitly handles this case, logging a warning 
that it "probably means the task is corrupted". Shouldn't we add a similar 
guard here, e.g. `if (endOffset <= offset) return false`?



##########
clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManager.java:
##########
@@ -147,19 +151,87 @@ public StreamsGroupHeartbeatRequestData 
buildRequestData() {
                 data.setActiveTasks(fromStreamsToHeartbeatRequest(Set.of()));
                 data.setStandbyTasks(fromStreamsToHeartbeatRequest(Set.of()));
                 data.setWarmupTasks(fromStreamsToHeartbeatRequest(Set.of()));
+                
data.setTaskOffsets(convertToList(streamsRebalanceData.taskOffsetSum()));
+                
data.setTaskEndOffsets(convertToList(streamsRebalanceData.taskEndOffsetSum()));
             } else {
-                StreamsRebalanceData.Assignment reconciledAssignment = 
streamsRebalanceData.reconciledAssignment();
-                if (!reconciledAssignment.equals(lastSentFields.assignment)) {
+                final StreamsRebalanceData.Assignment reconciledAssignment = 
streamsRebalanceData.reconciledAssignment();
+                final boolean assignmentChanged = 
!reconciledAssignment.equals(lastSentFields.assignment);
+
+                if (assignmentChanged) {
                     
data.setActiveTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.activeTasks()));
                     
data.setStandbyTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.standbyTasks()));
                     
data.setWarmupTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.warmupTasks()));
                     lastSentFields.assignment = reconciledAssignment;
                 }
+
+                // call both method only once, as they invoke an expensive 
`supplier`
+                final Map<StreamsRebalanceData.TaskId, Long> taskOffsetSum = 
streamsRebalanceData.taskOffsetSum();
+                final Map<StreamsRebalanceData.TaskId, Long> taskEndOffsetSum 
= streamsRebalanceData.taskEndOffsetSum();
+
+                if (assignmentChanged || taskOffsetIntervalPassed() || 
hasAtLeastOneHotWarmupTask(taskOffsetSum, taskEndOffsetSum)) {
+
+                    // TODO: send only if changed this last time
+                    data.setTaskOffsets(convertToList(taskOffsetSum));
+                    data.setTaskEndOffsets(convertToList(taskEndOffsetSum));
+
+                    lastTaskOffsetIntervalTs = time.milliseconds();
+                }
             }
             
data.setShutdownApplication(streamsRebalanceData.shutdownRequested());
             return data;
         }
 
+        private List<StreamsGroupHeartbeatRequestData.TaskOffset> 
convertToList(Map<StreamsRebalanceData.TaskId, Long> offsetsMap) {
+            return offsetsMap.entrySet().stream().map(
+                    entry -> new StreamsGroupHeartbeatRequestData.TaskOffset()
+                        .setSubtopologyId(entry.getKey().subtopologyId())
+                        .setPartition(entry.getKey().partitionId())
+                        .setOffset(entry.getValue()))
+                .collect(Collectors.toList());
+        }
+
+        private boolean taskOffsetIntervalPassed() {
+            return lastTaskOffsetIntervalTs + 
streamsRebalanceData.taskOffsetIntervalMs() <= time.milliseconds();
+        }
+
+        private boolean hasAtLeastOneHotWarmupTask(
+            final Map<StreamsRebalanceData.TaskId, Long> taskOffsetSum,
+            final Map<StreamsRebalanceData.TaskId, Long> taskEndOffsetSum
+        ) {
+            final long acceptableRecoveryLag = 
streamsRebalanceData.acceptableRecoveryLag();
+
+            // -1 means "unknown" (can happen when talking to older brokers)
+            // we must be conservative and assume that no warmup might be hot 
already
+            //
+            // technically, we should never get warmup tasks assigned when 
talking to older brokers,
+            // so this is just another safeguard, which should actually be 
redundant:
+            // the code futher below should automatically return false if 
there are no warmup tasks;
+            // checking `acceptableRecoveryLag` is cheaper though, so it's 
also a small micro optimization
+            if (acceptableRecoveryLag < 0) {
+                return false;
+            }
+
+            final Set<StreamsRebalanceData.TaskId> warmupTasks = 
streamsRebalanceData.reconciledAssignment().warmupTasks();

Review Comment:
   `reconciledAssignment()` is read again from the `AtomicReference` here, but 
the stable-branch caller already captured it at line 157. A concurrent 
`setReconciledAssignment()` between those two reads would cause `warmupTasks` 
to come from a different snapshot than the one `assignmentChanged` was computed 
against. Consider passing `reconciledAssignment.warmupTasks()` (or the 
`Assignment` itself) as a parameter instead.



##########
clients/src/main/java/org/apache/kafka/clients/consumer/internals/StreamsGroupHeartbeatRequestManager.java:
##########
@@ -147,19 +151,87 @@ public StreamsGroupHeartbeatRequestData 
buildRequestData() {
                 data.setActiveTasks(fromStreamsToHeartbeatRequest(Set.of()));
                 data.setStandbyTasks(fromStreamsToHeartbeatRequest(Set.of()));
                 data.setWarmupTasks(fromStreamsToHeartbeatRequest(Set.of()));
+                
data.setTaskOffsets(convertToList(streamsRebalanceData.taskOffsetSum()));
+                
data.setTaskEndOffsets(convertToList(streamsRebalanceData.taskEndOffsetSum()));
             } else {
-                StreamsRebalanceData.Assignment reconciledAssignment = 
streamsRebalanceData.reconciledAssignment();
-                if (!reconciledAssignment.equals(lastSentFields.assignment)) {
+                final StreamsRebalanceData.Assignment reconciledAssignment = 
streamsRebalanceData.reconciledAssignment();
+                final boolean assignmentChanged = 
!reconciledAssignment.equals(lastSentFields.assignment);
+
+                if (assignmentChanged) {
                     
data.setActiveTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.activeTasks()));
                     
data.setStandbyTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.standbyTasks()));
                     
data.setWarmupTasks(fromStreamsToHeartbeatRequest(reconciledAssignment.warmupTasks()));
                     lastSentFields.assignment = reconciledAssignment;
                 }
+
+                // call both method only once, as they invoke an expensive 
`supplier`
+                final Map<StreamsRebalanceData.TaskId, Long> taskOffsetSum = 
streamsRebalanceData.taskOffsetSum();
+                final Map<StreamsRebalanceData.TaskId, Long> taskEndOffsetSum 
= streamsRebalanceData.taskEndOffsetSum();
+
+                if (assignmentChanged || taskOffsetIntervalPassed() || 
hasAtLeastOneHotWarmupTask(taskOffsetSum, taskEndOffsetSum)) {
+
+                    // TODO: send only if changed this last time
+                    data.setTaskOffsets(convertToList(taskOffsetSum));
+                    data.setTaskEndOffsets(convertToList(taskEndOffsetSum));
+
+                    lastTaskOffsetIntervalTs = time.milliseconds();
+                }
             }
             
data.setShutdownApplication(streamsRebalanceData.shutdownRequested());
             return data;
         }
 
+        private List<StreamsGroupHeartbeatRequestData.TaskOffset> 
convertToList(Map<StreamsRebalanceData.TaskId, Long> offsetsMap) {
+            return offsetsMap.entrySet().stream().map(
+                    entry -> new StreamsGroupHeartbeatRequestData.TaskOffset()
+                        .setSubtopologyId(entry.getKey().subtopologyId())
+                        .setPartition(entry.getKey().partitionId())
+                        .setOffset(entry.getValue()))
+                .collect(Collectors.toList());
+        }
+
+        private boolean taskOffsetIntervalPassed() {
+            return lastTaskOffsetIntervalTs + 
streamsRebalanceData.taskOffsetIntervalMs() <= time.milliseconds();
+        }
+
+        private boolean hasAtLeastOneHotWarmupTask(
+            final Map<StreamsRebalanceData.TaskId, Long> taskOffsetSum,
+            final Map<StreamsRebalanceData.TaskId, Long> taskEndOffsetSum
+        ) {
+            final long acceptableRecoveryLag = 
streamsRebalanceData.acceptableRecoveryLag();
+
+            // -1 means "unknown" (can happen when talking to older brokers)
+            // we must be conservative and assume that no warmup might be hot 
already
+            //
+            // technically, we should never get warmup tasks assigned when 
talking to older brokers,
+            // so this is just another safeguard, which should actually be 
redundant:
+            // the code futher below should automatically return false if 
there are no warmup tasks;

Review Comment:
   Typo: "futher" → "further"



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