frankvicky commented on code in PR #22458:
URL: https://github.com/apache/kafka/pull/22458#discussion_r3481815842


##########
streams/src/main/java/org/apache/kafka/streams/internals/StreamsConfigUtils.java:
##########
@@ -80,4 +82,46 @@ public static long totalCacheSize(final StreamsConfig 
config) {
         // only new or no config set. Use default or user specified value.
         return config.getLong(STATESTORE_CACHE_MAX_BYTES_CONFIG);
     }
+
+    // Returns -1 unless the deprecated config is the only one set; -1 
disables the legacy per-partition pause.
+    @SuppressWarnings("deprecation")
+    public static int getBufferedRecordsPerPartition(final StreamsConfig 
config) {
+        if 
(config.originals().containsKey(BUFFERED_RECORDS_PER_PARTITION_CONFIG) && 
config.originals().containsKey(INPUT_BUFFER_MAX_BYTES_CONFIG)) {
+            LOG.warn("Both deprecated config {} and the new config {} are set, 
hence {} is ignored and {} will be used instead to keep memory usage under 
control.",
+                BUFFERED_RECORDS_PER_PARTITION_CONFIG,
+                INPUT_BUFFER_MAX_BYTES_CONFIG,
+                BUFFERED_RECORDS_PER_PARTITION_CONFIG,
+                INPUT_BUFFER_MAX_BYTES_CONFIG);
+            return -1;
+        } else if 
(config.originals().containsKey(BUFFERED_RECORDS_PER_PARTITION_CONFIG)) {
+            LOG.warn("Deprecated config {} is set, and will be used; we 
suggest setting the new config {} to keep memory usage under control " +
+                    "instead as deprecated {} would be removed in the future.",
+                BUFFERED_RECORDS_PER_PARTITION_CONFIG,
+                INPUT_BUFFER_MAX_BYTES_CONFIG,
+                BUFFERED_RECORDS_PER_PARTITION_CONFIG);
+            return config.getInt(BUFFERED_RECORDS_PER_PARTITION_CONFIG);
+        }
+        return -1;
+    }
+
+    // Returns -1L when only the legacy config is set; -1L disables the 
bytes-based pause.
+    @SuppressWarnings("deprecation")
+    public static long getInputBufferMaxBytes(final StreamsConfig config) {
+        if 
(config.originals().containsKey(BUFFERED_RECORDS_PER_PARTITION_CONFIG) && 
config.originals().containsKey(INPUT_BUFFER_MAX_BYTES_CONFIG)) {
+            LOG.warn("Both deprecated config {} and the new config {} are set, 
hence {} is ignored and {} will be used instead to keep memory usage under 
control.",
+                BUFFERED_RECORDS_PER_PARTITION_CONFIG,
+                INPUT_BUFFER_MAX_BYTES_CONFIG,
+                BUFFERED_RECORDS_PER_PARTITION_CONFIG,
+                INPUT_BUFFER_MAX_BYTES_CONFIG);
+            return config.getLong(INPUT_BUFFER_MAX_BYTES_CONFIG);
+        } else if 
(config.originals().containsKey(BUFFERED_RECORDS_PER_PARTITION_CONFIG)) {
+            LOG.warn("Deprecated config {} is set, and will be used; we 
suggest setting the new config {} to keep memory usage under control " +
+                    "instead as deprecated {} would be removed in the future.",
+                BUFFERED_RECORDS_PER_PARTITION_CONFIG,
+                INPUT_BUFFER_MAX_BYTES_CONFIG,
+                BUFFERED_RECORDS_PER_PARTITION_CONFIG);
+            return -1L;

Review Comment:
   This branch silently disables the new bytes-based cap for any user upgrading 
from a release that had `buffered.records.per.partition` set, even though 
`INPUT_BUFFER_MAX_BYTES_CONFIG` has a 512 MiB schema default that should still 
apply.
   
   Concrete migration path:
   - User has been running 4.x with 
`props.put(BUFFERED_RECORDS_PER_PARTITION_CONFIG, 2000);` (a common pattern for 
memory tuning under bursty workloads).
   - They upgrade to a build containing this PR; never touch the new key.
   - `originals()` contains only the deprecated key → this branch fires → 
returns `-1L`.
   - `KafkaStreams.bufferSizePerThread` returns `-1L` for every thread → 
`StreamThread.maxBufferSizeInBytes` stays `UNDEFINED` → bytes guard never runs.
   
   KIP-770's stated goal — bounding memory for users on the deprecated config — 
is precisely not achieved for the population the deprecation is aimed at. This 
is structurally the same shape as the [Feb 2022 backward-compat 
revert](https://github.com/apache/kafka/pull/11424) of the original KIP-770 PR 
(the [resubmit](https://github.com/apache/kafka/pull/11796) introduced 
`getTotalCacheSize()` exactly to avoid silently honoring only one side of the 
old/new pair on the cache path; the input-buffer path needs the same treatment).
   
   I think this branch should return the new config's value so the schema 
default flows through:
   
   ```java
   } else if 
(config.originals().containsKey(BUFFERED_RECORDS_PER_PARTITION_CONFIG)) {
       LOG.warn("Deprecated config {} is set; we suggest setting {} explicitly. 
" +
                "Using {} = {} (default) for memory bounding.",
                BUFFERED_RECORDS_PER_PARTITION_CONFIG,
                INPUT_BUFFER_MAX_BYTES_CONFIG,
                INPUT_BUFFER_MAX_BYTES_CONFIG,
                config.getLong(INPUT_BUFFER_MAX_BYTES_CONFIG));
       return config.getLong(INPUT_BUFFER_MAX_BYTES_CONFIG);
   }
   ```
   
   The matching `getBufferedRecordsPerPartition` (lines 96-102) is correct 
as-is: when only the deprecated config is set, honor it. The bytes helper just 
shouldn't disable itself in that case.
   
   If we keep the current behavior, please at least promote the log to `WARN` 
and state explicitly that the new bytes cap is disabled — the current message 
implies the opposite ("will be used... to keep memory usage under control").



##########
streams/src/main/java/org/apache/kafka/streams/TopologyConfig.java:
##########
@@ -312,6 +309,47 @@ public TopologyConfig(final String topologyName, final 
StreamsConfig globalAppCo
                 .getOrDefault(StreamsConfig.TRANSACTIONAL_STATE_STORES_CONFIG, 
"false")));
     }
 
+    // local sentinel mirroring StreamTask.UNDEFINED_MAX_BUFFERED_SIZE; -1 
disables the legacy
+    // per-partition pause and lets the bytes guard own buffering.
+    private static final int UNDEFINED_MAX_BUFFERED_SIZE = -1;
+
+    private int configureMaxBufferedSize() {
+        final boolean bufferedRecordsPerPartitionOverridden = 
isTopologyOverride(BUFFERED_RECORDS_PER_PARTITION_CONFIG, topologyOverrides);
+        // a global input.buffer.max.bytes setting also locks out the legacy 
per-topology override,
+        // since the user has explicitly opted into the bytes path app-wide.
+        final boolean inputBufferMaxBytesOverridden =
+            isTopologyOverride(INPUT_BUFFER_MAX_BYTES_CONFIG, 
topologyOverrides)
+                || 
globalAppConfigs.originals().containsKey(INPUT_BUFFER_MAX_BYTES_CONFIG);

Review Comment:
   Following up on the thread above — I agree with the spirit of @unknowntpo's 
suggestion (a globally chosen bytes path should win over a topology-level 
legacy override), but I think the current literal check is too loose and 
reintroduces a silent-config-drop bug that's the same shape as the [Feb 2022 
backward-compat revert](https://github.com/apache/kafka/pull/11424) of KIP-770.
   
   `globalAppConfigs.originals().containsKey(INPUT_BUFFER_MAX_BYTES_CONFIG)` 
returns true whenever the key appears in the user's `Properties` — even when 
the value is identical to the schema default. So:
   
   - NamedTopology user sets `topologyOverrides = { 
buffered.records.per.partition: 1000 }` on topology A.
   - Their global `Properties` contains `input.buffer.max.bytes = 536870912` 
(the documented default, copied from a docs example or an ops template).
   - `inputBufferMaxBytesOverridden` → true via `originals().containsKey(...)`.
   - `setMaxBufferedRecordsPerPartition(true, true)` returns 
`UNDEFINED_MAX_BUFFERED_SIZE` → the 1000-record topology override is silently 
dropped with only an `INFO` log.
   
   The user never opted into the bytes path; they just wrote down the 
documented default.
   
   Two ways to preserve @unknowntpo's intent without the silent drop:
   
   **(a)** Detect "explicit opt-in" by comparing against the default:
   
   ```java
   || (globalAppConfigs.originals().containsKey(INPUT_BUFFER_MAX_BYTES_CONFIG)
       && globalAppConfigs.getLong(INPUT_BUFFER_MAX_BYTES_CONFIG)
          != /* default 512 MiB */)
   ```
   
   Slightly fragile (a user could re-set to the default and expect lock-out), 
but matches docs.
   
   **(b)** Drop the global-side override-lock entirely. A topology-level 
override is the strongest signal of intent; let it win. The two guards aren't 
mutually exclusive — bytes-mode at `StreamThread` keeps the global memory 
bound, count-mode at the affected topology keeps the per-partition fairness 
bound. This is what the design already does when both configs are set at the 
global level (`getBufferedRecordsPerPartition` returns `-1`, 
`getInputBufferMaxBytes` returns the bytes value).
   
   I lean (b): simpler, behavior matches user expectation ("if I asked for X on 
topology A, I should get X on topology A"), and doesn't depend on default 
detection. WDYT?
   
   Whichever direction we go, if any silent-drop semantics remain I'd suggest 
promoting the `INFO` log on line 334 to `WARN` — operators rarely spot-check 
`INFO` for config behavior changes.



##########
streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamTask.java:
##########
@@ -804,9 +810,11 @@ record = partitionGroup.nextRecord(recordInfo, 
wallClockTime);
             log.trace("Task processed record: topic={}, partition={}, 
offset={}",
                 record.topic(), record.partition(), record.offset());
 
-            // after processing this record, if its partition queue's buffered 
size has been
-            // decreased to the threshold, we can then resume the consumption 
on this partition
-            if (recordInfo.queue().size() <= maxBufferedSize) {
+            // headRecordIsCorrupted() catches a queue whose only remaining 
records are corrupted —
+            // size() never drops below the legacy threshold, so without this 
we'd stay paused forever.
+            if (recordInfo.queue().isEmpty()

Review Comment:
   This branch un-pauses partitions that `StreamThread` paused for the bytes 
guard, which I think re-introduces the perf-regression class that drove the Jul 
2022 [revert](https://github.com/apache/kafka/pull/12383/) of KIP-770.
   
   The pause side (line 1158, `addRecords`) is gated on `maxBufferedSize != 
UNDEFINED_MAX_BUFFERED_SIZE` — so in bytes-mode `StreamTask` never pauses. But 
the resume side here fires on `recordInfo.queue().isEmpty()` (and 
`headRecordIsCorrupted()`) regardless of mode. Combined with 
`taskManager.resumePollingForPartitionsWithAvailableSpace()` at the top of 
every `runOnce*` iteration, the consequence is:
   
   1. `pollPhase` finds `totalBytes` > cap, pauses all non-empty partitions 
`{P1..Pn}` on `mainConsumer` and records them in 
`partitionsPausedForBufferOverflow`.
   2. `StreamTask` processes some records on partition `Pa`; its queue empties; 
`Pa` is added to `partitionsToResume` by this branch.
   3. Next iteration: `mainConsumer.resume(Pa)` — even though `totalBytes` 
across the other tasks is still well over the cap.
   4. Consumer fetches a fresh batch on `Pa`; totalBytes spikes; goto 1.
   
   End result: the per-thread bytes cap is enforced only as a soft jitter per 
task, not as a true ceiling. Under sustained fan-in across multiple tasks this 
becomes pause/resume churn on the consumer every iteration.
   
   I think the resume conditions need to be gated by mode the same way the 
pause side is, e.g.:
   
   ```java
   if (maxBufferedSize != UNDEFINED_MAX_BUFFERED_SIZE
       && (recordInfo.queue().isEmpty()
           || recordInfo.queue().size() <= maxBufferedSize
           || recordInfo.queue().headRecordIsCorrupted())) {
       partitionsToResume.add(partition);
   }
   ```
   
   — so `StreamTask` owns resume only for partitions `StreamTask` itself paused 
(count-mode), and `StreamThread.maybeResumePartitionsPausedForBufferOverflow` 
remains the sole owner of resume in bytes-mode.
   
   The `headRecordIsCorrupted()` comment on line 813 explains the legacy 
concern correctly, but that concern only applies when count-mode owns the pause 
— in bytes-mode `StreamThread`'s threshold doesn't depend on `queue.size()`.
   
   Would it be possible to add a test that exercises this: bytes cap N, two 
tasks each with one partition, force one task to drain its queue while 
totalBytes still exceeds N, and assert the consumer remains paused on its 
partition?



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