-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-9727: cleanup the state store for standby task dirty close and check null for changelogs #8307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
KAFKA-9727: cleanup the state store for standby task dirty close and check null for changelogs #8307
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -798,10 +798,16 @@ public void remove(final Collection<TopicPartition> revokedChangelogs) { | |
|
|
||
| for (final TopicPartition partition : revokedChangelogs) { | ||
| final ChangelogMetadata changelogMetadata = changelogs.remove(partition); | ||
| if (changelogMetadata.state() != ChangelogState.REGISTERED) { | ||
| revokedInitializedChangelogs.add(partition); | ||
| if (changelogMetadata != null) { | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fix 3
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch, this is indeed possible. |
||
| if (changelogMetadata.state() != ChangelogState.REGISTERED) { | ||
| revokedInitializedChangelogs.add(partition); | ||
| } | ||
| changelogMetadata.clear(); | ||
| } else { | ||
| log.debug("Changelog partition {} could not be found, " + | ||
| "it could be already cleaned up during the handling" + | ||
| "of task corruption and never restore again", partition); | ||
| } | ||
| changelogMetadata.clear(); | ||
| } | ||
|
|
||
| removeChangelogsFromRestoreConsumer(revokedInitializedChangelogs); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,7 +78,7 @@ public class StreamTask extends AbstractTask implements ProcessorNodePunctuator, | |
| // we want to abstract eos logic out of StreamTask, however | ||
| // there's still an optimization that requires this info to be | ||
| // leaked into this class, which is to checkpoint after committing if EOS is not enabled. | ||
| private final boolean eosDisabled; | ||
| private final boolean eosEnabled; | ||
|
|
||
| private final long maxTaskIdleMs; | ||
| private final int maxBufferedSize; | ||
|
|
@@ -120,7 +120,7 @@ public StreamTask(final TaskId id, | |
|
|
||
| this.time = time; | ||
| this.recordCollector = recordCollector; | ||
| eosDisabled = !StreamsConfig.EXACTLY_ONCE.equals(config.getString(StreamsConfig.PROCESSING_GUARANTEE_CONFIG)); | ||
| eosEnabled = StreamsConfig.EXACTLY_ONCE.equals(config.getString(StreamsConfig.PROCESSING_GUARANTEE_CONFIG)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part will have some conflicts with @mjsax 's PR, just a note.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, one of us probably needs to rebase |
||
|
|
||
| final String threadId = Thread.currentThread().getName(); | ||
| closeTaskSensor = ThreadMetrics.closeTaskSensor(threadId, streamsMetrics); | ||
|
|
@@ -326,7 +326,7 @@ public void postCommit() { | |
| commitNeeded = false; | ||
| commitRequested = false; | ||
|
|
||
| if (eosDisabled) { | ||
| if (!eosEnabled) { | ||
| stateMgr.checkpoint(checkpointableOffsets()); | ||
| } | ||
|
|
||
|
|
@@ -487,7 +487,7 @@ private void close(final boolean clean, | |
| case SUSPENDED: | ||
| // if EOS is enabled, we wipe out the whole state store for unclean close | ||
| // since they are invalid to use anymore | ||
| final boolean wipeStateStore = !clean && !eosDisabled; | ||
| final boolean wipeStateStore = !clean && eosEnabled; | ||
|
|
||
| // first close state manager (which is idempotent) then close the record collector (which could throw), | ||
| // if the latter throws and we re-close dirty which would close the state manager again. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| /* | ||
| * 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.clients.consumer.ConsumerConfig; | ||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.apache.kafka.common.serialization.IntegerSerializer; | ||
| import org.apache.kafka.common.serialization.Serdes; | ||
| import org.apache.kafka.common.utils.MockTime; | ||
| 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.streams.kstream.Consumed; | ||
| import org.apache.kafka.streams.processor.TaskId; | ||
| import org.apache.kafka.streams.processor.internals.StateDirectory; | ||
| import org.apache.kafka.streams.state.internals.OffsetCheckpoint; | ||
| import org.apache.kafka.test.TestUtils; | ||
| import org.junit.Before; | ||
| import org.junit.ClassRule; | ||
| import org.junit.Test; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.time.Duration; | ||
| import java.util.Collections; | ||
| import java.util.Properties; | ||
| import java.util.concurrent.CountDownLatch; | ||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import static org.apache.kafka.test.TestUtils.waitForCondition; | ||
| import static org.junit.Assert.assertTrue; | ||
|
|
||
| /** | ||
| * An integration test to verify the conversion of a dirty-closed EOS | ||
| * task towards a standby task is safe across restarts of the application. | ||
| */ | ||
| public class StandbyTaskEOSIntegrationTest { | ||
|
|
||
| private final String inputTopic = "input"; | ||
|
|
||
| @ClassRule | ||
| public static final EmbeddedKafkaCluster CLUSTER = new EmbeddedKafkaCluster(3); | ||
|
|
||
| @Before | ||
| public void createTopics() throws Exception { | ||
| CLUSTER.createTopic(inputTopic, 1, 3); | ||
| } | ||
|
|
||
| @Test | ||
| public void surviveWithOneTaskAsStandby() throws ExecutionException, InterruptedException, IOException { | ||
| IntegrationTestUtils.produceKeyValuesSynchronouslyWithTimestamp( | ||
| inputTopic, | ||
| Collections.singletonList( | ||
| new KeyValue<>(0, 0)), | ||
| TestUtils.producerConfig( | ||
| CLUSTER.bootstrapServers(), | ||
| IntegerSerializer.class, | ||
| IntegerSerializer.class, | ||
| new Properties()), | ||
| 10L); | ||
|
|
||
| final String appId = "eos-test-app"; | ||
| final String stateDirPath = TestUtils.tempDirectory(appId).getPath(); | ||
|
|
||
| final CountDownLatch instanceLatch = new CountDownLatch(1); | ||
|
|
||
| final String stateDirPathOne = stateDirPath + "/" + appId + "-1/"; | ||
| final KafkaStreams streamInstanceOne = | ||
| buildStreamWithDirtyStateDir(appId, stateDirPathOne, instanceLatch); | ||
|
|
||
| final String stateDirPathTwo = stateDirPath + "/" + appId + "-2/"; | ||
| final KafkaStreams streamInstanceTwo = | ||
| buildStreamWithDirtyStateDir(appId, stateDirPathTwo, instanceLatch); | ||
|
|
||
| streamInstanceOne.start(); | ||
|
|
||
| streamInstanceTwo.start(); | ||
|
|
||
| // Wait for the record to be processed | ||
| assertTrue(instanceLatch.await(15, TimeUnit.SECONDS)); | ||
|
|
||
| waitForCondition(() -> streamInstanceOne.state().equals(KafkaStreams.State.RUNNING), | ||
| "Stream instance one should be up and running by now"); | ||
| waitForCondition(() -> streamInstanceTwo.state().equals(KafkaStreams.State.RUNNING), | ||
| "Stream instance one should be up and running by now"); | ||
|
|
||
| streamInstanceOne.close(Duration.ofSeconds(30)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For my own education: before the fix, this integration test will fail when instance-2 is started?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, actually either instance-1 or instance-2 would fail, depending on which box gets standby assignment. There would be a IllegalState + NPE exception sequence happening. |
||
| streamInstanceTwo.close(Duration.ofSeconds(30)); | ||
|
|
||
| } | ||
|
|
||
| private KafkaStreams buildStreamWithDirtyStateDir(final String appId, | ||
| final String stateDirPath, | ||
| final CountDownLatch recordProcessLatch) throws IOException { | ||
|
|
||
| final StreamsBuilder builder = new StreamsBuilder(); | ||
| final TaskId taskId = new TaskId(0, 0); | ||
|
|
||
| final Properties props = props(appId, stateDirPath); | ||
|
|
||
| final StateDirectory stateDirectory = new StateDirectory( | ||
| new StreamsConfig(props), new MockTime(), true); | ||
|
|
||
| new OffsetCheckpoint(new File(stateDirectory.directoryForTask(taskId), ".checkpoint")) | ||
| .write(Collections.singletonMap(new TopicPartition("unknown-topic", 0), 5L)); | ||
|
|
||
| assertTrue(new File(stateDirectory.directoryForTask(taskId), | ||
| "rocksdb/KSTREAM-AGGREGATE-STATE-STORE-0000000001").mkdirs()); | ||
|
|
||
| builder.stream(inputTopic, | ||
| Consumed.with(Serdes.Integer(), Serdes.Integer())) | ||
| .groupByKey() | ||
| .count() | ||
| .toStream() | ||
| .peek((key, value) -> recordProcessLatch.countDown()); | ||
|
|
||
| return new KafkaStreams(builder.build(), props); | ||
| } | ||
|
|
||
| private Properties props(final String appId, final String stateDirPath) { | ||
| final Properties streamsConfiguration = new Properties(); | ||
| streamsConfiguration.put(StreamsConfig.APPLICATION_ID_CONFIG, appId); | ||
| streamsConfiguration.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, CLUSTER.bootstrapServers()); | ||
| streamsConfiguration.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 0); | ||
| streamsConfiguration.put(StreamsConfig.STATE_DIR_CONFIG, stateDirPath); | ||
| streamsConfiguration.put(StreamsConfig.NUM_STANDBY_REPLICAS_CONFIG, 1); | ||
| streamsConfiguration.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE); | ||
| streamsConfiguration.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.Integer().getClass()); | ||
| streamsConfiguration.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.Integer().getClass()); | ||
| streamsConfiguration.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 1000); | ||
| streamsConfiguration.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); | ||
|
|
||
| return streamsConfiguration; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for this PR: we can clean up the task-manager code to not pass in the checkpoint at all.