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 5da1831e1f6 KAFKA-20504: Update state wipe tests (#22959)
5da1831e1f6 is described below
commit 5da1831e1f6b27018550302a52b9f59bc9403ce9
Author: Nick Telford <[email protected]>
AuthorDate: Tue Aug 4 15:45:06 2026 +0100
KAFKA-20504: Update state wipe tests (#22959)
Existing tests verifying state-wipe behaviour on dirty EOS close need to
be scoped to the non-transactional case, since transactional state
stores never let uncommitted writes reach the base store and so should
not be wiped on error.
Most of this was already covered by earlier KIP-892 work —
`StateManagerUtilTest`, `StandbyTaskTest`, `ProcessorStateManagerTest`,
and `StandbyTaskEOSIntegrationTest` already exercise both the
transactional and non-transactional paths correctly. Two gaps remained:
- `StreamTaskTest` had a non-transactional active-task wipe test but no
transactional counterpart (unlike its `StandbyTaskTest` sibling), so
this adds
`shouldNotWipeStateDirectoryWhenCloseDirtyAndEosEnabledWithTransactionalStateStores`
mirroring the existing pattern.
- `EOSUncleanShutdownIntegrationTest` had no transactional coverage at
all, and mutated a shared static `Properties` field in place (a footgun
once a second test method exists). This refactors both tests to build
their own `Properties` copy, makes the existing test explicitly
non-transactional, and adds
`shouldNotWipeStateStoreOnUncleanShutdownWhenTransactional` asserting
the state directory survives an unclean shutdown when transactional
stores are enabled.
Reviewers: Bill Bejeck <[email protected]>
---
.../EOSUncleanShutdownIntegrationTest.java | 87 +++++++++++++++++++++-
.../processor/internals/StreamTaskTest.java | 31 ++++++++
2 files changed, 115 insertions(+), 3 deletions(-)
diff --git
a/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/EOSUncleanShutdownIntegrationTest.java
b/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/EOSUncleanShutdownIntegrationTest.java
index 32f5dd69b75..e4d1c0c702d 100644
---
a/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/EOSUncleanShutdownIntegrationTest.java
+++
b/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/EOSUncleanShutdownIntegrationTest.java
@@ -86,11 +86,16 @@ public class EOSUncleanShutdownIntegrationTest {
private static final int RECORD_TOTAL = 3;
+ // Exercises the non-transactional store path: without transactional state
stores, an unclean
+ // shutdown under EOS may leave uncommitted data on disk, so the state
directory is wiped.
@Test
public void shouldWorkWithUncleanShutdownWipeOutStateStore() throws
InterruptedException {
final String appId = "shouldWorkWithUncleanShutdownWipeOutStateStore";
- STREAMS_CONFIG.put(StreamsConfig.APPLICATION_ID_CONFIG, appId);
- STREAMS_CONFIG.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
+ final Properties config = new Properties();
+ config.putAll(STREAMS_CONFIG);
+ config.put(StreamsConfig.APPLICATION_ID_CONFIG, appId);
+ config.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
+ config.put(StreamsConfig.TRANSACTIONAL_STATE_STORES_CONFIG, "false");
final String input = "input-topic";
cleanStateBeforeTest(CLUSTER, input);
@@ -120,7 +125,7 @@ public class EOSUncleanShutdownIntegrationTest {
mkEntry(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
((Serializer<String>) STRING_SERIALIZER).getClass().getName()),
mkEntry(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
CLUSTER.bootstrapServers())
));
- final KafkaStreams driver = new KafkaStreams(builder.build(),
STREAMS_CONFIG);
+ final KafkaStreams driver = new KafkaStreams(builder.build(), config);
driver.cleanUp();
driver.start();
@@ -163,4 +168,80 @@ public class EOSUncleanShutdownIntegrationTest {
quietlyCleanStateAfterTest(CLUSTER, driver);
}
}
+
+ // With transactional state stores enabled, uncommitted writes never reach
the base store, so
+ // an unclean shutdown (that is not due to store corruption) must NOT wipe
the state directory.
+ @Test
+ public void shouldNotWipeStateStoreOnUncleanShutdownWhenTransactional()
throws InterruptedException {
+ final String appId =
"shouldNotWipeStateStoreOnUncleanShutdownWhenTransactional";
+ final Properties config = new Properties();
+ config.putAll(STREAMS_CONFIG);
+ config.put(StreamsConfig.APPLICATION_ID_CONFIG, appId);
+ config.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
+ config.put(StreamsConfig.TRANSACTIONAL_STATE_STORES_CONFIG, "true");
+
+ final String input = "input-topic-transactional";
+ cleanStateBeforeTest(CLUSTER, input);
+
+ final StreamsBuilder builder = new StreamsBuilder();
+
+ final KStream<String, String> inputStream = builder.stream(input);
+
+ final AtomicInteger recordCount = new AtomicInteger(0);
+
+ final KTable<String, String> valueCounts = inputStream
+ .groupByKey()
+ .aggregate(
+ () -> "()",
+ (key, value, aggregate) -> aggregate + ",(" + key + ": " +
value + ")",
+ Materialized.as("aggregated_value"));
+
+ valueCounts.toStream().peek((key, value) -> {
+ if (recordCount.incrementAndGet() >= RECORD_TOTAL) {
+ throw new IllegalStateException("Crash on the " + RECORD_TOTAL
+ " record");
+ }
+ });
+
+ final Properties producerConfig = mkProperties(mkMap(
+ mkEntry(ProducerConfig.CLIENT_ID_CONFIG, "anything"),
+ mkEntry(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
((Serializer<String>) STRING_SERIALIZER).getClass().getName()),
+ mkEntry(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
((Serializer<String>) STRING_SERIALIZER).getClass().getName()),
+ mkEntry(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
CLUSTER.bootstrapServers())
+ ));
+ final KafkaStreams driver = new KafkaStreams(builder.build(), config);
+ driver.cleanUp();
+ driver.start();
+
+ TestUtils.waitForCondition(() -> driver.state().equals(State.RUNNING),
+ () -> "Expected RUNNING state but driver is on " +
driver.state());
+
+ // Task's StateDir
+ final File taskStateDir = new File(String.join("/",
TEST_FOLDER.getPath(), appId, "0_0"));
+
+ try {
+ IntegrationTestUtils.produceSynchronously(producerConfig, false,
input, Optional.empty(),
+ singletonList(new KeyValueTimestamp<>("k1", "v1", 0L)));
+
+ // wait until the first request is processed and some files are
created in it
+ TestUtils.waitForCondition(() -> taskStateDir.exists() &&
taskStateDir.isDirectory() && taskStateDir.list().length > 0,
+ "Failed awaiting CreateTopics first request failure");
+ IntegrationTestUtils.produceSynchronously(producerConfig, false,
input, Optional.empty(),
+ asList(new KeyValueTimestamp<>("k2", "v2", 1L),
+ new KeyValueTimestamp<>("k3", "v3", 2L)));
+
+ TestUtils.waitForCondition(() -> recordCount.get() == RECORD_TOTAL,
+ () -> "Expected " + RECORD_TOTAL + " records processed but
only got " + recordCount.get());
+ } catch (final Exception e) {
+ e.printStackTrace();
+ } finally {
+ TestUtils.waitForCondition(() ->
driver.state().equals(State.ERROR),
+ () -> "Expected ERROR state but driver is on " +
driver.state());
+
+ driver.close();
+
+ assertTrue(taskStateDir.exists());
+
+ quietlyCleanStateAfterTest(CLUSTER, driver);
+ }
+ }
}
diff --git
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamTaskTest.java
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamTaskTest.java
index 48639fad16a..eeffde86b29 100644
---
a/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamTaskTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/processor/internals/StreamTaskTest.java
@@ -397,6 +397,8 @@ public class StreamTaskTest {
verify(stateDirectory, never()).lock(any());
}
+ // Covers the non-transactional case: without transactional state stores,
a dirty close under
+ // EOS may have left uncommitted data on disk, so the state directory must
be wiped.
@Test
public void
shouldAttemptToDeleteStateDirectoryWhenCloseDirtyAndEosEnabled() {
when(stateManager.taskId()).thenReturn(taskId);
@@ -426,6 +428,35 @@ public class StreamTaskTest {
inOrder.verify(stateDirectory).unlock(taskId);
}
+ @Test
+ public void
shouldNotWipeStateDirectoryWhenCloseDirtyAndEosEnabledWithTransactionalStateStores()
{
+ when(stateManager.taskId()).thenReturn(taskId);
+ when(stateManager.taskType()).thenReturn(TaskType.ACTIVE);
+ when(stateManager.hasCorruptedStores()).thenReturn(false);
+ // Clean up state directory created as part of setup
+ stateDirectory.close();
+ stateDirectory = mock(StateDirectory.class);
+
+ when(stateDirectory.lock(taskId)).thenReturn(true);
+
+ final StreamsConfig config = createConfig(
+ StreamsConfig.EXACTLY_ONCE_V2,
+ "100",
+ LogAndFailExceptionHandler.class,
+ LogAndFailProcessingExceptionHandler.class,
+ FailOnInvalidTimestamp.class,
+ true);
+
+ task = createStatefulTask(config, true, stateManager);
+ task.suspend();
+ task.closeDirty();
+ task = null;
+
+ // With transactional state stores, the state dir should NOT be wiped
on dirty close
+ // unless stores are specifically marked as corrupted.
+ verify(stateManager, never()).baseDir();
+ }
+
@Test
public void shouldResetOffsetsToLastCommittedForSpecifiedPartitions() {
when(stateManager.taskId()).thenReturn(taskId);