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 04bfe7dbb0f KAFKA-20497: Add TimeOrderedCachingWindowStore#readOnly
(#22315)
04bfe7dbb0f is described below
commit 04bfe7dbb0f8760022afe51647a2e7dd7f434288
Author: Nick Telford <[email protected]>
AuthorDate: Mon Jun 15 18:10:35 2026 +0100
KAFKA-20497: Add TimeOrderedCachingWindowStore#readOnly (#22315)
The cache holds uncommitted writes that must not be visible under
READ_COMMITTED, so that isolation level bypasses the cache entirely and
delegates straight to the inner store's readOnly view. READ_UNCOMMITTED
requires a merged view through a ReadOnlyView. The ReadOnlyView
delegates to the existing fetchInternal/fetchKeyRange/fetchAllInternal
helpers and converts Instant arguments to epoch-milliseconds before
calling them; the cache key schema operates in longs throughout, so
converting early avoids repeated Instant-to-long conversions in iterator
hot paths.
KAFKA-20497
Reviewers: Bill Bejeck <[email protected]>
---
.../internals/TimeOrderedCachingWindowStore.java | 234 +++++++++++++--------
...imeOrderedCachingPersistentWindowStoreTest.java | 175 ++++++++++++++-
.../internals/TimeOrderedWindowStoreTest.java | 3 +-
3 files changed, 328 insertions(+), 84 deletions(-)
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/TimeOrderedCachingWindowStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/TimeOrderedCachingWindowStore.java
index 4e0fd29f09d..b11920c8536 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/TimeOrderedCachingWindowStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/TimeOrderedCachingWindowStore.java
@@ -16,12 +16,14 @@
*/
package org.apache.kafka.streams.state.internals;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.TopicPartition;
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.KeyValue;
import org.apache.kafka.streams.StreamsConfig;
+import org.apache.kafka.streams.internals.ApiUtils;
import org.apache.kafka.streams.kstream.Windowed;
import org.apache.kafka.streams.kstream.internals.Change;
import org.apache.kafka.streams.processor.StateStore;
@@ -32,6 +34,7 @@ import
org.apache.kafka.streams.processor.internals.ProcessorRecordContext;
import org.apache.kafka.streams.processor.internals.ProcessorStateManager;
import org.apache.kafka.streams.processor.internals.RecordQueue;
import org.apache.kafka.streams.state.KeyValueIterator;
+import org.apache.kafka.streams.state.ReadOnlyWindowStore;
import org.apache.kafka.streams.state.StateSerdes;
import org.apache.kafka.streams.state.WindowStore;
import org.apache.kafka.streams.state.WindowStoreIterator;
@@ -45,11 +48,13 @@ import
org.apache.kafka.streams.state.internals.ThreadCache.DirtyEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.time.Instant;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
@@ -302,59 +307,48 @@ public class TimeOrderedCachingWindowStore
@Override
public byte[] fetch(final Bytes key,
final long timestamp) {
+ return fetchPointInternal(wrapped(), key, timestamp);
+ }
+
+ private byte[] fetchPointInternal(final ReadOnlyWindowStore<Bytes, byte[]>
store,
+ final Bytes key,
+ final long timestamp) {
validateStoreOpen();
if (internalContext.cache() == null) {
- return wrapped().fetch(key, timestamp);
+ return store.fetch(key, timestamp);
}
-
final Bytes baseBytesKey =
TimeFirstWindowKeySchema.toStoreKeyBinary(key, timestamp, 0);
final Bytes cacheKey = baseKeyCacheFunction.cacheKey(baseBytesKey);
-
final LRUCacheEntry entry = internalContext.cache().get(cacheName,
cacheKey);
- if (entry == null) {
- return wrapped().fetch(key, timestamp);
- } else {
- return entry.value();
- }
+ return entry == null ? store.fetch(key, timestamp) : entry.value();
}
@Override
- public synchronized WindowStoreIterator<byte[]> fetch(final Bytes key,
- final long timeFrom,
- final long timeTo) {
- // since this function may not access the underlying inner store, we
need to validate
- // if store is open outside as well.
- validateStoreOpen();
-
- final WindowStoreIterator<byte[]> underlyingIterator =
wrapped().fetch(key, timeFrom, timeTo);
- if (internalContext.cache() == null) {
- return underlyingIterator;
- }
-
- return fetchInternal(underlyingIterator, key, timeFrom, timeTo, true);
+ public WindowStoreIterator<byte[]> fetch(final Bytes key,
+ final long timeFrom,
+ final long timeTo) {
+ return fetchInternal(wrapped(), key, timeFrom, timeTo, true);
}
@Override
- public synchronized WindowStoreIterator<byte[]> backwardFetch(final Bytes
key,
- final long
timeFrom,
- final long
timeTo) {
- // since this function may not access the underlying inner store, we
need to validate
- // if store is open outside as well.
- validateStoreOpen();
+ public WindowStoreIterator<byte[]> backwardFetch(final Bytes key,
+ final long timeFrom,
+ final long timeTo) {
+ return fetchInternal(wrapped(), key, timeFrom, timeTo, false);
+ }
- final WindowStoreIterator<byte[]> underlyingIterator =
wrapped().backwardFetch(key, timeFrom, timeTo);
+ private synchronized WindowStoreIterator<byte[]> fetchInternal(final
ReadOnlyWindowStore<Bytes, byte[]> store,
+ final Bytes
key,
+ final long
timeFrom,
+ final long
timeTo,
+ final
boolean forward) {
+ validateStoreOpen();
+ final WindowStoreIterator<byte[]> underlyingIterator = forward
+ ? store.fetch(key, Instant.ofEpochMilli(timeFrom),
Instant.ofEpochMilli(timeTo))
+ : store.backwardFetch(key, Instant.ofEpochMilli(timeFrom),
Instant.ofEpochMilli(timeTo));
if (internalContext.cache() == null) {
return underlyingIterator;
}
-
- return fetchInternal(underlyingIterator, key, timeFrom, timeTo, false);
- }
-
- private WindowStoreIterator<byte[]> fetchInternal(final
WindowStoreIterator<byte[]> underlyingIterator,
- final Bytes key,
- final long timeFrom,
- final long timeTo,
- final boolean forward) {
final PeekingKeyValueIterator<Bytes, LRUCacheEntry> cacheIterator =
new CacheIteratorWrapper(
key, timeFrom, timeTo, forward, hasIndex);
final KeySchema keySchema = hasIndex ? indexKeySchema : baseKeySchema;
@@ -374,25 +368,7 @@ public class TimeOrderedCachingWindowStore
final Bytes keyTo,
final long timeFrom,
final long timeTo) {
- if (keyFrom != null && keyTo != null && keyFrom.compareTo(keyTo) > 0) {
- LOG.warn("Returning empty iterator for fetch with invalid key
range: from > to. " +
- "This may be due to range arguments set in the wrong order, " +
- "or serdes that don't preserve ordering when lexicographically
comparing the serialized bytes. " +
- "Note that the built-in numerical serdes do not follow this
for negative numbers");
- return KeyValueIterators.emptyIterator();
- }
-
- // since this function may not access the underlying inner store, we
need to validate
- // if store is open outside as well.
- validateStoreOpen();
-
- final KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator =
- wrapped().fetch(keyFrom, keyTo, timeFrom, timeTo);
- if (internalContext.cache() == null) {
- return underlyingIterator;
- }
-
- return fetchKeyRange(underlyingIterator, keyFrom, keyTo, timeFrom,
timeTo, true);
+ return fetchKeyRange(wrapped(), keyFrom, keyTo, timeFrom, timeTo,
true);
}
@Override
@@ -400,32 +376,25 @@ public class TimeOrderedCachingWindowStore
final Bytes
keyTo,
final long
timeFrom,
final long
timeTo) {
- if (keyFrom != null && keyTo != null && keyFrom.compareTo(keyTo) > 0) {
- LOG.warn("Returning empty iterator for fetch with invalid key
range: from > to. "
- + "This may be due to serdes that don't preserve ordering when
lexicographically comparing the serialized bytes. " +
- "Note that the built-in numerical serdes do not follow this
for negative numbers");
- return KeyValueIterators.emptyIterator();
- }
-
- // since this function may not access the underlying inner store, we
need to validate
- // if store is open outside as well.
- validateStoreOpen();
-
- final KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator =
- wrapped().backwardFetch(keyFrom, keyTo, timeFrom, timeTo);
- if (internalContext.cache() == null) {
- return underlyingIterator;
- }
-
- return fetchKeyRange(underlyingIterator, keyFrom, keyTo, timeFrom,
timeTo, false);
+ return fetchKeyRange(wrapped(), keyFrom, keyTo, timeFrom, timeTo,
false);
}
- private KeyValueIterator<Windowed<Bytes>, byte[]> fetchKeyRange(final
KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator,
+ private KeyValueIterator<Windowed<Bytes>, byte[]> fetchKeyRange(final
ReadOnlyWindowStore<Bytes, byte[]> store,
final
Bytes keyFrom,
final
Bytes keyTo,
final long
timeFrom,
final long
timeTo,
final
boolean forward) {
+ if (isInvalidKeyRange(keyFrom, keyTo)) {
+ return KeyValueIterators.emptyIterator();
+ }
+ validateStoreOpen();
+ final KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator =
forward
+ ? store.fetch(keyFrom, keyTo, Instant.ofEpochMilli(timeFrom),
Instant.ofEpochMilli(timeTo))
+ : store.backwardFetch(keyFrom, keyTo,
Instant.ofEpochMilli(timeFrom), Instant.ofEpochMilli(timeTo));
+ if (internalContext.cache() == null) {
+ return underlyingIterator;
+ }
final PeekingKeyValueIterator<Bytes, LRUCacheEntry> cacheIterator =
new CacheIteratorWrapper(
keyFrom, keyTo, timeFrom, timeTo, forward, hasIndex);
@@ -453,19 +422,24 @@ public class TimeOrderedCachingWindowStore
@Override
public KeyValueIterator<Windowed<Bytes>, byte[]> fetchAll(final long
timeFrom,
final long
timeTo) {
- validateStoreOpen();
-
- final KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator =
wrapped().fetchAll(timeFrom, timeTo);
- return fetchAllInternal(underlyingIterator, timeFrom, timeTo, true);
+ return fetchAllInternal(wrapped(), timeFrom, timeTo, true);
}
@Override
public KeyValueIterator<Windowed<Bytes>, byte[]> backwardFetchAll(final
long timeFrom,
final
long timeTo) {
- validateStoreOpen();
+ return fetchAllInternal(wrapped(), timeFrom, timeTo, false);
+ }
- final KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator =
wrapped().backwardFetchAll(timeFrom, timeTo);
- return fetchAllInternal(underlyingIterator, timeFrom, timeTo, false);
+ private KeyValueIterator<Windowed<Bytes>, byte[]> fetchAllInternal(final
ReadOnlyWindowStore<Bytes, byte[]> store,
+ final
long timeFrom,
+ final
long timeTo,
+ final
boolean forward) {
+ validateStoreOpen();
+ final KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator =
forward
+ ? store.fetchAll(Instant.ofEpochMilli(timeFrom),
Instant.ofEpochMilli(timeTo))
+ : store.backwardFetchAll(Instant.ofEpochMilli(timeFrom),
Instant.ofEpochMilli(timeTo));
+ return fetchAllInternal(underlyingIterator, timeFrom, timeTo, forward);
}
private KeyValueIterator<Windowed<Bytes>, byte[]> fetchAllInternal(final
KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator,
@@ -538,6 +512,102 @@ public class TimeOrderedCachingWindowStore
}
}
+ @Override
+ public ReadOnlyWindowStore<Bytes, byte[]> readOnly(final IsolationLevel
isolationLevel) {
+ Objects.requireNonNull(isolationLevel, "isolationLevel cannot be
null");
+ if (isolationLevel == IsolationLevel.READ_COMMITTED) {
+ return wrapped().readOnly(isolationLevel);
+ }
+ return new ReadOnlyView(wrapped().readOnly(isolationLevel));
+ }
+
+ private boolean isInvalidKeyRange(final Bytes keyFrom, final Bytes keyTo) {
+ if (keyFrom != null && keyTo != null && keyFrom.compareTo(keyTo) > 0) {
+ LOG.warn("Returning empty iterator for fetch with invalid key
range: from > to. " +
+ "This may be due to range arguments set in the wrong
order, " +
+ "or serdes that don't preserve ordering when
lexicographically comparing the serialized bytes. " +
+ "Note that the built-in numerical serdes do not follow
this for negative numbers");
+ return true;
+ }
+ return false;
+ }
+
+ private final class ReadOnlyView implements ReadOnlyWindowStore<Bytes,
byte[]> {
+
+ private final ReadOnlyWindowStore<Bytes, byte[]> underlying;
+
+ ReadOnlyView(final ReadOnlyWindowStore<Bytes, byte[]> underlying) {
+ this.underlying = underlying;
+ }
+
+ @Override
+ public byte[] fetch(final Bytes key, final long timestamp) {
+ return fetchPointInternal(underlying, key, timestamp);
+ }
+
+ @Override
+ public WindowStoreIterator<byte[]> fetch(final Bytes key, final
Instant timeFrom, final Instant timeTo) {
+ return fetchInternal(underlying, key,
+ ApiUtils.validateMillisecondInstant(timeFrom,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeFrom, "timeFrom")),
+ ApiUtils.validateMillisecondInstant(timeTo,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeTo, "timeTo")),
+ true);
+ }
+
+ @Override
+ public WindowStoreIterator<byte[]> backwardFetch(final Bytes key,
final Instant timeFrom, final Instant timeTo) {
+ return fetchInternal(underlying, key,
+ ApiUtils.validateMillisecondInstant(timeFrom,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeFrom, "timeFrom")),
+ ApiUtils.validateMillisecondInstant(timeTo,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeTo, "timeTo")),
+ false);
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> fetch(final Bytes
keyFrom, final Bytes keyTo, final Instant timeFrom, final Instant timeTo) {
+ return fetchKeyRange(underlying, keyFrom, keyTo,
+ ApiUtils.validateMillisecondInstant(timeFrom,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeFrom, "timeFrom")),
+ ApiUtils.validateMillisecondInstant(timeTo,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeTo, "timeTo")),
+ true);
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> backwardFetch(final
Bytes keyFrom, final Bytes keyTo, final Instant timeFrom, final Instant timeTo)
{
+ return fetchKeyRange(underlying, keyFrom, keyTo,
+ ApiUtils.validateMillisecondInstant(timeFrom,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeFrom, "timeFrom")),
+ ApiUtils.validateMillisecondInstant(timeTo,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeTo, "timeTo")),
+ false);
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> fetchAll(final
Instant timeFrom, final Instant timeTo) {
+ return fetchAllInternal(underlying,
+ ApiUtils.validateMillisecondInstant(timeFrom,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeFrom, "timeFrom")),
+ ApiUtils.validateMillisecondInstant(timeTo,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeTo, "timeTo")),
+ true);
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]>
backwardFetchAll(final Instant timeFrom, final Instant timeTo) {
+ return fetchAllInternal(underlying,
+ ApiUtils.validateMillisecondInstant(timeFrom,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeFrom, "timeFrom")),
+ ApiUtils.validateMillisecondInstant(timeTo,
ApiUtils.prepareMillisCheckFailMsgPrefix(timeTo, "timeTo")),
+ false);
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> all() {
+ validateStoreOpen();
+ final KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator
= underlying.all();
+ return fetchAllInternal(underlyingIterator, 0, Long.MAX_VALUE,
true);
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> backwardAll() {
+ validateStoreOpen();
+ final KeyValueIterator<Windowed<Bytes>, byte[]> underlyingIterator
= underlying.backwardAll();
+ return fetchAllInternal(underlyingIterator, 0, Long.MAX_VALUE,
false);
+ }
+ }
+
private class CacheIteratorWrapper implements
PeekingKeyValueIterator<Bytes, LRUCacheEntry> {
diff --git
a/streams/src/test/java/org/apache/kafka/streams/state/internals/TimeOrderedCachingPersistentWindowStoreTest.java
b/streams/src/test/java/org/apache/kafka/streams/state/internals/TimeOrderedCachingPersistentWindowStoreTest.java
index b1148fa0716..2245e1eab00 100644
---
a/streams/src/test/java/org/apache/kafka/streams/state/internals/TimeOrderedCachingPersistentWindowStoreTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/state/internals/TimeOrderedCachingPersistentWindowStoreTest.java
@@ -17,6 +17,7 @@
package org.apache.kafka.streams.state.internals;
import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.header.internals.RecordHeaders;
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.serialization.IntegerSerializer;
@@ -43,6 +44,7 @@ import
org.apache.kafka.streams.processor.internals.MockStreamsMetrics;
import org.apache.kafka.streams.processor.internals.ProcessorRecordContext;
import org.apache.kafka.streams.query.Position;
import org.apache.kafka.streams.state.KeyValueIterator;
+import org.apache.kafka.streams.state.ReadOnlyWindowStore;
import org.apache.kafka.streams.state.StoreBuilder;
import org.apache.kafka.streams.state.Stores;
import org.apache.kafka.streams.state.TimestampedWindowStore;
@@ -1219,7 +1221,8 @@ public class TimeOrderedCachingPersistentWindowStoreTest {
assertThat(
messages,
hasItem("Returning empty iterator for fetch with invalid key
range: from > to." +
- " This may be due to serdes that don't preserve ordering
when lexicographically comparing the serialized bytes." +
+ " This may be due to range arguments set in the wrong
order, " +
+ "or serdes that don't preserve ordering when
lexicographically comparing the serialized bytes." +
" Note that the built-in numerical serdes do not follow
this for negative numbers")
);
}
@@ -1256,6 +1259,176 @@ public class
TimeOrderedCachingPersistentWindowStoreTest {
verifyAndTearDownCloseTests();
}
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldReadCommittedBypassesCache(final boolean hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("a"), DEFAULT_TIMESTAMP);
+
+ try (final WindowStoreIterator<byte[]> it =
+ cachingStore.readOnly(IsolationLevel.READ_COMMITTED)
+ .fetch(bytesKey("a"), ofEpochMilli(DEFAULT_TIMESTAMP),
ofEpochMilli(DEFAULT_TIMESTAMP))) {
+ assertFalse(it.hasNext());
+ }
+
+ cachingStore.commit(Map.of());
+
+ try (final WindowStoreIterator<byte[]> it =
+ cachingStore.readOnly(IsolationLevel.READ_COMMITTED)
+ .fetch(bytesKey("a"), ofEpochMilli(DEFAULT_TIMESTAMP),
ofEpochMilli(DEFAULT_TIMESTAMP))) {
+ assertTrue(it.hasNext());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldReadUncommittedViewFetchPointInTime(final boolean
hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("store"),
DEFAULT_TIMESTAMP);
+ cachingStore.commit(Map.of());
+ cachingStore.put(bytesKey("b"), bytesValue("cache"),
DEFAULT_TIMESTAMP);
+
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ assertArrayEquals(bytesValue("store"), view.fetch(bytesKey("a"),
DEFAULT_TIMESTAMP));
+ assertArrayEquals(bytesValue("cache"), view.fetch(bytesKey("b"),
DEFAULT_TIMESTAMP));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void
shouldReadUncommittedViewFetchSingleKeyMergesCacheAndStore(final boolean
hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("1"), DEFAULT_TIMESTAMP);
+ cachingStore.commit(Map.of());
+ cachingStore.put(bytesKey("a"), bytesValue("2"), DEFAULT_TIMESTAMP +
20);
+
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ try (final WindowStoreIterator<byte[]> it =
+ view.fetch(bytesKey("a"), ofEpochMilli(DEFAULT_TIMESTAMP),
ofEpochMilli(DEFAULT_TIMESTAMP + 20))) {
+ verifyKeyValue(it.next(), DEFAULT_TIMESTAMP, "1");
+ verifyKeyValue(it.next(), DEFAULT_TIMESTAMP + 20, "2");
+ assertFalse(it.hasNext());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void
shouldReadUncommittedViewBackwardFetchSingleKeyMergesCacheAndStore(final
boolean hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("1"), DEFAULT_TIMESTAMP);
+ cachingStore.commit(Map.of());
+ cachingStore.put(bytesKey("a"), bytesValue("2"), DEFAULT_TIMESTAMP +
20);
+
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ try (final WindowStoreIterator<byte[]> it =
+ view.backwardFetch(bytesKey("a"),
ofEpochMilli(DEFAULT_TIMESTAMP), ofEpochMilli(DEFAULT_TIMESTAMP + 20))) {
+ verifyKeyValue(it.next(), DEFAULT_TIMESTAMP + 20, "2");
+ verifyKeyValue(it.next(), DEFAULT_TIMESTAMP, "1");
+ assertFalse(it.hasNext());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldReadUncommittedViewFetchRangeMergesCacheAndStore(final
boolean hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("1"), DEFAULT_TIMESTAMP);
+ cachingStore.commit(Map.of());
+ cachingStore.put(bytesKey("b"), bytesValue("2"), DEFAULT_TIMESTAMP);
+
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ final List<KeyValue<Windowed<Bytes>, byte[]>> results =
toListAndCloseIterator(
+ view.fetch(bytesKey("a"), bytesKey("b"),
ofEpochMilli(DEFAULT_TIMESTAMP), ofEpochMilli(DEFAULT_TIMESTAMP)));
+ assertEquals(2, results.size());
+ assertEquals(bytesKey("a"), results.get(0).key.key());
+ assertEquals(bytesKey("b"), results.get(1).key.key());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void
shouldReadUncommittedViewBackwardFetchRangeMergesCacheAndStore(final boolean
hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("1"), DEFAULT_TIMESTAMP);
+ cachingStore.commit(Map.of());
+ cachingStore.put(bytesKey("b"), bytesValue("2"), DEFAULT_TIMESTAMP);
+
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ final List<KeyValue<Windowed<Bytes>, byte[]>> results =
toListAndCloseIterator(
+ view.backwardFetch(bytesKey("a"), bytesKey("b"),
ofEpochMilli(DEFAULT_TIMESTAMP), ofEpochMilli(DEFAULT_TIMESTAMP)));
+ assertEquals(2, results.size());
+ assertEquals(bytesKey("b"), results.get(0).key.key());
+ assertEquals(bytesKey("a"), results.get(1).key.key());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldReadUncommittedViewFetchAllMergesCacheAndStore(final
boolean hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("1"), DEFAULT_TIMESTAMP);
+ cachingStore.commit(Map.of());
+ cachingStore.put(bytesKey("b"), bytesValue("2"), DEFAULT_TIMESTAMP);
+
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ final List<KeyValue<Windowed<Bytes>, byte[]>> results =
toListAndCloseIterator(
+ view.fetchAll(ofEpochMilli(DEFAULT_TIMESTAMP),
ofEpochMilli(DEFAULT_TIMESTAMP)));
+ assertEquals(2, results.size());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldReadUncommittedViewAllMergesCacheAndStore(final boolean
hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("1"), DEFAULT_TIMESTAMP);
+ cachingStore.commit(Map.of());
+ cachingStore.put(bytesKey("b"), bytesValue("2"), DEFAULT_TIMESTAMP);
+
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ final List<KeyValue<Windowed<Bytes>, byte[]>> results =
toListAndCloseIterator(view.all());
+ assertEquals(2, results.size());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldReadUncommittedViewBackwardAllMergesCacheAndStore(final
boolean hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("1"), DEFAULT_TIMESTAMP);
+ cachingStore.commit(Map.of());
+ cachingStore.put(bytesKey("b"), bytesValue("2"), DEFAULT_TIMESTAMP);
+
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ final List<KeyValue<Windowed<Bytes>, byte[]>> results =
toListAndCloseIterator(view.backwardAll());
+ assertEquals(2, results.size());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void
shouldReadUncommittedViewBackwardFetchAllMergesCacheAndStore(final boolean
hasIndex) {
+ setUp(hasIndex);
+ cachingStore.put(bytesKey("a"), bytesValue("1"), DEFAULT_TIMESTAMP);
+ cachingStore.commit(Map.of());
+ cachingStore.put(bytesKey("b"), bytesValue("2"), DEFAULT_TIMESTAMP);
+
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ final List<KeyValue<Windowed<Bytes>, byte[]>> results =
toListAndCloseIterator(
+ view.backwardFetchAll(ofEpochMilli(DEFAULT_TIMESTAMP),
ofEpochMilli(DEFAULT_TIMESTAMP)));
+ assertEquals(2, results.size());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldThrowOnNullInstantInViewFetch(final boolean hasIndex) {
+ setUp(hasIndex);
+ final ReadOnlyWindowStore<Bytes, byte[]> view =
cachingStore.readOnly(IsolationLevel.READ_UNCOMMITTED);
+ // underlying.fetch validates via ApiUtils.validateMillisecondInstant
before toEpochMilli() is reached
+ assertThrows(IllegalArgumentException.class, () ->
view.fetch(bytesKey("a"), null, ofEpochMilli(0)));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void shouldThrowNpeOnNullIsolationLevel(final boolean hasIndex) {
+ setUp(hasIndex);
+ assertThrows(NullPointerException.class, () ->
cachingStore.readOnly(null));
+ }
+
@SuppressWarnings("unchecked")
private void setUpCloseTests() {
underlyingStore = mock(RocksDBTimeOrderedWindowStore.class);
diff --git
a/streams/src/test/java/org/apache/kafka/streams/state/internals/TimeOrderedWindowStoreTest.java
b/streams/src/test/java/org/apache/kafka/streams/state/internals/TimeOrderedWindowStoreTest.java
index 51d65a330ae..5028380b893 100644
---
a/streams/src/test/java/org/apache/kafka/streams/state/internals/TimeOrderedWindowStoreTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/state/internals/TimeOrderedWindowStoreTest.java
@@ -1225,7 +1225,8 @@ public class TimeOrderedWindowStoreTest {
assertThat(
messages,
hasItem("Returning empty iterator for fetch with invalid key
range: from > to." +
- " This may be due to serdes that don't preserve ordering
when lexicographically comparing the serialized bytes." +
+ " This may be due to range arguments set in the wrong
order, " +
+ "or serdes that don't preserve ordering when
lexicographically comparing the serialized bytes." +
" Note that the built-in numerical serdes do not follow
this for negative numbers")
);
}