[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r535383909 ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -870,43 +900,75 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, cacheSizePerThread, stateDirectory, delegatingStateRestoreListener, -i + 1, +threadIdx, KafkaStreams.this::closeToError, -this::defaultStreamsUncaughtExceptionHandler -); -threads.add(streamThread); -threadState.put(streamThread.getId(), streamThread.state()); -storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); -} +streamsUncaughtExceptionHandler Review comment: nit: If it happens that you need to push another commit, could you fix the indentation here? Sorry that I haven't noticed this before. ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -870,43 +900,75 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, cacheSizePerThread, stateDirectory, delegatingStateRestoreListener, -i + 1, +threadIdx, KafkaStreams.this::closeToError, -this::defaultStreamsUncaughtExceptionHandler -); -threads.add(streamThread); -threadState.put(streamThread.getId(), streamThread.state()); -storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); -} +streamsUncaughtExceptionHandler +); +streamThread.setStateListener(streamStateListener); +threads.add(streamThread); +threadState.put(streamThread.getId(), streamThread.state()); +storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); +return streamThread; +} -ClientMetrics.addNumAliveStreamThreadMetric(streamsMetrics, (metricsConfig, now) -> -Math.toIntExact(threads.stream().filter(thread -> thread.state().isAlive()).count())); +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@link StreamsConfig#CACHE_MAX_BYTES_BUFFERING_CONFIG}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +synchronized (newThread) { +if (isRunningOrRebalancing()) { +final int threadIdx = getNextThreadIndex(); +final long cacheSizePerThread = getCacheSizePerThread(threads.size() + 1); +resizeThreadCache(cacheSizePerThread); +final StreamThread streamThread = createStreamThread(cacheSizePerThread, threadIdx); +synchronized (stateLock) { +if (isRunningOrRebalancing()) { +streamThread.start(); +return Optional.of(streamThread.getName()); +} else { +streamThread.shutdown(); +threads.remove(streamThread); + resizeThreadCache(getCacheSizePerThread(threads.size())); +return Optional.empty(); +} +} +} +} +return Optional.empty(); +} -final StreamStateListener streamStateListener = new StreamStateListener(threadState, globalThreadState); -if (hasGlobalTopology) { -globalStreamThread.setStateListener(streamStateListener); +private int getNextThreadIndex() { +final HashSet names = new HashSet<>(); +for (final StreamThread streamThread: threads) { +names.add(streamThread.getName()); } -for (final StreamThread thread : threads) { -thread.setStateListener(streamStateListener); +final String baseName = clientId + "-StreamThread-"; +for (int i = 0; i < threads.size(); i++) { Review comment: Shouldn't that be `int i = 1; i <= threads.size(); i++`? Otherwise, we would look up `*-StreamThread-0"` and we would not look up `"*-StreamThread-" + threads.size()`. Could you add some tests that check the correct naming as @mjsax suggested? ##
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r534319500 ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -870,43 +899,73 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, cacheSizePerThread, stateDirectory, delegatingStateRestoreListener, -i + 1, +threadIdx, KafkaStreams.this::closeToError, -this::defaultStreamsUncaughtExceptionHandler -); -threads.add(streamThread); -threadState.put(streamThread.getId(), streamThread.state()); -storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); -} +streamsUncaughtExceptionHandler +); +streamThread.setStateListener(streamStateListener); +threads.add(streamThread); +threadState.put(streamThread.getId(), streamThread.state()); +storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); +return streamThread; +} -ClientMetrics.addNumAliveStreamThreadMetric(streamsMetrics, (metricsConfig, now) -> -Math.toIntExact(threads.stream().filter(thread -> thread.state().isAlive()).count())); +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@link StreamsConfig#CACHE_MAX_BYTES_BUFFERING_CONFIG}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +if (isRunningOrRebalancing()) { +final int threadIdx = getNextThreadIndex(); +final long cacheSizePerThread = getCacheSizePerThread(threads.size() + 1); +resizeThreadCache(cacheSizePerThread); +final StreamThread streamThread = createStreamThread(cacheSizePerThread, threadIdx); +synchronized (stateLock) { +if (isRunningOrRebalancing()) { +streamThread.start(); +return Optional.of(streamThread.getName()); +} else { +threads.remove(streamThread); Review comment: > Unfortunately I don't think we can shutdown a thread until we have started it. Have a look at https://github.com/apache/kafka/blob/aeeb7b2f9a9abe8f49543a2278757722e5974cb3/streams/src/main/java/org/apache/kafka/streams/processor/internals/StreamThread.java#L976-L983 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. For queries about this service, please contact Infrastructure at: us...@infra.apache.org
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r534263746 ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,19 +885,88 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +private StreamThread createStreamThread(final long cacheSizePerThread, final int threadIdx) { +final StreamThread streamThread = StreamThread.create( +internalTopologyBuilder, +config, +clientSupplier, +adminClient, +processId, +clientId, +streamsMetrics, +time, +streamsMetadataState, +cacheSizePerThread, +stateDirectory, +delegatingStateRestoreListener, +threadIdx, +KafkaStreams.this::closeToError, +streamsUncaughtExceptionHandler +); +threads.add(streamThread); +threadState.put(streamThread.getId(), streamThread.state()); +storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); +return streamThread; +} + +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +if (isRunningOrRebalancing()) { +final int threadIdx = getNextThreadIndex(); +final long cacheSizePerThread = getCacheSizePerThread(threads.size() + 1); +resizeThreadCache(cacheSizePerThread); +final StreamThread streamThread = createStreamThread(cacheSizePerThread, threadIdx); +streamThread.setStateListener(streamStateListener); +synchronized (stateLock) { +if (isRunningOrRebalancing()) { +streamThread.start(); Review comment: See my other comment https://github.com/apache/kafka/pull/9615#discussion_r534261843 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. For queries about this service, please contact Infrastructure at: us...@infra.apache.org
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r534261843 ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -870,43 +899,73 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, cacheSizePerThread, stateDirectory, delegatingStateRestoreListener, -i + 1, +threadIdx, KafkaStreams.this::closeToError, -this::defaultStreamsUncaughtExceptionHandler -); -threads.add(streamThread); -threadState.put(streamThread.getId(), streamThread.state()); -storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); -} +streamsUncaughtExceptionHandler +); +streamThread.setStateListener(streamStateListener); +threads.add(streamThread); +threadState.put(streamThread.getId(), streamThread.state()); +storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); +return streamThread; +} -ClientMetrics.addNumAliveStreamThreadMetric(streamsMetrics, (metricsConfig, now) -> -Math.toIntExact(threads.stream().filter(thread -> thread.state().isAlive()).count())); +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@link StreamsConfig#CACHE_MAX_BYTES_BUFFERING_CONFIG}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +if (isRunningOrRebalancing()) { +final int threadIdx = getNextThreadIndex(); +final long cacheSizePerThread = getCacheSizePerThread(threads.size() + 1); +resizeThreadCache(cacheSizePerThread); +final StreamThread streamThread = createStreamThread(cacheSizePerThread, threadIdx); +synchronized (stateLock) { +if (isRunningOrRebalancing()) { +streamThread.start(); +return Optional.of(streamThread.getName()); +} else { +threads.remove(streamThread); Review comment: Good point about the shutdown of the stream thread! Actually, I did not want to have everything in the synchronized block because I thought blocking the client state more than needed was not a good idea. I thought decreasing the size of the cache might be costly if the evicted records are forwarded downstream. Now that you mention to synchronize on a separate lock, I noticed that we probably need to put resize, start, and cleanup in the same synchronized block. The reason is that if two threads call `addStreamThread()` one after the other and the later thread passes ``` final long cacheSizePerThread = getCacheSizePerThread(threads.size() + 1); ``` before the earlier thread adds the new stream thread to `threads` in `createStreamThread()`, the later thread would compute the wrong cache size. So, I am in favor of having a separate lock that just synchronizes the threads calling `addStreamThread()`. Maybe we can simply synchronize the whole method (which means to synchronize with `start()` and `close()`). Still a minor issue seems to be the synchronization between`isRunningOrRebalancing()` and `streamThread.start()`. If between these two calls the Streams client transits to `ERROR` (the global stream thread died) an `IllegalStateException` would be thrown from the `StreamStateListener` because the Streams client would try to transit from `ERROR` to `REBALANCING`. But I guess that would also happen if the Streams client transits to `ERROR` before the new stream thread transits to `PARTITION_ASSIGNED` and calls the `StreamStateListener` that would transit the Streams client to `REBALANCING`. So it needs to be fixed somewhere else. Did I miss something? 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. For queries about this service, please contact Infrastructure at: us...@infra.apache.org
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r533687031 ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,19 +885,88 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +private StreamThread createStreamThread(final long cacheSizePerThread, final int threadIdx) { +final StreamThread streamThread = StreamThread.create( +internalTopologyBuilder, +config, +clientSupplier, +adminClient, +processId, +clientId, +streamsMetrics, +time, +streamsMetadataState, +cacheSizePerThread, +stateDirectory, +delegatingStateRestoreListener, +threadIdx, +KafkaStreams.this::closeToError, +streamsUncaughtExceptionHandler +); +threads.add(streamThread); +threadState.put(streamThread.getId(), streamThread.state()); +storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); +return streamThread; +} + +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +if (isRunningOrRebalancing()) { +final int threadIdx = getNextThreadIndex(); +final long cacheSizePerThread = getCacheSizePerThread(threads.size() + 1); +resizeThreadCache(cacheSizePerThread); +final StreamThread streamThread = createStreamThread(cacheSizePerThread, threadIdx); +streamThread.setStateListener(streamStateListener); +synchronized (stateLock) { +if (isRunningOrRebalancing()) { +streamThread.start(); Review comment: Yes, currently this assumption is correct, but if the state transitions change in future, we would be safe if we do the cleanup. On a second thought, we are probably not 100% safe because if a transition from `NOT_RUNNING` to `RUNNING` is added (or any other transition that goes from the above mentioned states to `RUNNING` or `REBALANCING`), we would still not do the clean up. 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. For queries about this service, please contact Infrastructure at: us...@infra.apache.org
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r533675429 ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,19 +885,88 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +private StreamThread createStreamThread(final long cacheSizePerThread, final int threadIdx) { +final StreamThread streamThread = StreamThread.create( +internalTopologyBuilder, +config, +clientSupplier, +adminClient, +processId, +clientId, +streamsMetrics, +time, +streamsMetadataState, +cacheSizePerThread, +stateDirectory, +delegatingStateRestoreListener, +threadIdx, +KafkaStreams.this::closeToError, +streamsUncaughtExceptionHandler +); +threads.add(streamThread); +threadState.put(streamThread.getId(), streamThread.state()); +storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); +return streamThread; +} + +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +if (isRunningOrRebalancing()) { +final int threadIdx = getNextThreadIndex(); +final long cacheSizePerThread = getCacheSizePerThread(threads.size() + 1); +resizeThreadCache(cacheSizePerThread); +final StreamThread streamThread = createStreamThread(cacheSizePerThread, threadIdx); +streamThread.setStateListener(streamStateListener); +synchronized (stateLock) { +if (isRunningOrRebalancing()) { +streamThread.start(); Review comment: What about checking for the state and do the clean-up only if the state is not `PENDING_SHUTDOWN` and not `ERROR` and not `NOT_RUNNING`? In this way we are safe for future changes that break our assumption on state transitions and we make sure not to do unnecessary stuff when we are shutting down. 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. For queries about this service, please contact Infrastructure at: us...@infra.apache.org
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r529858401 ## File path: streams/src/test/java/org/apache/kafka/streams/integration/AdjustStreamThreadCountTest.java ## @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + *http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.kafka.streams.integration; + +import org.apache.kafka.common.serialization.Serdes; +import org.apache.kafka.streams.KafkaStreams; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.StreamsConfig; +import org.apache.kafka.streams.integration.utils.EmbeddedKafkaCluster; +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils; +import org.apache.kafka.streams.processor.ThreadMetadata; +import org.apache.kafka.test.IntegrationTest; +import org.apache.kafka.test.StreamsTestUtils; +import org.apache.kafka.test.TestUtils; +import org.hamcrest.CoreMatchers; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestName; + +import java.io.IOException; +import java.util.Optional; +import java.util.Properties; + +import static org.apache.kafka.common.utils.Utils.mkEntry; +import static org.apache.kafka.common.utils.Utils.mkMap; +import static org.apache.kafka.common.utils.Utils.mkObjectProperties; +import static org.apache.kafka.streams.integration.utils.IntegrationTestUtils.purgeLocalStreamsState; +import static org.apache.kafka.streams.integration.utils.IntegrationTestUtils.safeUniqueTestName; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +@Category(IntegrationTest.class) +public class AdjustStreamThreadCountTest { + +@ClassRule +public static final EmbeddedKafkaCluster CLUSTER = new EmbeddedKafkaCluster(1); + +@Rule +public TestName testName = new TestName(); + +private static String inputTopic; +private static StreamsBuilder builder; +private static Properties properties; +private static String appId = ""; + +@Before +public void setup() { +final String testId = safeUniqueTestName(getClass(), testName); +appId = "appId_" + testId; +inputTopic = "input" + testId; +IntegrationTestUtils.cleanStateBeforeTest(CLUSTER, inputTopic); + +builder = new StreamsBuilder(); +builder.stream(inputTopic); + +properties = mkObjectProperties( +mkMap( +mkEntry(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, CLUSTER.bootstrapServers()), +mkEntry(StreamsConfig.APPLICATION_ID_CONFIG, appId), +mkEntry(StreamsConfig.STATE_DIR_CONFIG, TestUtils.tempDirectory().getPath()), +mkEntry(StreamsConfig.NUM_STREAM_THREADS_CONFIG, 2), +mkEntry(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.StringSerde.class), +mkEntry(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.StringSerde.class) +) +); +} + +@After +public void teardown() throws IOException { +purgeLocalStreamsState(properties); +} + +@Test +public void shouldAddStreamThread() throws Exception { +try (final KafkaStreams kafkaStreams = new KafkaStreams(builder.build(), properties)) { + StreamsTestUtils.startKafkaStreamsAndWaitForRunningState(kafkaStreams); +final int oldThreadCount = kafkaStreams.localThreadsMetadata().size(); + +final Optional name = kafkaStreams.addStreamThread(); +assertThat(name, CoreMatchers.not(Optional.empty())); +TestUtils.waitForCondition(() -> kafkaStreams.localThreadsMetadata().stream().sequential().map(ThreadMetadata::threadName).anyMatch(t -> t.equals(name.orElse(""))), +"Wait for the thread to be added"); +assertThat(kafkaStreams.localThreadsMetadata().size(), equalTo(oldThreadCount + 1)); Review comment: nit: If not all parameters fit on one line, we put each of them on a new line. Additionally we put also the closing parenthesis on a new line.
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r529461975 ## File path: streams/src/test/java/org/apache/kafka/streams/integration/AdjustStreamThreadCountTest.java ## @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + *http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.kafka.streams.integration; + +import org.apache.kafka.common.serialization.Serdes; +import org.apache.kafka.common.serialization.StringSerializer; +import org.apache.kafka.streams.KafkaStreams; +import org.apache.kafka.streams.KeyValue; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.StreamsConfig; +import org.apache.kafka.streams.integration.utils.EmbeddedKafkaCluster; +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils; +import org.apache.kafka.test.IntegrationTest; +import org.apache.kafka.test.StreamsTestUtils; +import org.apache.kafka.test.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestName; + +import java.io.IOException; +import java.util.Collections; +import java.util.Properties; + +import static org.apache.kafka.common.utils.Utils.mkEntry; +import static org.apache.kafka.common.utils.Utils.mkMap; +import static org.apache.kafka.common.utils.Utils.mkObjectProperties; +import static org.apache.kafka.streams.integration.utils.IntegrationTestUtils.purgeLocalStreamsState; +import static org.apache.kafka.streams.integration.utils.IntegrationTestUtils.safeUniqueTestName; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +@Category(IntegrationTest.class) +public class AdjustStreamThreadCountTest { + +@ClassRule +public static final EmbeddedKafkaCluster CLUSTER = new EmbeddedKafkaCluster(1); + +@Rule +public TestName testName = new TestName(); + +private static String inputTopic; +private static StreamsBuilder builder; +private static Properties properties; +private static String appId = ""; + +@Before +public void setup() { +final String testId = safeUniqueTestName(getClass(), testName); +appId = "appId_" + testId; +inputTopic = "input" + testId; +IntegrationTestUtils.cleanStateBeforeTest(CLUSTER, inputTopic); + +builder = new StreamsBuilder(); +builder.stream(inputTopic); + +properties = mkObjectProperties( +mkMap( +mkEntry(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, CLUSTER.bootstrapServers()), +mkEntry(StreamsConfig.APPLICATION_ID_CONFIG, appId), +mkEntry(StreamsConfig.STATE_DIR_CONFIG, TestUtils.tempDirectory().getPath()), +mkEntry(StreamsConfig.NUM_STREAM_THREADS_CONFIG, 2), +mkEntry(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.StringSerde.class), + mkEntry(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.StringSerde.class) +) +); +} + +@After +public void teardown() throws IOException { +purgeLocalStreamsState(properties); +} + +private void produceMessages(final long timestamp, final String streamOneInput, final String msg) { +IntegrationTestUtils.produceKeyValuesSynchronouslyWithTimestamp( +streamOneInput, +Collections.singletonList(new KeyValue<>("1", msg)), +TestUtils.producerConfig( +CLUSTER.bootstrapServers(), +StringSerializer.class, +StringSerializer.class, +new Properties()), +timestamp); Review comment: wrong indentation ## File path: streams/src/test/java/org/apache/kafka/streams/integration/AdjustStreamThreadCountTest.java ## @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r527530444 ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,11 +885,77 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +private StreamThread makeThread(final long cacheSizePerThread, final int threadIdx) { +final StreamThread streamThread = StreamThread.create( +internalTopologyBuilder, +config, +clientSupplier, +adminClient, +processId, +clientId, +streamsMetrics, +time, +streamsMetadataState, +cacheSizePerThread, +stateDirectory, +delegatingStateRestoreListener, +threadIdx, +KafkaStreams.this::closeToError, +streamsUncaughtExceptionHandler +); +threads.add(streamThread); +threadState.put(streamThread.getId(), streamThread.state()); +storeProviders.add(new StreamThreadStateStoreProvider(streamThread)); +return streamThread; +} + +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +synchronized (stateLock) { +if (isRunningOrRebalancing()) { +final int threadIdx = getNextThreadIndex(); +final long cacheSizePerThread = getCacheSizePerThread(threads.size() + 1); +resizeThreadCache(threads.size() + 1); Review comment: I think it would be cleaner to pass `cacheSizePerThread` to `resizeThreadCache()` instead of the number of stream threads. We would then just call `getCacheSizePerThread()` once instead of once in `addStreamThread()` and once in `resizeThreadCache()`. We would also just need to compute `threads.size() + 1` once. ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,11 +885,77 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +private StreamThread makeThread(final long cacheSizePerThread, final int threadIdx) { Review comment: IMO, `createStreamThread()` would describe the behavior better. ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,11 +904,72 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r527145996 ## File path: streams/src/test/java/org/apache/kafka/streams/KafkaStreamsTest.java ## @@ -588,6 +592,29 @@ public void testCloseIsIdempotent() { closeCount, MockMetricsReporter.CLOSE_COUNT.get()); } +@Test +public void testAddThread() { +props.put(StreamsConfig.NUM_STREAM_THREADS_CONFIG, 1); +final KafkaStreams streams = new KafkaStreams(getBuilderWithSource().build(), props, supplier, time); +streams.start(); +final int oldSize = streams.threads.size(); +try { +TestUtils.waitForCondition(() -> streams.state() == KafkaStreams.State.RUNNING, 15L, "wait until running"); +} catch (final InterruptedException e) { +e.printStackTrace(); +} Review comment: You should not use `try-catch` here but just add `throws InterruptedException` to the method signature. ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,11 +904,72 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +synchronized (stateLock) { Review comment: Why do we need to synchronize the whole method on `stateLock`? ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,11 +904,72 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +synchronized (stateLock) { +if (state == State.RUNNING || state == State.REBALANCING) { Review comment: Could we also use `isRunningOrRebalancing()` here? ## File path: streams/src/test/java/org/apache/kafka/streams/KafkaStreamsTest.java ## @@ -588,6 +592,29 @@ public void testCloseIsIdempotent() { closeCount, MockMetricsReporter.CLOSE_COUNT.get()); } +@Test +public void testAddThread() { Review comment: I would prefer to use `shouldAddThread()` as name although the pattern is different for the other test methods. ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,11 +904,72 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +/** + * Adds and starts a stream
[GitHub] [kafka] cadonna commented on a change in pull request #9615: KAFKA-10500: Add thread option
cadonna commented on a change in pull request #9615: URL: https://github.com/apache/kafka/pull/9615#discussion_r527055689 ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,11 +904,57 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +synchronized (stateLock) { +if (state == State.RUNNING || state == State.REBALANCING) { +final int threadIdx = threads.size() + 1; +final long cacheSizePerThread = getCacheSizePerThread(threadIdx); Review comment: This should be: ```suggestion final long cacheSizePerThread = getCacheSizePerThread(threads.size() + 1); ``` ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,11 +904,57 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +synchronized (stateLock) { +if (state == State.RUNNING || state == State.REBALANCING) { +final int threadIdx = threads.size() + 1; Review comment: Assume the following thread list [t2, t3, t4], `threadIdx` would be 4, which is already there. You should keep the currently used `threadIdx`s and check those to decide on the next `threadIdx`. ## File path: streams/src/main/java/org/apache/kafka/streams/KafkaStreams.java ## @@ -894,11 +904,57 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder, queryableStoreProvider = new QueryableStoreProvider(storeProviders, globalStateStoreProvider); stateDirCleaner = setupStateDirCleaner(); -oldHandler = false; maybeWarnAboutCodeInRocksDBConfigSetter(log, config); rocksDBMetricsRecordingService = maybeCreateRocksDBMetricsRecordingService(clientId, config); } +/** + * Adds and starts a stream thread in addition to the stream threads that are already running in this + * Kafka Streams client. + * + * Since the number of stream threads increases, the sizes of the caches in the new stream thread + * and the existing stream threads are adapted so that the sum of the cache sizes over all stream + * threads does not exceed the total cache size specified in configuration + * {@code cache.max.bytes.buffering}. + * + * Stream threads can only be added if this Kafka Streams client is in state RUNNING or REBALANCING. + * + * @return name of the added stream thread or empty if a new stream thread could not be added + */ +public Optional addStreamThread() { +synchronized (stateLock) { +if (state == State.RUNNING || state ==