This is an automated email from the ASF dual-hosted git repository.
bbejeck 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 6110d3e1ba4 KAFKA-20491: Add uncommitted bytes limit (#22597)
6110d3e1ba4 is described below
commit 6110d3e1ba47fe92ad776eae6b5d64cdcc19ddbf
Author: Nick Telford <[email protected]>
AuthorDate: Fri Jun 19 16:03:40 2026 +0100
KAFKA-20491: Add uncommitted bytes limit (#22597)
Introduces the `statestore.uncommitted.max.bytes` config, which triggers
an early commit when the total uncommitted bytes across all
transactional state stores on a thread exceeds the configured limit,
regardless of `commit.interval.ms`. This bounds the memory consumed by
pending write buffers under high write throughput.
The limit is divided equally across StreamThreads and the
GlobalStreamThread. Each thread independently enforces its share:
StreamThreads trigger an early task commit via `maybeCommit()`; the
GlobalStreamThread flushes at the end of each `pollAndUpdate()` cycle.
`StateStore` gains a default `approximateNumUncommittedBytes()` method
(returning 0); adapter and wrapper classes delegate to the inner store.
Metered key-value stores register an uncommitted-bytes gauge. Segmented
and versioned stores aggregate across their underlying segments.
The per-thread limit is recomputed whenever stream threads are
dynamically added or removed via
`addStreamThread()`/`removeStreamThread()`, mirroring the existing
thread-cache resize logic. The per-thread limit field is `volatile` so
running threads pick up the new value on their next commit check without
synchronisation.
This is part of
[KIP-892](https://cwiki.apache.org/confluence/display/KAFKA/KIP-892%3A+Transactional+Semantics+for+State+Stores).
Reviewers: Bill Bejeck <[email protected]>
---
.../org/apache/kafka/streams/KafkaStreams.java | 31 ++-
.../org/apache/kafka/streams/StreamsConfig.java | 15 ++
.../apache/kafka/streams/processor/StateStore.java | 9 +
.../processor/internals/GlobalStateMaintainer.java | 2 +
.../processor/internals/GlobalStateManager.java | 2 +
.../internals/GlobalStateManagerImpl.java | 11 +
.../processor/internals/GlobalStateUpdateTask.java | 5 +
.../processor/internals/GlobalStreamThread.java | 22 ++
.../processor/internals/ProcessorStateManager.java | 8 +
.../streams/processor/internals/ReadOnlyTask.java | 5 +
.../streams/processor/internals/StreamTask.java | 5 +
.../streams/processor/internals/StreamThread.java | 21 +-
.../kafka/streams/processor/internals/Task.java | 4 +
.../streams/processor/internals/TaskManager.java | 8 +
.../AbstractRocksDBSegmentedBytesStore.java | 9 +
.../state/internals/KeyValueStoreWrapper.java | 5 +
...ValueToTimestampedKeyValueByteStoreAdapter.java | 5 +
.../state/internals/LogicalKeyValueSegments.java | 4 +
.../state/internals/MeteredKeyValueStore.java | 4 +
.../internals/PlainToHeadersStoreAdapter.java | 5 +
.../PlainToHeadersWindowStoreAdapter.java | 5 +
.../state/internals/RocksDBVersionedStore.java | 6 +
.../internals/SessionToHeadersStoreAdapter.java | 5 +
.../TimestampedToHeadersStoreAdapter.java | 5 +
.../TimestampedToHeadersWindowStoreAdapter.java | 5 +
.../VersionedKeyValueToBytesStoreAdapter.java | 5 +
.../WindowToTimestampedWindowByteStoreAdapter.java | 5 +
.../streams/state/internals/WrappedStateStore.java | 5 +
.../state/internals/metrics/StateStoreMetrics.java | 20 ++
.../org/apache/kafka/streams/KafkaStreamsTest.java | 1 +
.../internals/GlobalStateManagerImplTest.java | 28 +++
.../processor/internals/GlobalStateTaskTest.java | 6 +
.../internals/GlobalStreamThreadTest.java | 26 +++
.../processor/internals/StateConsumerTest.java | 5 +
.../processor/internals/StreamThreadTest.java | 236 +++++++++++++++++++--
.../apache/kafka/test/GlobalStateManagerStub.java | 6 +
36 files changed, 522 insertions(+), 27 deletions(-)
diff --git a/streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java
b/streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java
index 851fced582c..e5f1ee47c54 100644
--- a/streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java
+++ b/streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java
@@ -1012,6 +1012,7 @@ public class KafkaStreams implements AutoCloseable {
totalCacheSize = totalCacheSize(applicationConfigs);
final int numStreamThreads =
topologyMetadata.numStreamThreads(applicationConfigs);
final long cacheSizePerThread = cacheSizePerThread(numStreamThreads);
+ final long maxUncommittedBytesPerThread =
maxUncommittedBytesPerThread(numStreamThreads);
GlobalStreamThread.State globalThreadState = null;
if (hasGlobalTopology) {
@@ -1022,6 +1023,7 @@ public class KafkaStreams implements AutoCloseable {
clientSupplier.getGlobalConsumer(applicationConfigs.getGlobalConsumerConfigs(clientId)),
stateDirectory,
cacheSizePerThread,
+ maxUncommittedBytesPerThread,
streamsMetrics,
time,
globalThreadId,
@@ -1043,7 +1045,7 @@ public class KafkaStreams implements AutoCloseable {
globalStateStoreProvider,
applicationConfigs::defaultInteractiveQueryIsolationLevel);
for (int i = 1; i <= numStreamThreads; i++) {
- createAndAddStreamThread(cacheSizePerThread, i);
+ createAndAddStreamThread(cacheSizePerThread,
maxUncommittedBytesPerThread, i);
}
stateDirCleaner = setupStateDirCleaner();
@@ -1066,7 +1068,9 @@ public class KafkaStreams implements AutoCloseable {
}
}
- private StreamThread createAndAddStreamThread(final long
cacheSizePerThread, final int threadIdx) {
+ private StreamThread createAndAddStreamThread(final long
cacheSizePerThread,
+ final long
maxUncommittedBytesPerThread,
+ final int threadIdx) {
final StreamThread streamThread = StreamThread.create(
topologyMetadata,
applicationConfigs,
@@ -1078,6 +1082,7 @@ public class KafkaStreams implements AutoCloseable {
time,
streamsMetadataState,
cacheSizePerThread,
+ maxUncommittedBytesPerThread,
stateDirectory,
delegatingStateRestoreListener,
delegatingStandbyUpdateListener,
@@ -1124,12 +1129,14 @@ public class KafkaStreams implements AutoCloseable {
final int threadIdx = nextThreadIndex();
final int numLiveThreads = numLiveStreamThreads();
final long cacheSizePerThread =
cacheSizePerThread(numLiveThreads + 1);
+ final long maxUncommittedBytesPerThread =
maxUncommittedBytesPerThread(numLiveThreads + 1);
log.info("Adding StreamThread-{}, there will now be {} live
threads and the new cache size per thread is {}",
threadIdx, numLiveThreads + 1, cacheSizePerThread);
resizeThreadCache(cacheSizePerThread);
+ resizeMaxUncommittedBytes(maxUncommittedBytesPerThread);
// Creating thread should hold the lock in order to avoid
duplicate thread index.
// If the duplicate index happen, the metadata of thread may
be duplicate too.
- streamThread = createAndAddStreamThread(cacheSizePerThread,
threadIdx);
+ streamThread = createAndAddStreamThread(cacheSizePerThread,
maxUncommittedBytesPerThread, threadIdx);
}
synchronized (stateLock) {
@@ -1143,6 +1150,7 @@ public class KafkaStreams implements AutoCloseable {
final long cacheSizePerThread =
cacheSizePerThread(numLiveStreamThreads());
log.info("Resizing thread cache due to terminating added
thread, new cache size per thread is {}", cacheSizePerThread);
resizeThreadCache(cacheSizePerThread);
+
resizeMaxUncommittedBytes(maxUncommittedBytesPerThread(numLiveStreamThreads()));
return Optional.empty();
}
}
@@ -1223,6 +1231,7 @@ public class KafkaStreams implements AutoCloseable {
final long cacheSizePerThread =
cacheSizePerThread(numLiveStreamThreads());
log.info("Resizing thread cache due to thread removal,
new cache size per thread is {}", cacheSizePerThread);
resizeThreadCache(cacheSizePerThread);
+
resizeMaxUncommittedBytes(maxUncommittedBytesPerThread(numLiveStreamThreads()));
final long remainingTimeMs = timeoutMs -
(time.milliseconds() - startMs);
if (remainingTimeMs <= 0) {
throw new TimeoutException("Thread " +
streamThread.getName() + " did not stop in the allotted time");
@@ -1324,6 +1333,15 @@ public class KafkaStreams implements AutoCloseable {
return totalCacheSize / (numStreamThreads +
(topologyMetadata.hasGlobalTopology() ? 1 : 0));
}
+ private long maxUncommittedBytesPerThread(final int numStreamThreads) {
+ final long totalMax =
applicationConfigs.getLong(StreamsConfig.STATESTORE_UNCOMMITTED_MAX_BYTES_CONFIG);
+ if (totalMax <= 0) {
+ return -1;
+ }
+ final int divisor = Math.max(numStreamThreads, 0) +
(topologyMetadata.hasGlobalTopology() ? 1 : 0);
+ return divisor == 0 ? totalMax : totalMax / divisor;
+ }
+
private void resizeThreadCache(final long cacheSizePerThread) {
processStreamThread(thread -> thread.resizeCache(cacheSizePerThread));
if (globalStreamThread != null) {
@@ -1331,6 +1349,13 @@ public class KafkaStreams implements AutoCloseable {
}
}
+ private void resizeMaxUncommittedBytes(final long
maxUncommittedBytesPerThread) {
+ processStreamThread(thread ->
thread.resizeMaxUncommittedBytes(maxUncommittedBytesPerThread));
+ if (globalStreamThread != null) {
+
globalStreamThread.resizeMaxUncommittedBytes(maxUncommittedBytesPerThread);
+ }
+ }
+
private ScheduledExecutorService setupStateDirCleaner() {
return Executors.newSingleThreadScheduledExecutor(r -> {
final Thread thread = new Thread(r, clientId + "-CleanupThread");
diff --git a/streams/src/main/java/org/apache/kafka/streams/StreamsConfig.java
b/streams/src/main/java/org/apache/kafka/streams/StreamsConfig.java
index b227c058a81..d9e1569fe36 100644
--- a/streams/src/main/java/org/apache/kafka/streams/StreamsConfig.java
+++ b/streams/src/main/java/org/apache/kafka/streams/StreamsConfig.java
@@ -808,6 +808,15 @@ public class StreamsConfig extends AbstractConfig {
@Deprecated
public static final String STATESTORE_CACHE_MAX_BYTES_DOC = "Maximum
number of memory bytes to be used for statestore cache across all threads";
+ /** {@code statestore.uncommitted.max.bytes} */
+ public static final String STATESTORE_UNCOMMITTED_MAX_BYTES_CONFIG =
"statestore.uncommitted.max.bytes";
+ private static final String STATESTORE_UNCOMMITTED_MAX_BYTES_DOC =
+ "The maximum number of uncommitted bytes across all transactional
state stores on this " +
+ "application instance before an early commit is triggered, regardless
of commit.interval.ms. " +
+ "The limit is divided equally across the configured number of stream
threads, and the global state thread," +
+ "if the topology has any global stores. Set to -1 to disable. Default
is 67108864 (64 MB).";
+ private static final long STATESTORE_UNCOMMITTED_MAX_BYTES_DEFAULT =
67_108_864L;
+
/** {@code task.assignor.class} */
@SuppressWarnings("WeakerAccess")
public static final String TASK_ASSIGNOR_CLASS_CONFIG =
"task.assignor.class";
@@ -959,6 +968,12 @@ public class StreamsConfig extends AbstractConfig {
atLeast(0),
Importance.MEDIUM,
STATESTORE_CACHE_MAX_BYTES_DOC)
+ .define(STATESTORE_UNCOMMITTED_MAX_BYTES_CONFIG,
+ Type.LONG,
+ STATESTORE_UNCOMMITTED_MAX_BYTES_DEFAULT,
+ atLeast(-1),
+ Importance.LOW,
+ STATESTORE_UNCOMMITTED_MAX_BYTES_DOC)
.define(CLIENT_ID_CONFIG,
Type.STRING,
"",
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/StateStore.java
b/streams/src/main/java/org/apache/kafka/streams/processor/StateStore.java
index 6f91222f64b..19112419ddc 100644
--- a/streams/src/main/java/org/apache/kafka/streams/processor/StateStore.java
+++ b/streams/src/main/java/org/apache/kafka/streams/processor/StateStore.java
@@ -215,6 +215,15 @@ public interface StateStore {
return QueryResult.forUnknownQueryType(query, this);
}
+ /**
+ * Returns an approximation of the number of uncommitted bytes currently
buffered in this store's
+ * transaction buffer, or 0 if the store is not transactional. This is
used to trigger early
+ * commits when the buffer grows too large.
+ */
+ default long approximateNumUncommittedBytes() {
+ return 0;
+ }
+
/**
* Returns the position the state store is at with respect to the input
topic/partitions
*/
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateMaintainer.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateMaintainer.java
index 06afb6fde4f..8c4c993459c 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateMaintainer.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateMaintainer.java
@@ -36,4 +36,6 @@ interface GlobalStateMaintainer {
void update(ConsumerRecord<byte[], byte[]> record);
void maybeCheckpoint();
+
+ long approximateNumUncommittedBytes();
}
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateManager.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateManager.java
index f470254142e..3baa7d99025 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateManager.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateManager.java
@@ -29,4 +29,6 @@ public interface GlobalStateManager extends StateManager {
* @throws StreamsException if the store's change log does not contain the
partition
*/
Set<String> initialize();
+
+ long approximateNumUncommittedBytes();
}
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateManagerImpl.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateManagerImpl.java
index 8e7347e6fa4..fe7f7028904 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateManagerImpl.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateManagerImpl.java
@@ -570,6 +570,17 @@ public class GlobalStateManagerImpl implements
GlobalStateManager {
return currentDeadlineMs;
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ long total = 0;
+ for (final Optional<StateStore> entry : globalStores.values()) {
+ if (entry.isPresent()) {
+ total += entry.get().approximateNumUncommittedBytes();
+ }
+ }
+ return total;
+ }
+
@Override
public void commit() {
log.debug("Committing all global globalStores registered in the state
manager");
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateUpdateTask.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateUpdateTask.java
index 37178598451..45d21d619e9 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateUpdateTask.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStateUpdateTask.java
@@ -168,6 +168,11 @@ public class GlobalStateUpdateTask implements
GlobalStateMaintainer {
}
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return stateMgr.approximateNumUncommittedBytes();
+ }
+
@Override
public void maybeCheckpoint() {
final long now = time.milliseconds();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStreamThread.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStreamThread.java
index bede888525a..4b5bc646e0d 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStreamThread.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/GlobalStreamThread.java
@@ -75,6 +75,7 @@ public class GlobalStreamThread extends Thread {
private volatile long fetchDeadlineClientInstanceId = -1;
private volatile KafkaFutureImpl<Uuid> clientInstanceIdFuture = new
KafkaFutureImpl<>();
private final CountDownLatch initializationLatch = new CountDownLatch(1);
+ private volatile long maxUncommittedBytes;
/**
* The states that the global stream thread can be in
@@ -202,6 +203,7 @@ public class GlobalStreamThread extends Thread {
final Consumer<byte[], byte[]> globalConsumer,
final StateDirectory stateDirectory,
final long cacheSizeBytes,
+ final long maxUncommittedBytes,
final StreamsMetricsImpl streamsMetrics,
final Time time,
final String threadClientId,
@@ -221,6 +223,7 @@ public class GlobalStreamThread extends Thread {
this.stateRestoreListener = stateRestoreListener;
this.streamsUncaughtExceptionHandler = streamsUncaughtExceptionHandler;
this.cacheSize = new AtomicLong(-1L);
+ this.maxUncommittedBytes = maxUncommittedBytes;
}
static class StateConsumer {
@@ -259,6 +262,14 @@ public class GlobalStreamThread extends Thread {
stateMaintainer.maybeCheckpoint();
}
+ void flushState() {
+ stateMaintainer.flushState();
+ }
+
+ long approximateNumUncommittedBytes() {
+ return stateMaintainer.approximateNumUncommittedBytes();
+ }
+
public void close(final boolean wipeStateStore) throws IOException {
try {
globalConsumer.close();
@@ -301,6 +312,13 @@ public class GlobalStreamThread extends Thread {
}
stateConsumer.pollAndUpdate();
+ final long uncommittedLimit = maxUncommittedBytes;
+ if (uncommittedLimit > 0
+ && stateConsumer.approximateNumUncommittedBytes() >
uncommittedLimit) {
+ log.debug("Committing global state: uncommitted bytes
exceeded {}", uncommittedLimit);
+ stateConsumer.flushState();
+ }
+
if (fetchDeadlineClientInstanceId != -1) {
if (fetchDeadlineClientInstanceId >= time.milliseconds()) {
try {
@@ -372,6 +390,10 @@ public class GlobalStreamThread extends Thread {
this.cacheSize.set(cacheSize);
}
+ public void resizeMaxUncommittedBytes(final long maxUncommittedBytes) {
+ this.maxUncommittedBytes = maxUncommittedBytes;
+ }
+
private StateConsumer initialize() {
StateConsumer stateConsumer = null;
try {
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/ProcessorStateManager.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/ProcessorStateManager.java
index d6a5a2fea3c..6d620a56a52 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/ProcessorStateManager.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/ProcessorStateManager.java
@@ -629,6 +629,14 @@ public class ProcessorStateManager implements StateManager
{
}
}
+ long approximateNumUncommittedBytes() {
+ long total = 0;
+ for (final StateStoreMetadata metadata : stores.values()) {
+ total += metadata.stateStore.approximateNumUncommittedBytes();
+ }
+ return total;
+ }
+
/**
* {@link StateStore#close() Close} all stores (even in case of failure).
* Log all exceptions and re-throw the first exception that occurred at
the end.
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/ReadOnlyTask.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/ReadOnlyTask.java
index e81fabc704f..eaaf9db01e1 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/ReadOnlyTask.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/ReadOnlyTask.java
@@ -246,4 +246,9 @@ public class ReadOnlyTask implements Task {
public ProcessorStateManager stateManager() {
throw new UnsupportedOperationException("This task is read-only");
}
+
+ @Override
+ public long approximateNumUncommittedBytes() {
+ throw new UnsupportedOperationException("This task is read-only");
+ }
}
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamTask.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamTask.java
index 3b65e677674..2ab631fcfaa 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamTask.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamTask.java
@@ -1351,6 +1351,11 @@ public class StreamTask extends AbstractTask implements
ProcessorNodePunctuator,
return sb.toString();
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return stateMgr.approximateNumUncommittedBytes();
+ }
+
@Override
public boolean commitNeeded() {
// we need to do an extra check if the flag was false, that
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java
index b265c3fc276..c5c7e3b87cb 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java
@@ -330,6 +330,7 @@ public class StreamThread extends Thread implements
ProcessingThread {
private final Sensor commitRatioSensor;
private final Sensor failedStreamThreadSensor;
+ private volatile long maxUncommittedBytesPerThread;
private final long logSummaryIntervalMs; // the count summary log output
time interval
private long lastLogSummaryMs = -1L;
private long totalRecordsProcessedSinceLastSummary = 0L;
@@ -406,6 +407,7 @@ public class StreamThread extends Thread implements
ProcessingThread {
final Time time,
final StreamsMetadataState
streamsMetadataState,
final long cacheSizeBytes,
+ final long maxUncommittedBytesPerThread,
final StateDirectory stateDirectory,
final StateRestoreListener
userStateRestoreListener,
final StandbyUpdateListener
userStandbyUpdateListener,
@@ -542,7 +544,8 @@ public class StreamThread extends Thread implements
ProcessingThread {
cache::resize,
mainConsumerSetup.streamsRebalanceData,
streamsMetadataState,
- metricsReporter
+ metricsReporter,
+ maxUncommittedBytesPerThread
);
return streamThread.updateThreadMetadata(adminClientId(clientId));
@@ -790,7 +793,8 @@ public class StreamThread extends Thread implements
ProcessingThread {
final java.util.function.Consumer<Long> cacheResizer,
final Optional<StreamsRebalanceData>
streamsRebalanceData,
final StreamsMetadataState streamsMetadataState,
- final StreamsThreadMetricsDelegatingReporter
metricsReporter
+ final StreamsThreadMetricsDelegatingReporter
metricsReporter,
+ final long maxUncommittedBytesPerThread
) {
super(threadId);
this.stateLock = new Object();
@@ -885,6 +889,7 @@ public class StreamThread extends Thread implements
ProcessingThread {
this.eosEnabled = eosEnabled(config);
this.processingThreadsEnabled =
InternalConfig.processingThreadsEnabled(config.originals());
this.logSummaryIntervalMs =
config.getLong(StreamsConfig.LOG_SUMMARY_INTERVAL_MS_CONFIG);
+ this.maxUncommittedBytesPerThread = maxUncommittedBytesPerThread;
this.streamsRebalanceData = streamsRebalanceData;
this.streamsMetadataState = streamsMetadataState;
@@ -1816,7 +1821,7 @@ public class StreamThread extends Thread implements
ProcessingThread {
// visible for testing
int maybeCommit() {
final int committed;
- if (now - lastCommitMs > commitTimeMs) {
+ if (now - lastCommitMs > commitTimeMs ||
shouldCommitDueToUncommittedBytes()) {
if (log.isDebugEnabled()) {
log.debug("Committing all active tasks {} and standby tasks {}
since {}ms has elapsed (commit interval is {}ms)",
taskManager.activeRunningTaskIds(),
taskManager.standbyTaskIds(), now - lastCommitMs, commitTimeMs);
@@ -1849,6 +1854,16 @@ public class StreamThread extends Thread implements
ProcessingThread {
return committed;
}
+ private boolean shouldCommitDueToUncommittedBytes() {
+ final long limit = maxUncommittedBytesPerThread;
+ if (limit <= 0) return false;
+ return taskManager.totalUncommittedBytes() > limit;
+ }
+
+ public void resizeMaxUncommittedBytes(final long
maxUncommittedBytesPerThread) {
+ this.maxUncommittedBytesPerThread = maxUncommittedBytesPerThread;
+ }
+
/**
* Compute the latency based on the current marked timestamp, and update
the marked timestamp
* with the current system timestamp.
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/Task.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/Task.java
index e2404badef3..eb7f45042e7 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/Task.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/Task.java
@@ -238,6 +238,10 @@ public interface Task {
boolean commitNeeded();
+ default long approximateNumUncommittedBytes() {
+ return 0;
+ }
+
default boolean commitRequested() {
return false;
}
diff --git
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/TaskManager.java
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/TaskManager.java
index a25e0e32f88..0e2723d07af 100644
---
a/streams/src/main/java/org/apache/kafka/streams/processor/internals/TaskManager.java
+++
b/streams/src/main/java/org/apache/kafka/streams/processor/internals/TaskManager.java
@@ -1690,6 +1690,14 @@ public class TaskManager {
return tasks.allInitializedTasksPerId();
}
+ long totalUncommittedBytes() {
+ long total = 0;
+ for (final Task task : allRunningTasks().values()) {
+ total += task.approximateNumUncommittedBytes();
+ }
+ return total;
+ }
+
Set<Task> readOnlyAllTasks() {
// not bothering with an unmodifiable map, since the tasks themselves
are mutable, but
// if any outside code modifies the map or the tasks, it would be a
severe transgression.
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStore.java
index c11e276a19e..9357583ec53 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStore.java
@@ -334,6 +334,15 @@ public class AbstractRocksDBSegmentedBytesStore<S extends
Segment> implements Se
return segments.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ long total = 0;
+ for (final S segment : segments.allSegments(true)) {
+ total += segment.approximateNumUncommittedBytes();
+ }
+ return total;
+ }
+
@Override
public void close() {
open = false;
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/KeyValueStoreWrapper.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/KeyValueStoreWrapper.java
index 62d939562ca..836a609c184 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/KeyValueStoreWrapper.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/KeyValueStoreWrapper.java
@@ -151,6 +151,11 @@ public class KeyValueStoreWrapper<K, V> implements
StateStore {
store.commit(changelogOffsets);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return store.approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
store.close();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/KeyValueToTimestampedKeyValueByteStoreAdapter.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/KeyValueToTimestampedKeyValueByteStoreAdapter.java
index 3612776fbeb..5c5ededb8b4 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/KeyValueToTimestampedKeyValueByteStoreAdapter.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/KeyValueToTimestampedKeyValueByteStoreAdapter.java
@@ -117,6 +117,11 @@ public class KeyValueToTimestampedKeyValueByteStoreAdapter
implements KeyValueSt
return store.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return store.approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
store.close();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/LogicalKeyValueSegments.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/LogicalKeyValueSegments.java
index 9fbdf762c8d..90f15a33daa 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/LogicalKeyValueSegments.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/LogicalKeyValueSegments.java
@@ -55,6 +55,10 @@ public class LogicalKeyValueSegments extends
AbstractSegments<LogicalKeyValueSeg
this.physicalStore = new RocksDBStore(name, parentDir,
metricsRecorder, false);
}
+ RocksDBStore physicalStore() {
+ return physicalStore;
+ }
+
@Override
protected LogicalKeyValueSegment createSegment(final long segmentId, final
String segmentName) {
if (segmentId < 0) {
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStore.java
index 327ba3ff69a..ca8f12ac743 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredKeyValueStore.java
@@ -190,6 +190,10 @@ public class MeteredKeyValueStore<K, V>
}
);
}
+ StateStoreMetrics.addUncommittedBytesGauge(
+ taskId.toString(), metricsScope, name(), streamsMetrics,
+ (config, now) -> wrapped().approximateNumUncommittedBytes()
+ );
}
@Override
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/PlainToHeadersStoreAdapter.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/PlainToHeadersStoreAdapter.java
index 17415dbe207..4fa41db8ece 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/PlainToHeadersStoreAdapter.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/PlainToHeadersStoreAdapter.java
@@ -121,6 +121,11 @@ public class PlainToHeadersStoreAdapter implements
KeyValueStore<Bytes, byte[]>
return store.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return store.approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
store.close();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/PlainToHeadersWindowStoreAdapter.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/PlainToHeadersWindowStoreAdapter.java
index a463dbc97f2..218182878e8 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/PlainToHeadersWindowStoreAdapter.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/PlainToHeadersWindowStoreAdapter.java
@@ -232,6 +232,11 @@ public class PlainToHeadersWindowStoreAdapter implements
WindowStore<Bytes, byte
return store.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return store.approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
store.close();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBVersionedStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBVersionedStore.java
index e9c705972ee..c4c26d047f5 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBVersionedStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBVersionedStore.java
@@ -320,6 +320,12 @@ public class RocksDBVersionedStore implements
VersionedKeyValueStore<Bytes, byte
return segmentStores.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ // All logical segments share one physical store — its buffer holds
all uncommitted bytes
+ return segmentStores.physicalStore().approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
open = false;
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/SessionToHeadersStoreAdapter.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/SessionToHeadersStoreAdapter.java
index db753e52bbe..91c79234325 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/SessionToHeadersStoreAdapter.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/SessionToHeadersStoreAdapter.java
@@ -167,6 +167,11 @@ public class SessionToHeadersStoreAdapter implements
SessionStore<Bytes, byte[]>
return store.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return store.approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
store.close();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersStoreAdapter.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersStoreAdapter.java
index facef5f0a06..918893b77d5 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersStoreAdapter.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersStoreAdapter.java
@@ -123,6 +123,11 @@ public class TimestampedToHeadersStoreAdapter implements
KeyValueStore<Bytes, by
return store.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return store.approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
store.close();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersWindowStoreAdapter.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersWindowStoreAdapter.java
index cde8ac607d8..889533f5534 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersWindowStoreAdapter.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersWindowStoreAdapter.java
@@ -179,6 +179,11 @@ public class TimestampedToHeadersWindowStoreAdapter
implements WindowStore<Bytes
return store.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return store.approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
store.close();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/VersionedKeyValueToBytesStoreAdapter.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/VersionedKeyValueToBytesStoreAdapter.java
index c02cec5747a..ee422bd837e 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/VersionedKeyValueToBytesStoreAdapter.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/VersionedKeyValueToBytesStoreAdapter.java
@@ -110,6 +110,11 @@ public class VersionedKeyValueToBytesStoreAdapter
implements VersionedBytesStore
return inner.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return inner.approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
inner.close();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/WindowToTimestampedWindowByteStoreAdapter.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/WindowToTimestampedWindowByteStoreAdapter.java
index 5a7f05c1b2b..eb037c77534 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/WindowToTimestampedWindowByteStoreAdapter.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/WindowToTimestampedWindowByteStoreAdapter.java
@@ -178,6 +178,11 @@ public class WindowToTimestampedWindowByteStoreAdapter
implements WindowStore<By
return store.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return store.approximateNumUncommittedBytes();
+ }
+
@Override
public void close() {
store.close();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/WrappedStateStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/WrappedStateStore.java
index 73786170a27..6032feb362a 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/WrappedStateStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/WrappedStateStore.java
@@ -174,6 +174,11 @@ public abstract class WrappedStateStore<S extends
StateStore, K, V> implements S
return result;
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return wrapped.approximateNumUncommittedBytes();
+ }
+
@Override
public Position getPosition() {
return wrapped.getPosition();
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/metrics/StateStoreMetrics.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/metrics/StateStoreMetrics.java
index 551ee1d0983..8a235a5db5f 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/metrics/StateStoreMetrics.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/metrics/StateStoreMetrics.java
@@ -169,6 +169,10 @@ public class StateStoreMetrics {
private static final String NUM_KEYS_DESCRIPTION =
"The current number of keys in the in-memory state store";
+ private static final String UNCOMMITTED_BYTES = "uncommitted-bytes";
+ private static final String UNCOMMITTED_BYTES_DESCRIPTION =
+ "The approximate number of uncommitted bytes in the transactional
state store buffer";
+
private static final String OLDEST_ITERATOR_OPEN_SINCE_MS =
"oldest-iterator-open-since-ms";
private static final String OLDEST_ITERATOR_OPEN_SINCE_MS_DESCRIPTION =
"The UNIX timestamp the oldest still open iterator was created, in
milliseconds";
@@ -563,4 +567,20 @@ public class StateStoreMetrics {
numKeysGauge
);
}
+
+ public static void addUncommittedBytesGauge(final String taskId,
+ final String storeType,
+ final String storeName,
+ final StreamsMetricsImpl
streamsMetrics,
+ final Gauge<Long>
uncommittedBytesGauge) {
+ streamsMetrics.addStoreLevelMutableMetric(
+ taskId,
+ storeType,
+ storeName,
+ UNCOMMITTED_BYTES,
+ UNCOMMITTED_BYTES_DESCRIPTION,
+ RecordingLevel.INFO,
+ uncommittedBytesGauge
+ );
+ }
}
diff --git
a/streams/src/test/java/org/apache/kafka/streams/KafkaStreamsTest.java
b/streams/src/test/java/org/apache/kafka/streams/KafkaStreamsTest.java
index 4ee975805dc..e1d8af5d318 100644
--- a/streams/src/test/java/org/apache/kafka/streams/KafkaStreamsTest.java
+++ b/streams/src/test/java/org/apache/kafka/streams/KafkaStreamsTest.java
@@ -255,6 +255,7 @@ public class KafkaStreamsTest {
any(Time.class),
any(StreamsMetadataState.class),
anyLong(),
+ anyLong(),
any(StateDirectory.class),
any(StateRestoreListener.class),
any(StandbyUpdateListener.class),
diff --git
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStateManagerImplTest.java
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStateManagerImplTest.java
index 4759bdc0508..c5eefac3bca 100644
---
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStateManagerImplTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStateManagerImplTest.java
@@ -394,6 +394,34 @@ public class GlobalStateManagerImplTest {
}
+ @Test
+ public void
shouldReportZeroApproximateNumUncommittedBytesBeforeStoresAreRegistered() {
+ initializeConsumer(0, 0, t1, t2, t3, t4, t5);
+ stateManager.initialize();
+ assertEquals(0L, stateManager.approximateNumUncommittedBytes());
+ }
+
+ @Test
+ public void
shouldAggregateApproximateNumUncommittedBytesAcrossRegisteredStores() {
+ initializeConsumer(0, 0, t1, t2, t3, t4, t5);
+ stateManager.initialize();
+ initializeConsumer(1, 0, t1);
+ stateManager.registerStore(new NoOpReadOnlyStore<>(store1.name()) {
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return 100L;
+ }
+ }, stateRestoreCallback, null);
+ initializeConsumer(1, 0, t2);
+ stateManager.registerStore(new NoOpReadOnlyStore<>(store2.name()) {
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return 250L;
+ }
+ }, stateRestoreCallback, null);
+ assertEquals(350L, stateManager.approximateNumUncommittedBytes());
+ }
+
@Test
public void shouldCommitStateStores() {
initializeConsumer(0, 0, t1, t2, t3, t4, t5);
diff --git
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStateTaskTest.java
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStateTaskTest.java
index d5f22be3a2b..c082f9d6bc7 100644
---
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStateTaskTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStateTaskTest.java
@@ -246,6 +246,12 @@ public class GlobalStateTaskTest {
}
+ @Test
+ public void shouldDelegateApproximateNumUncommittedBytesToStateManager() {
+ stateMgr.approximateNumUncommittedBytes = 4242L;
+ assertEquals(4242L, globalStateTask.approximateNumUncommittedBytes());
+ }
+
@Test
public void shouldCommitStateManagerWithOffsets() {
final Map<TopicPartition, Long> expectedOffsets = new HashMap<>();
diff --git
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStreamThreadTest.java
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStreamThreadTest.java
index 7c0216cd07f..8a77af5a2ea 100644
---
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStreamThreadTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/GlobalStreamThreadTest.java
@@ -132,6 +132,7 @@ public class GlobalStreamThreadTest {
mockConsumer,
new StateDirectory(config, time, true, false),
0,
+ -1L,
new StreamsMetricsImpl(new Metrics(), "test-client", time),
time,
"clientId",
@@ -169,6 +170,7 @@ public class GlobalStreamThreadTest {
mockConsumer,
new StateDirectory(config, time, true, false),
0,
+ -1L,
new StreamsMetricsImpl(new Metrics(), "test-client", time),
time,
"clientId",
@@ -188,6 +190,29 @@ public class GlobalStreamThreadTest {
assertFalse(globalStreamThread.stillRunning());
}
+ @Test
+ public void shouldAllowResizingMaxUncommittedBytesBeforeStart() {
+ // Invoking the resize should be safe before the thread has started.
+ globalStreamThread.resizeMaxUncommittedBytes(4096L);
+ }
+
+ @Test
+ public void shouldAllowResizingMaxUncommittedBytesWhileRunning() throws
Exception {
+ initializeConsumer();
+ startAndSwallowError();
+
+ TestUtils.waitForCondition(
+ () -> globalStreamThread.state() == RUNNING,
+ 10 * 1000,
+ "Thread never started.");
+
+ globalStreamThread.resizeMaxUncommittedBytes(4096L);
+ assertTrue(globalStreamThread.stillRunning());
+
+ globalStreamThread.shutdown();
+ globalStreamThread.join();
+ }
+
@Test
public void shouldBeRunningAfterSuccessfulStart() throws Exception {
initializeConsumer();
@@ -418,6 +443,7 @@ public class GlobalStreamThreadTest {
consumer,
new StateDirectory(config, time, true, false),
0,
+ -1L,
new StreamsMetricsImpl(new Metrics(), "test-client", time),
time,
"clientId",
diff --git
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StateConsumerTest.java
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StateConsumerTest.java
index 72c0fc38d8c..a779678098e 100644
---
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StateConsumerTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StateConsumerTest.java
@@ -145,6 +145,11 @@ public class StateConsumerTest {
public void maybeCheckpoint() {
flushState();
}
+
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return 0;
+ }
}
}
diff --git
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamThreadTest.java
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamThreadTest.java
index fb8bb26c0be..95292c15899 100644
---
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamThreadTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamThreadTest.java
@@ -328,6 +328,7 @@ public class StreamThreadTest {
time,
streamsMetadataState,
0,
+ -1L,
stateDirectory,
new MockStateRestoreListener(),
new MockStandbyUpdateListener(),
@@ -764,6 +765,7 @@ public class StreamThreadTest {
mockTime,
streamsMetadataState,
0,
+ -1L,
stateDirectory,
new MockStateRestoreListener(),
new MockStandbyUpdateListener(),
@@ -826,6 +828,7 @@ public class StreamThreadTest {
mockTime,
streamsMetadataState,
0,
+ -1L,
stateDirectory,
new MockStateRestoreListener(),
new MockStandbyUpdateListener(),
@@ -1100,6 +1103,166 @@ public class StreamThreadTest {
assertTrue(committed.get());
}
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldCommitWhenUncommittedBytesExceedLimit(final boolean
processingThreadsEnabled) {
+ final long commitInterval = Duration.ofMinutes(1).toMillis();
+ final long maxUncommittedBytesPerThread = 1024L;
+
+ final Properties props = configProps(false, processingThreadsEnabled);
+ props.setProperty(StreamsConfig.STATE_DIR_CONFIG, stateDir);
+ props.setProperty(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG,
Long.toString(commitInterval));
+
+ final StreamsConfig config = new StreamsConfig(props);
+ final ConsumerGroupMetadata consumerGroupMetadata =
mock(ConsumerGroupMetadata.class);
+ when(consumer.groupMetadata()).thenReturn(consumerGroupMetadata);
+
when(consumerGroupMetadata.groupInstanceId()).thenReturn(Optional.empty());
+
+ final AtomicBoolean committed = new AtomicBoolean(false);
+ final AtomicLong uncommittedBytes = new AtomicLong(0L);
+ final TopologyMetadata topologyMetadata = new
TopologyMetadata(internalTopologyBuilder, config);
+ final TaskManager taskManager = new TaskManager(
+ null, null, null, null, null, null,
+ new Tasks(new LogContext()),
+ topologyMetadata,
+ null, null, null, null
+ ) {
+ @Override
+ int commit(final Collection<? extends Task> tasksToCommit) {
+ committed.set(true);
+ return 1;
+ }
+
+ @Override
+ long totalUncommittedBytes() {
+ return uncommittedBytes.get();
+ }
+ };
+ topologyMetadata.buildAndRewriteTopology();
+ thread = buildStreamThread(consumer, taskManager, config,
topologyMetadata, maxUncommittedBytesPerThread);
+
+ // prime lastCommitMs so the interval-based trigger does not fire
+ thread.setNow(mockTime.milliseconds());
+ thread.maybeCommit();
+ committed.set(false);
+
+ // below the limit: no commit
+ uncommittedBytes.set(maxUncommittedBytesPerThread);
+ thread.setNow(mockTime.milliseconds());
+ thread.maybeCommit();
+ assertFalse(committed.get());
+
+ // exceeding the limit triggers an early commit regardless of commit
interval
+ uncommittedBytes.set(maxUncommittedBytesPerThread + 1);
+ thread.setNow(mockTime.milliseconds());
+ thread.maybeCommit();
+ assertTrue(committed.get());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldNotCommitOnUncommittedBytesWhenLimitIsDisabled(final
boolean processingThreadsEnabled) {
+ final long commitInterval = Duration.ofMinutes(1).toMillis();
+
+ final Properties props = configProps(false, processingThreadsEnabled);
+ props.setProperty(StreamsConfig.STATE_DIR_CONFIG, stateDir);
+ props.setProperty(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG,
Long.toString(commitInterval));
+
+ final StreamsConfig config = new StreamsConfig(props);
+ final ConsumerGroupMetadata consumerGroupMetadata =
mock(ConsumerGroupMetadata.class);
+ when(consumer.groupMetadata()).thenReturn(consumerGroupMetadata);
+
when(consumerGroupMetadata.groupInstanceId()).thenReturn(Optional.empty());
+
+ final AtomicBoolean committed = new AtomicBoolean(false);
+ final TopologyMetadata topologyMetadata = new
TopologyMetadata(internalTopologyBuilder, config);
+ final TaskManager taskManager = new TaskManager(
+ null, null, null, null, null, null,
+ new Tasks(new LogContext()),
+ topologyMetadata,
+ null, null, null, null
+ ) {
+ @Override
+ int commit(final Collection<? extends Task> tasksToCommit) {
+ committed.set(true);
+ return 1;
+ }
+
+ @Override
+ long totalUncommittedBytes() {
+ return Long.MAX_VALUE;
+ }
+ };
+ topologyMetadata.buildAndRewriteTopology();
+ thread = buildStreamThread(consumer, taskManager, config,
topologyMetadata, -1L);
+
+ // prime lastCommitMs so the interval-based trigger does not fire
+ thread.setNow(mockTime.milliseconds());
+ thread.maybeCommit();
+ committed.set(false);
+
+ thread.setNow(mockTime.milliseconds());
+ thread.maybeCommit();
+ assertFalse(committed.get());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldResizeMaxUncommittedBytes(final boolean
processingThreadsEnabled) {
+ final long commitInterval = Duration.ofMinutes(1).toMillis();
+ final long initialLimit = 1024L;
+ final long resizedLimit = 4096L;
+
+ final Properties props = configProps(false, processingThreadsEnabled);
+ props.setProperty(StreamsConfig.STATE_DIR_CONFIG, stateDir);
+ props.setProperty(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG,
Long.toString(commitInterval));
+
+ final StreamsConfig config = new StreamsConfig(props);
+ final ConsumerGroupMetadata consumerGroupMetadata =
mock(ConsumerGroupMetadata.class);
+ when(consumer.groupMetadata()).thenReturn(consumerGroupMetadata);
+
when(consumerGroupMetadata.groupInstanceId()).thenReturn(Optional.empty());
+
+ final AtomicBoolean committed = new AtomicBoolean(false);
+ final AtomicLong uncommittedBytes = new AtomicLong(0L);
+ final TopologyMetadata topologyMetadata = new
TopologyMetadata(internalTopologyBuilder, config);
+ final TaskManager taskManager = new TaskManager(
+ null, null, null, null, null, null,
+ new Tasks(new LogContext()),
+ topologyMetadata,
+ null, null, null, null
+ ) {
+ @Override
+ int commit(final Collection<? extends Task> tasksToCommit) {
+ committed.set(true);
+ return 1;
+ }
+
+ @Override
+ long totalUncommittedBytes() {
+ return uncommittedBytes.get();
+ }
+ };
+ topologyMetadata.buildAndRewriteTopology();
+ thread = buildStreamThread(consumer, taskManager, config,
topologyMetadata, initialLimit);
+
+ // prime lastCommitMs so the interval-based trigger does not fire
+ thread.setNow(mockTime.milliseconds());
+ thread.maybeCommit();
+ committed.set(false);
+
+ // usage between initial and resized limit: commits under initial
limit, but
+ // should no longer commit once the limit is raised above the usage
+ uncommittedBytes.set(initialLimit + 1);
+ thread.setNow(mockTime.milliseconds());
+ thread.maybeCommit();
+ assertTrue(committed.get());
+
+ thread.resizeMaxUncommittedBytes(resizedLimit);
+ committed.set(false);
+ thread.setNow(mockTime.milliseconds());
+ thread.maybeCommit();
+ assertFalse(committed.get());
+ }
+
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void shouldPurgeAfterPurgeInterval(final boolean
processingThreadsEnabled) {
@@ -1453,7 +1616,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
final StreamsException thrown = assertThrows(StreamsException.class,
thread::run);
@@ -1549,7 +1713,7 @@ public class StreamThreadTest {
PROCESS_ID, CLIENT_ID, new LogContext(""),
null, new AtomicLong(Long.MAX_VALUE), new LinkedList<>(),
null, HANDLER, null,
- Optional.of(streamsRebalanceData), null, null
+ Optional.of(streamsRebalanceData), null, null, -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.shutdown(org.apache.kafka.streams.CloseOptions.GroupMembershipOperation.DEFAULT);
@@ -1582,7 +1746,7 @@ public class StreamThreadTest {
PROCESS_ID, CLIENT_ID, new LogContext(""),
null, new AtomicLong(Long.MAX_VALUE), new LinkedList<>(),
null, HANDLER, null,
- Optional.of(streamsRebalanceData), null, null
+ Optional.of(streamsRebalanceData), null, null, -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.shutdown(org.apache.kafka.streams.CloseOptions.GroupMembershipOperation.REMAIN_IN_GROUP);
@@ -2008,6 +2172,7 @@ public class StreamThreadTest {
mockTime,
streamsMetadataState,
0,
+ -1L,
stateDirectory,
new MockStateRestoreListener(),
new MockStandbyUpdateListener(),
@@ -2571,7 +2736,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ -1L
) {
@Override
void runOnceWithProcessingThreads() {
@@ -2633,7 +2799,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ -1L
) {
@Override
void runOnceWithProcessingThreads() {
@@ -2703,7 +2870,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ -1L
) {
@Override
void runOnceWithProcessingThreads() {
@@ -2770,7 +2938,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ -1L
) {
@Override
void runOnceWithProcessingThreads() {
@@ -2842,7 +3011,8 @@ public class StreamThreadTest {
null,
Optional.of(streamsRebalanceData),
null,
- null
+ null,
+ -1L
) {
@Override
void runOnceWithProcessingThreads() {
@@ -2909,7 +3079,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ -1L
) {
@Override
void runOnceWithProcessingThreads() {
@@ -3142,7 +3313,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ -1L
);
final MetricName testMetricName = new MetricName("test_metric", "",
"", new HashMap<>());
final Metric testMetric = new KafkaMetric(
@@ -3202,7 +3374,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ -1L
) {
@Override
void runOnceWithProcessingThreads() {
@@ -3581,6 +3754,7 @@ public class StreamThreadTest {
mockTime,
streamsMetadataState,
0,
+ -1L,
stateDirectory,
new MockStateRestoreListener(),
new MockStandbyUpdateListener(),
@@ -3639,6 +3813,7 @@ public class StreamThreadTest {
mockTime,
streamsMetadataState,
0,
+ -1L,
stateDirectory,
new MockStateRestoreListener(),
new MockStandbyUpdateListener(),
@@ -3704,6 +3879,7 @@ public class StreamThreadTest {
mockTime,
streamsMetadataState,
0,
+ -1L,
stateDirectory,
new MockStateRestoreListener(),
new MockStandbyUpdateListener(),
@@ -3790,7 +3966,8 @@ public class StreamThreadTest {
null,
Optional.of(streamsRebalanceData),
streamsMetadataState,
- null
+ null,
+ -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.setState(State.STARTING);
@@ -3849,7 +4026,8 @@ public class StreamThreadTest {
null,
Optional.of(streamsRebalanceData),
streamsMetadataState,
- null
+ null,
+ -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.sendShutdownRequest();
@@ -3896,7 +4074,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
streamsMetadataState,
- null
+ null,
+ -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.sendShutdownRequest();
@@ -3951,7 +4130,8 @@ public class StreamThreadTest {
null,
Optional.of(streamsRebalanceData),
streamsMetadataState,
- null
+ null,
+ -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.setState(State.STARTING);
@@ -4022,7 +4202,8 @@ public class StreamThreadTest {
null,
Optional.of(streamsRebalanceData),
streamsMetadataState,
- null
+ null,
+ -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.setState(State.STARTING);
@@ -4084,7 +4265,8 @@ public class StreamThreadTest {
null,
Optional.of(streamsRebalanceData),
streamsMetadataState,
- null
+ null,
+ -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.setState(State.STARTING);
@@ -4146,7 +4328,8 @@ public class StreamThreadTest {
null,
Optional.of(streamsRebalanceData),
streamsMetadataState,
- null
+ null,
+ -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.setState(State.STARTING);
@@ -4217,7 +4400,8 @@ public class StreamThreadTest {
null,
Optional.of(streamsRebalanceData),
streamsMetadataState,
- null
+ null,
+ -1L
).updateThreadMetadata(adminClientId(CLIENT_ID));
thread.setState(State.STARTING);
@@ -4342,7 +4526,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ -1L
);
}
@@ -4418,6 +4603,14 @@ public class StreamThreadTest {
final TaskManager taskManager,
final StreamsConfig config,
final TopologyMetadata
topologyMetadata) {
+ return buildStreamThread(consumer, taskManager, config,
topologyMetadata, -1L);
+ }
+
+ private StreamThread buildStreamThread(final Consumer<byte[], byte[]>
consumer,
+ final TaskManager taskManager,
+ final StreamsConfig config,
+ final TopologyMetadata
topologyMetadata,
+ final long
maxUncommittedBytesPerThread) {
final StreamsMetricsImpl streamsMetrics =
new StreamsMetricsImpl(metrics, CLIENT_ID, mockTime);
@@ -4444,7 +4637,8 @@ public class StreamThreadTest {
null,
Optional.empty(),
null,
- null
+ null,
+ maxUncommittedBytesPerThread
);
}
diff --git
a/streams/src/test/java/org/apache/kafka/test/GlobalStateManagerStub.java
b/streams/src/test/java/org/apache/kafka/test/GlobalStateManagerStub.java
index 1237deacf9b..354762737a0 100644
--- a/streams/src/test/java/org/apache/kafka/test/GlobalStateManagerStub.java
+++ b/streams/src/test/java/org/apache/kafka/test/GlobalStateManagerStub.java
@@ -36,6 +36,7 @@ public class GlobalStateManagerStub implements
GlobalStateManager {
public boolean initialized;
public boolean closed;
public boolean committed;
+ public long approximateNumUncommittedBytes;
public GlobalStateManagerStub(final Set<String> storeNames,
final Map<TopicPartition, Long> offsets,
@@ -69,6 +70,11 @@ public class GlobalStateManagerStub implements
GlobalStateManager {
committed = true;
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ return approximateNumUncommittedBytes;
+ }
+
@Override
public void close() {
closed = true;