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 433e99a218d KAFKA-20760: Parameterise PositionRestartIntegrationTest
over transactional mode (#22750)
433e99a218d is described below
commit 433e99a218ddc00f13e008ed6f9a3f166a8842e9
Author: Nick Telford <[email protected]>
AuthorDate: Fri Jul 3 22:08:16 2026 +0100
KAFKA-20760: Parameterise PositionRestartIntegrationTest over transactional
mode (#22750)
Extends `PositionRestartIntegrationTest` to run every store type and
configuration with transactional state stores both enabled and disabled.
This exercises interactive-query position consistency and, crucially,
position recovery on restart for transactional stores — the end-to-end
coverage for the series of transactional state-store position fixes
(KAFKA-20760).
Also adds unit tests for `InMemoryWindowStore` and
`InMemorySessionStore`, mirroring the existing `InMemoryKeyValueStore`
test: each verifies that a transactional store exposes its staged
(uncommitted) position via a READ_UNCOMMITTED query and via
`getPosition()` before commit, and its committed position after commit.
Reviewers: Bill Bejeck <[email protected]>
Co-authored-by: Bill Bejeck <[email protected]>
---
.../PositionRestartIntegrationTest.java | 28 +++++++------
.../state/internals/InMemorySessionStoreTest.java | 48 ++++++++++++++++++++++
.../state/internals/InMemoryWindowStoreTest.java | 40 ++++++++++++++++++
3 files changed, 104 insertions(+), 12 deletions(-)
diff --git
a/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/PositionRestartIntegrationTest.java
b/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/PositionRestartIntegrationTest.java
index 5217f133d20..588cdeba05e 100644
---
a/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/PositionRestartIntegrationTest.java
+++
b/streams/integration-tests/src/test/java/org/apache/kafka/streams/integration/PositionRestartIntegrationTest.java
@@ -255,12 +255,14 @@ public class PositionRestartIntegrationTest {
final List<Arguments> values = new ArrayList<>();
for (final boolean cacheEnabled : Arrays.asList(true, false)) {
for (final boolean logEnabled : Arrays.asList(true, false)) {
- for (final StoresToTest toTest : StoresToTest.values()) {
- // We don't need to test if non-persistent stores without
logging
- // survive restarts, since those are by definition not
durable.
- if (logEnabled || toTest.supplier().get().persistent()) {
- for (final String kind : Arrays.asList("DSL", "PAPI"))
{
- values.add(Arguments.of(cacheEnabled, logEnabled,
toTest, kind));
+ for (final boolean transactional : Arrays.asList(true, false))
{
+ for (final StoresToTest toTest : StoresToTest.values()) {
+ // We don't need to test if non-persistent stores
without logging
+ // survive restarts, since those are by definition not
durable.
+ if (logEnabled ||
toTest.supplier().get().persistent()) {
+ for (final String kind : Arrays.asList("DSL",
"PAPI")) {
+ values.add(Arguments.of(cacheEnabled,
logEnabled, transactional, toTest, kind));
+ }
}
}
}
@@ -361,8 +363,8 @@ public class PositionRestartIntegrationTest {
@ParameterizedTest
@MethodSource("data")
- public void verifyStore(final boolean cache, final boolean log, final
StoresToTest storeToTest, final String kind) {
- final Properties streamsConfig = streamsConfiguration(cache, log,
storeToTest.name(), kind);
+ public void verifyStore(final boolean cache, final boolean log, final
boolean transactional, final StoresToTest storeToTest, final String kind) {
+ final Properties streamsConfig = streamsConfiguration(cache, log,
transactional, storeToTest.name(), kind);
final StreamsBuilder streamsBuilder = getStreamBuilder(cache, log,
storeToTest, kind);
kafkaStreams = IntegrationTestUtils.getStartedStreams(streamsConfig,
streamsBuilder, true);
@@ -647,11 +649,12 @@ public class PositionRestartIntegrationTest {
}
private static Properties streamsConfiguration(final boolean cache,
- final boolean log,
- final String supplier,
- final String kind) {
+ final boolean log,
+ final boolean transactional,
+ final String supplier,
+ final String kind) {
final String safeTestName =
- PositionRestartIntegrationTest.class.getName() + "-" + cache + "-"
+ log + "-"
+ PositionRestartIntegrationTest.class.getName() + "-" + cache + "-"
+ log + "-" + transactional + "-"
+ supplier + "-" + kind;
final Properties config = new Properties();
config.put(StreamsConfig.TOPOLOGY_OPTIMIZATION_CONFIG,
StreamsConfig.OPTIMIZE);
@@ -668,6 +671,7 @@ public class PositionRestartIntegrationTest {
config.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 100L);
config.put(StreamsConfig.NUM_STREAM_THREADS_CONFIG, 1);
config.put(InternalConfig.IQ_CONSISTENCY_OFFSET_VECTOR_ENABLED, true);
+ config.put(StreamsConfig.TRANSACTIONAL_STATE_STORES_CONFIG,
transactional);
return config;
}
}
\ No newline at end of file
diff --git
a/streams/src/test/java/org/apache/kafka/streams/state/internals/InMemorySessionStoreTest.java
b/streams/src/test/java/org/apache/kafka/streams/state/internals/InMemorySessionStoreTest.java
index cfcf156253c..30543189977 100644
---
a/streams/src/test/java/org/apache/kafka/streams/state/internals/InMemorySessionStoreTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/state/internals/InMemorySessionStoreTest.java
@@ -17,11 +17,18 @@
package org.apache.kafka.streams.state.internals;
import org.apache.kafka.common.IsolationLevel;
+import org.apache.kafka.common.header.internals.RecordHeaders;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.kstream.Windowed;
import org.apache.kafka.streams.kstream.internals.SessionWindow;
+import org.apache.kafka.streams.processor.internals.ProcessorRecordContext;
+import org.apache.kafka.streams.query.Position;
+import org.apache.kafka.streams.query.PositionBound;
+import org.apache.kafka.streams.query.QueryConfig;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.WindowRangeQuery;
import org.apache.kafka.streams.state.KeyValueIterator;
import org.apache.kafka.streams.state.ReadOnlySessionStore;
import org.apache.kafka.test.InternalMockProcessorContext;
@@ -37,6 +44,8 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
+import static org.apache.kafka.common.utils.Utils.mkEntry;
+import static org.apache.kafka.common.utils.Utils.mkMap;
import static org.apache.kafka.test.StreamsTestUtils.valuesToSet;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -254,6 +263,45 @@ public class InMemorySessionStoreTest extends
AbstractSessionBytesStoreTest {
}
}
+ @Test
+ public void shouldReportUncommittedPositionForTransactionalStore() {
+ final Properties props = StreamsTestUtils.getStreamsConfig();
+ props.setProperty(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
+ props.setProperty(StreamsConfig.TRANSACTIONAL_STATE_STORES_CONFIG,
"true");
+ final InternalMockProcessorContext<Bytes, byte[]> ctx = new
InternalMockProcessorContext<>(
+ TestUtils.tempDirectory(),
+ new Serdes.BytesSerde(),
+ new Serdes.ByteArraySerde(),
+ new StreamsConfig(props)
+ );
+ final InMemorySessionStore store = new InMemorySessionStore(
+ "txn-pos-session-store", RETENTION_PERIOD, "scope");
+ store.init(ctx, store);
+ try {
+ ctx.setRecordContext(new ProcessorRecordContext(0, 1, 0, "topic",
new RecordHeaders()));
+ store.put(new Windowed<>(Bytes.wrap("k".getBytes()), new
SessionWindow(0, 10)), "v".getBytes());
+
+ final Position expected = Position.fromMap(mkMap(mkEntry("topic",
mkMap(mkEntry(0, 1L)))));
+
+ // READ_UNCOMMITTED query should expose the staged position before
commit
+ final QueryResult<?> uncommitted = store.query(
+ WindowRangeQuery.withKey(Bytes.wrap("k".getBytes())),
+ PositionBound.unbounded(),
+ new QueryConfig(false));
+ assertEquals(expected, uncommitted.getPosition(),
"READ_UNCOMMITTED query position");
+
+ // getPosition() reports the uncommitted (committed + staged)
position, mirroring
+ // RocksDBStore, so the changelog consistency vector reflects the
staged write.
+ assertEquals(expected, store.getPosition(), "getPosition before
commit (uncommitted)");
+
+ // after commit, committed position populated
+ store.commit(Map.of());
+ assertEquals(expected, store.getPosition(), "getPosition after
commit");
+ } finally {
+ store.close();
+ }
+ }
+
private InMemorySessionStore openTransactionalSessionStore() {
final Properties props = StreamsTestUtils.getStreamsConfig();
props.setProperty(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
diff --git
a/streams/src/test/java/org/apache/kafka/streams/state/internals/InMemoryWindowStoreTest.java
b/streams/src/test/java/org/apache/kafka/streams/state/internals/InMemoryWindowStoreTest.java
index be6981ca9f5..72af0085de0 100644
---
a/streams/src/test/java/org/apache/kafka/streams/state/internals/InMemoryWindowStoreTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/state/internals/InMemoryWindowStoreTest.java
@@ -408,6 +408,46 @@ public class InMemoryWindowStoreTest extends
AbstractWindowBytesStoreTest {
}
}
+ @Test
+ public void shouldReportUncommittedPositionForTransactionalStore() {
+ final Properties props = StreamsTestUtils.getStreamsConfig();
+ props.setProperty(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
+ props.setProperty(StreamsConfig.TRANSACTIONAL_STATE_STORES_CONFIG,
"true");
+ final InternalMockProcessorContext<Bytes, byte[]> ctx = new
InternalMockProcessorContext<>(
+ TestUtils.tempDirectory(),
+ new Serdes.BytesSerde(),
+ new Serdes.ByteArraySerde(),
+ new StreamsConfig(props)
+ );
+ final InMemoryWindowStore store = new InMemoryWindowStore(
+ "txn-pos-window-store", RETENTION_PERIOD, WINDOW_SIZE, false,
"scope");
+ store.init(ctx, store);
+ try {
+ ctx.setRecordContext(new ProcessorRecordContext(0, 1, 0, "topic",
new RecordHeaders()));
+ store.put(Bytes.wrap("k".getBytes()), "v".getBytes(), 0L);
+
+ final Position expected = Position.fromMap(mkMap(mkEntry("topic",
mkMap(mkEntry(0, 1L)))));
+
+ // READ_UNCOMMITTED query should expose the staged position before
commit
+ final QueryResult<?> uncommitted = store.query(
+ WindowKeyQuery.withKeyAndWindowStartRange(
+ Bytes.wrap("k".getBytes()), Instant.ofEpochMilli(0),
Instant.ofEpochMilli(WINDOW_SIZE)),
+ PositionBound.unbounded(),
+ new QueryConfig(false));
+ assertEquals(expected, uncommitted.getPosition(),
"READ_UNCOMMITTED query position");
+
+ // getPosition() reports the uncommitted (committed + staged)
position, mirroring
+ // RocksDBStore, so the changelog consistency vector reflects the
staged write.
+ assertEquals(expected, store.getPosition(), "getPosition before
commit (uncommitted)");
+
+ // after commit, committed position populated
+ store.commit(java.util.Map.of());
+ assertEquals(expected, store.getPosition(), "getPosition after
commit");
+ } finally {
+ store.close();
+ }
+ }
+
private InMemoryWindowStore openTransactionalWindowStore() {
final Properties props = StreamsTestUtils.getStreamsConfig();
props.setProperty(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);