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 75e66426bf5 KAFKA-20768: Add transactional and IQ isolation to
time-ordered RocksDB stores (#22829)
75e66426bf5 is described below
commit 75e66426bf533b97898e7c558bd96712dd61c440
Author: Nick Telford <[email protected]>
AuthorDate: Thu Jul 16 00:47:56 2026 +0100
KAFKA-20768: Add transactional and IQ isolation to time-ordered RocksDB
stores (#22829)
The time-ordered / dual-schema RocksDB stores — used for stream-stream
joins and suppression buffers — were the remaining KIP-892 segmented
stores without transactional support or isolation-level interactive
queries. A `READ_COMMITTED` IQ against a time-ordered window or session
store returned writes that were still staged in an uncommitted
transaction. This was called out as an explicit follow-up in #22754,
which covered the standard window and session stores.
This change applies the same design to the
`AbstractDualSchemaRocksDBSegmentedBytesStore` subtree (the time-ordered
window and session stores and their with-headers variants). Because each
segment is itself a `RocksDBStore`, the per-segment write and commit
path already stages writes; the two gaps filled here are the same as
before:
- **Staged position** — `put` now stages the `Position` of an
uncommitted write in a `pendingPosition`, merged into the committed
position atomically with the per-segment buffer flush on `commit`.
`getPosition()` reflects staged writes (for the owner's own view and
READ_UNCOMMITTED queries), while the new `getCommittedPosition()`
excludes them to bound READ_COMMITTED queries.
- **Isolation-aware reads** — a `readOnly(IsolationLevel)` view over the
segmented store, exposed to
`RocksDBTimeOrderedWindowStore`/`RocksDBTimeOrderedSessionStore` so
their `readOnly(...)` views (and `StoreQueryUtils` dispatch) hide
uncommitted writes under READ_COMMITTED.
Reads follow the same convention as the other stores: the owner (stream
thread) reads live through the vanilla store methods, while interactive
queries read through `readOnly(IsolationLevel)`. The transaction
buffer's own owner-thread check keeps the owner's reads lock-free and
snapshots non-owner READ_UNCOMMITTED reads under the read lock;
READ_COMMITTED bypasses the buffer. As before, no isolation surface is
added to the `SegmentedBytesStore` interface — the isolation methods are
package-private utilities shared between the vanilla methods and the
read view — and the wrapping stores hold the concrete
`AbstractRocksDBTimeOrderedSegmentedBytesStore` so they reach that view
without casting.
The one wrinkle specific to the dual-schema stores is the index-to-base
lookup: an indexed store iterates the key-ordered index and then reads
each value from the time-ordered base store. That base read now goes
through the same isolation view as the index scan, so both sides observe
a consistent snapshot. The base read on the owner path also repairs
stale index entries; because an interactive query must never mutate the
store, that repair is skipped when reading through an isolation view.
The plain time-ordered key-value store (the suppression buffer backing
store) is not queried interactively, but it inherits the staged position
and commit changes for free and remains correct.
Testing: `AbstractDualSchemaRocksDBSegmentedBytesStoreTest` gains
transactional cases that run against all four concrete configurations
(window and session, each with and without an index), asserting that
READ_COMMITTED hides staged writes while READ_UNCOMMITTED exposes them,
that both levels converge after commit, that the committed position
excludes staged writes until commit, and that a non-transactional store
reads identically across levels.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewers: Bill Bejeck <[email protected]>
---
gradle/spotbugs-exclude.xml | 1 +
...stractDualSchemaRocksDBSegmentedBytesStore.java | 80 ++++++++---
...tractRocksDBTimeOrderedSegmentedBytesStore.java | 158 +++++++++++++++++----
.../RocksDBTimeOrderedKeyValueBytesStore.java | 4 +-
...cksDBTimeOrderedSessionSegmentedBytesStore.java | 31 +++-
.../internals/RocksDBTimeOrderedSessionStore.java | 129 ++++++++++++-----
...ocksDBTimeOrderedWindowSegmentedBytesStore.java | 11 +-
.../internals/RocksDBTimeOrderedWindowStore.java | 125 +++++++++++-----
...ctDualSchemaRocksDBSegmentedBytesStoreTest.java | 116 +++++++++++++++
9 files changed, 528 insertions(+), 127 deletions(-)
diff --git a/gradle/spotbugs-exclude.xml b/gradle/spotbugs-exclude.xml
index 7268ffc6ebe..4c12255637d 100644
--- a/gradle/spotbugs-exclude.xml
+++ b/gradle/spotbugs-exclude.xml
@@ -623,6 +623,7 @@ For a detailed description of spotbugs bug categories, see
https://spotbugs.read
<Class
name="org.apache.kafka.streams.processor.internals.StreamsProducer"/>
<Class
name="org.apache.kafka.streams.processor.internals.TaskManager"/>
<Class
name="org.apache.kafka.streams.state.internals.AbstractRocksDBSegmentedBytesStore"/>
+ <Class
name="org.apache.kafka.streams.state.internals.AbstractDualSchemaRocksDBSegmentedBytesStore"/>
<Class
name="org.apache.kafka.streams.state.internals.InMemoryTimeOrderedKeyValueChangeBuffer"/>
<Class
name="org.apache.kafka.streams.state.internals.RocksDBStore"/>
<Class
name="org.apache.kafka.streams.state.internals.TimeOrderedCachingWindowStore"/>
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractDualSchemaRocksDBSegmentedBytesStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractDualSchemaRocksDBSegmentedBytesStore.java
index 9dbb4210aa7..a90d87b9077 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractDualSchemaRocksDBSegmentedBytesStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractDualSchemaRocksDBSegmentedBytesStore.java
@@ -17,6 +17,7 @@
package org.apache.kafka.streams.state.internals;
import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.metrics.Sensor;
import org.apache.kafka.common.utils.Bytes;
@@ -60,6 +61,9 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStore<S extends Seg
protected long observedStreamTime = ConsumerRecord.NO_TIMESTAMP;
protected boolean consistencyEnabled = false;
protected Position position;
+ // Position of writes staged in the current transaction; merged into the
committed position on commit().
+ private Position pendingPosition = Position.emptyPosition();
+ private boolean transactional;
private volatile boolean open;
AbstractDualSchemaRocksDBSegmentedBytesStore(final String name,
@@ -81,36 +85,28 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStore<S extends Seg
@Override
public KeyValueIterator<Bytes, byte[]> all() {
-
- final long actualFrom = getActualFrom(0, baseKeySchema instanceof
PrefixedWindowKeySchemas.TimeFirstWindowKeySchema);
-
- final List<S> searchSpace = segments.allSegments(true);
- final Bytes from = baseKeySchema.lowerRange(null, actualFrom);
- final Bytes to = baseKeySchema.upperRange(null, Long.MAX_VALUE);
-
- return new SegmentIterator<>(
- searchSpace.iterator(),
- baseKeySchema.hasNextCondition(null, null, actualFrom,
Long.MAX_VALUE, true),
- from,
- to,
- true);
+ return all(true, null);
}
@Override
public KeyValueIterator<Bytes, byte[]> backwardAll() {
+ return all(false, null);
+ }
+ KeyValueIterator<Bytes, byte[]> all(final boolean forward, final
IsolationLevel isolationLevel) {
final long actualFrom = getActualFrom(0, baseKeySchema instanceof
PrefixedWindowKeySchemas.TimeFirstWindowKeySchema);
- final List<S> searchSpace = segments.allSegments(false);
+ final List<S> searchSpace = segments.allSegments(forward);
final Bytes from = baseKeySchema.lowerRange(null, actualFrom);
final Bytes to = baseKeySchema.upperRange(null, Long.MAX_VALUE);
return new SegmentIterator<>(
searchSpace.iterator(),
- baseKeySchema.hasNextCondition(null, null, actualFrom,
Long.MAX_VALUE, false),
+ baseKeySchema.hasNextCondition(null, null, actualFrom,
Long.MAX_VALUE, forward),
from,
to,
- false);
+ forward,
+ isolationLevel);
}
@Override
@@ -197,7 +193,8 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStore<S extends Seg
LOG.warn("Skipping record for expired segment.");
} else {
synchronized (position) {
- StoreQueryUtils.updatePosition(position,
internalProcessorContext);
+ // Transactional puts stage their position too, so
READ_COMMITTED never sees a position ahead of the data.
+ StoreQueryUtils.updatePosition(transactional ? pendingPosition
: position, internalProcessorContext);
// Put to index first so that if put to base failed, when we
iterate index, we will
// find no base value. If put to base first but putting to
index fails, when we iterate
@@ -214,6 +211,10 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStore<S extends Seg
@Override
public byte[] get(final Bytes rawKey) {
+ return get(rawKey, null);
+ }
+
+ byte[] get(final Bytes rawKey, final IsolationLevel isolationLevel) {
final long timestampFromRawKey =
baseKeySchema.segmentTimestamp(rawKey);
// check if timestamp is expired
@@ -235,7 +236,8 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStore<S extends Seg
if (segment == null) {
return null;
}
- return segment.get(rawKey);
+ // Live read for the vanilla path; interactive queries read through
the segment's isolation view.
+ return isolationLevel == null ? segment.get(rawKey) :
segment.readOnly(isolationLevel).get(rawKey);
}
@Override
@@ -259,6 +261,7 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStore<S extends Seg
segments.openExisting(internalProcessorContext, observedStreamTime);
this.position = segments.position;
+ this.pendingPosition = Position.emptyPosition();
StoreQueryUtils.maybeMigrateExistingPositionFile(stateStoreContext.stateDir(),
name(), this.position);
// register and possibly restore the state from the logs
@@ -275,11 +278,24 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStore<S extends Seg
IQ_CONSISTENCY_OFFSET_VECTOR_ENABLED,
false
);
+
+ transactional = StreamsConfig.InternalConfig.getBoolean(
+ stateStoreContext.appConfigs(),
+ StreamsConfig.TRANSACTIONAL_STATE_STORES_CONFIG,
+ false
+ );
}
@Override
public void commit(final Map<TopicPartition, Long> changelogOffsets) {
- segments.commit(changelogOffsets);
+ synchronized (position) {
+ if (transactional) {
+ // Publish the staged position atomically with the
segment-buffer flush below.
+ position.merge(pendingPosition);
+ pendingPosition = Position.emptyPosition();
+ }
+ segments.commit(changelogOffsets);
+ }
}
@SuppressWarnings("deprecation")
@@ -293,10 +309,21 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStore<S extends Seg
return segments.committedOffset(partition);
}
+ @Override
+ public long approximateNumUncommittedBytes() {
+ long total = 0;
+ for (final S segment : segments.allSegments(true)) {
+ total += segment.approximateNumUncommittedBytes();
+ }
+ return total;
+ }
+
@Override
public void close() {
open = false;
segments.close();
+ // The segments discard their uncommitted writes on close; drop the
staged position to match.
+ pendingPosition = Position.emptyPosition();
}
@Override
@@ -335,6 +362,21 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStore<S extends Seg
@Override
public Position getPosition() {
+ // Include staged writes so the owner's view (and READ_UNCOMMITTED
queries) stays consistent.
+ if (transactional) {
+ synchronized (position) {
+ return position.copy().merge(pendingPosition);
+ }
+ }
+ return position;
+ }
+
+ Position getCommittedPosition() {
+ if (transactional) {
+ synchronized (position) {
+ return position.copy();
+ }
+ }
return position;
}
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBTimeOrderedSegmentedBytesStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBTimeOrderedSegmentedBytesStore.java
index bec19c91abc..fb0b2741147 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBTimeOrderedSegmentedBytesStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBTimeOrderedSegmentedBytesStore.java
@@ -17,6 +17,7 @@
package org.apache.kafka.streams.state.internals;
import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.errors.ProcessorStateException;
@@ -34,6 +35,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
@@ -74,10 +76,13 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
public abstract class IndexToBaseStoreIterator implements
KeyValueIterator<Bytes, byte[]> {
private final KeyValueIterator<Bytes, byte[]> indexIterator;
private byte[] cachedValue;
+ // Null for the owner path (live base reads plus stale-index repair);
non-null for isolation-bound
+ // interactive queries, which read the base store through its
isolation view and never mutate the index.
+ private final IsolationLevel isolationLevel;
-
- IndexToBaseStoreIterator(final KeyValueIterator<Bytes, byte[]>
indexIterator) {
+ IndexToBaseStoreIterator(final KeyValueIterator<Bytes, byte[]>
indexIterator, final IsolationLevel isolationLevel) {
this.indexIterator = indexIterator;
+ this.isolationLevel = isolationLevel;
}
@Override
@@ -99,11 +104,14 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
final Bytes key = indexIterator.peekNextKey();
final Bytes baseKey = getBaseKey(key);
- cachedValue = get(baseKey);
+ cachedValue = get(baseKey, isolationLevel);
if (cachedValue == null) {
// Key not in base store or key is expired, inconsistency
happened and remove from index.
+ // Interactive queries read through an isolation view and
must not mutate, so skip the repair.
indexIterator.next();
-
AbstractRocksDBTimeOrderedSegmentedBytesStore.this.removeIndex(key);
+ if (isolationLevel == null) {
+
AbstractRocksDBTimeOrderedSegmentedBytesStore.this.removeIndex(key);
+ }
} else {
return true;
}
@@ -199,7 +207,8 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
return fetch(key, from, to, false);
}
- protected abstract IndexToBaseStoreIterator
getIndexToBaseStoreIterator(final SegmentIterator<S> segmentIterator);
+ protected abstract IndexToBaseStoreIterator
getIndexToBaseStoreIterator(final SegmentIterator<S> segmentIterator,
+
final IsolationLevel isolationLevel);
public void put(final Bytes key, final long timestamp, final int seqnum,
final byte[] value) {
throw new UnsupportedOperationException("This store does not support
put with timestamp and seqnum");
@@ -229,6 +238,14 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
final long from,
final long to,
final boolean forward) {
+ return fetch(key, from, to, forward, null);
+ }
+
+ KeyValueIterator<Bytes, byte[]> fetch(final Bytes key,
+ final long from,
+ final long to,
+ final boolean forward,
+ final IsolationLevel isolationLevel)
{
final long actualFrom = getActualFrom(from, baseKeySchema instanceof
PrefixedWindowKeySchemas.TimeFirstWindowKeySchema);
@@ -248,7 +265,8 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
indexKeySchema.get().hasNextCondition(key, key, actualFrom,
to, forward),
binaryFrom,
binaryTo,
- forward));
+ forward,
+ isolationLevel), isolationLevel);
}
@@ -263,7 +281,8 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
baseKeySchema.hasNextCondition(key, key, actualFrom, to, forward),
binaryFrom,
binaryTo,
- forward);
+ forward,
+ isolationLevel);
}
@Override
@@ -287,6 +306,15 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
final long from,
final long to,
final boolean forward) {
+ return fetch(keyFrom, keyTo, from, to, forward, null);
+ }
+
+ KeyValueIterator<Bytes, byte[]> fetch(final Bytes keyFrom,
+ final Bytes keyTo,
+ final long from,
+ final long to,
+ final boolean forward,
+ final IsolationLevel isolationLevel)
{
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, " +
@@ -313,7 +341,8 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
indexKeySchema.get().hasNextCondition(keyFrom, keyTo,
actualFrom, to, forward),
binaryFrom,
binaryTo,
- forward));
+ forward,
+ isolationLevel), isolationLevel);
}
final List<S> searchSpace = baseKeySchema.segmentsToSearch(segments,
actualFrom, to,
@@ -327,12 +356,26 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
baseKeySchema.hasNextCondition(keyFrom, keyTo, actualFrom, to,
forward),
binaryFrom,
binaryTo,
- forward);
+ forward,
+ isolationLevel);
}
@Override
public KeyValueIterator<Bytes, byte[]> fetchAll(final long timeFrom,
final long timeTo) {
+ return fetchAll(timeFrom, timeTo, true, null);
+ }
+
+ @Override
+ public KeyValueIterator<Bytes, byte[]> backwardFetchAll(final long
timeFrom,
+ final long timeTo)
{
+ return fetchAll(timeFrom, timeTo, false, null);
+ }
+
+ KeyValueIterator<Bytes, byte[]> fetchAll(final long timeFrom,
+ final long timeTo,
+ final boolean forward,
+ final IsolationLevel
isolationLevel) {
final long actualFrom = getActualFrom(timeFrom, baseKeySchema
instanceof PrefixedWindowKeySchemas.TimeFirstWindowKeySchema);
@@ -340,37 +383,98 @@ public abstract class
AbstractRocksDBTimeOrderedSegmentedBytesStore<S extends Se
return KeyValueIterators.emptyIterator();
}
- final List<S> searchSpace = segments.segments(actualFrom, timeTo,
true);
+ final List<S> searchSpace = segments.segments(actualFrom, timeTo,
forward);
final Bytes binaryFrom = baseKeySchema.lowerRange(null, actualFrom);
final Bytes binaryTo = baseKeySchema.upperRange(null, timeTo);
return new SegmentIterator<>(
searchSpace.iterator(),
- baseKeySchema.hasNextCondition(null, null, actualFrom, timeTo,
true),
+ baseKeySchema.hasNextCondition(null, null, actualFrom, timeTo,
forward),
binaryFrom,
binaryTo,
- true);
+ forward,
+ isolationLevel);
}
- @Override
- public KeyValueIterator<Bytes, byte[]> backwardFetchAll(final long
timeFrom,
- final long timeTo)
{
+ /**
+ * As {@link #fetchSession(Bytes, long, long)}, but reading through the
segment isolation view for the
+ * given level. Only the session store supports this; other stores throw.
+ */
+ byte[] fetchSession(final Bytes key,
+ final long sessionStartTime,
+ final long sessionEndTime,
+ final IsolationLevel isolationLevel) {
+ throw new UnsupportedOperationException("This store does not support
fetchSession");
+ }
- final long actualFrom = getActualFrom(timeFrom, baseKeySchema
instanceof PrefixedWindowKeySchemas.TimeFirstWindowKeySchema);
+ /**
+ * As {@link #fetchSessions(long, long)}, but reading through the segment
isolation view for the given
+ * level. Only the session store supports this; other stores throw.
+ */
+ KeyValueIterator<Bytes, byte[]> fetchSessions(final long
earliestSessionEndTime,
+ final long
latestSessionEndTime,
+ final IsolationLevel
isolationLevel) {
+ throw new UnsupportedOperationException("This store does not support
fetchSessions");
+ }
- if (baseKeySchema instanceof
PrefixedWindowKeySchemas.TimeFirstWindowKeySchema && timeTo < actualFrom) {
- return KeyValueIterators.emptyIterator();
+ /** An isolation-bound read view over this store, delegating to the
store's isolation-aware read utilities. */
+ ReadOnlyView readOnly(final IsolationLevel isolationLevel) {
+ Objects.requireNonNull(isolationLevel, "isolationLevel cannot be
null");
+ return new ReadOnlyView(this, isolationLevel);
+ }
+
+ /** Read view bound to an {@link IsolationLevel}; used by the wrapping
window and session stores for IQ reads. */
+ static final class ReadOnlyView {
+ private final AbstractRocksDBTimeOrderedSegmentedBytesStore<?> store;
+ private final IsolationLevel isolationLevel;
+
+ private ReadOnlyView(final
AbstractRocksDBTimeOrderedSegmentedBytesStore<?> store, final IsolationLevel
isolationLevel) {
+ this.store = store;
+ this.isolationLevel = isolationLevel;
}
- final List<S> searchSpace = segments.segments(actualFrom, timeTo,
false);
- final Bytes binaryFrom = baseKeySchema.lowerRange(null, actualFrom);
- final Bytes binaryTo = baseKeySchema.upperRange(null, timeTo);
+ byte[] get(final Bytes rawKey) {
+ return store.get(rawKey, isolationLevel);
+ }
- return new SegmentIterator<>(
- searchSpace.iterator(),
- baseKeySchema.hasNextCondition(null, null, actualFrom, timeTo,
false),
- binaryFrom,
- binaryTo,
- false);
+ KeyValueIterator<Bytes, byte[]> fetch(final Bytes key, final long
from, final long to) {
+ return store.fetch(key, from, to, true, isolationLevel);
+ }
+
+ KeyValueIterator<Bytes, byte[]> backwardFetch(final Bytes key, final
long from, final long to) {
+ return store.fetch(key, from, to, false, isolationLevel);
+ }
+
+ KeyValueIterator<Bytes, byte[]> fetch(final Bytes keyFrom, final Bytes
keyTo, final long from, final long to) {
+ return store.fetch(keyFrom, keyTo, from, to, true, isolationLevel);
+ }
+
+ KeyValueIterator<Bytes, byte[]> backwardFetch(final Bytes keyFrom,
final Bytes keyTo, final long from, final long to) {
+ return store.fetch(keyFrom, keyTo, from, to, false,
isolationLevel);
+ }
+
+ KeyValueIterator<Bytes, byte[]> fetchAll(final long from, final long
to) {
+ return store.fetchAll(from, to, true, isolationLevel);
+ }
+
+ KeyValueIterator<Bytes, byte[]> backwardFetchAll(final long from,
final long to) {
+ return store.fetchAll(from, to, false, isolationLevel);
+ }
+
+ KeyValueIterator<Bytes, byte[]> all() {
+ return store.all(true, isolationLevel);
+ }
+
+ KeyValueIterator<Bytes, byte[]> backwardAll() {
+ return store.all(false, isolationLevel);
+ }
+
+ byte[] fetchSession(final Bytes key, final long sessionStartTime,
final long sessionEndTime) {
+ return store.fetchSession(key, sessionStartTime, sessionEndTime,
isolationLevel);
+ }
+
+ KeyValueIterator<Bytes, byte[]> fetchSessions(final long
earliestSessionEndTime, final long latestSessionEndTime) {
+ return store.fetchSessions(earliestSessionEndTime,
latestSessionEndTime, isolationLevel);
+ }
}
}
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedKeyValueBytesStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedKeyValueBytesStore.java
index 37c6dce156b..e8de482e160 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedKeyValueBytesStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedKeyValueBytesStore.java
@@ -17,6 +17,7 @@
package org.apache.kafka.streams.state.internals;
import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.KeyValue;
import
org.apache.kafka.streams.state.internals.PrefixedWindowKeySchemas.TimeFirstWindowKeySchema;
@@ -57,7 +58,8 @@ public class RocksDBTimeOrderedKeyValueBytesStore extends
AbstractRocksDBTimeOrd
}
@Override
- protected IndexToBaseStoreIterator getIndexToBaseStoreIterator(final
SegmentIterator<KeyValueSegment> segmentIterator) {
+ protected IndexToBaseStoreIterator getIndexToBaseStoreIterator(final
SegmentIterator<KeyValueSegment> segmentIterator,
+ final
IsolationLevel isolationLevel) {
throw new UnsupportedOperationException("Do not use for
TimeOrderedKeyValueStore");
}
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedSessionSegmentedBytesStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedSessionSegmentedBytesStore.java
index dbb7713ab35..40dfdfea071 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedSessionSegmentedBytesStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedSessionSegmentedBytesStore.java
@@ -17,6 +17,7 @@
package org.apache.kafka.streams.state.internals;
import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.kstream.Window;
@@ -38,8 +39,9 @@ import java.util.Optional;
public class RocksDBTimeOrderedSessionSegmentedBytesStore<S extends Segment>
extends AbstractRocksDBTimeOrderedSegmentedBytesStore<S> {
private class SessionKeySchemaIndexToBaseStoreIterator extends
IndexToBaseStoreIterator {
- SessionKeySchemaIndexToBaseStoreIterator(final KeyValueIterator<Bytes,
byte[]> indexIterator) {
- super(indexIterator);
+ SessionKeySchemaIndexToBaseStoreIterator(final KeyValueIterator<Bytes,
byte[]> indexIterator,
+ final IsolationLevel
isolationLevel) {
+ super(indexIterator, isolationLevel);
}
@Override
@@ -67,16 +69,31 @@ public class RocksDBTimeOrderedSessionSegmentedBytesStore<S
extends Segment> ext
public byte[] fetchSession(final Bytes key,
final long sessionStartTime,
final long sessionEndTime) {
+ return fetchSession(key, sessionStartTime, sessionEndTime, null);
+ }
+
+ @Override
+ byte[] fetchSession(final Bytes key,
+ final long sessionStartTime,
+ final long sessionEndTime,
+ final IsolationLevel isolationLevel) {
return get(TimeFirstSessionKeySchema.toBinary(
key,
sessionStartTime,
sessionEndTime
- ));
+ ), isolationLevel);
}
@Override
public KeyValueIterator<Bytes, byte[]> fetchSessions(final long
earliestSessionEndTime,
final long
latestSessionEndTime) {
+ return fetchSessions(earliestSessionEndTime, latestSessionEndTime,
null);
+ }
+
+ @Override
+ KeyValueIterator<Bytes, byte[]> fetchSessions(final long
earliestSessionEndTime,
+ final long
latestSessionEndTime,
+ final IsolationLevel
isolationLevel) {
final List<S> searchSpace = segments.segments(earliestSessionEndTime,
latestSessionEndTime, true);
// here we want [0, latestSE, FF] as the upper bound to cover any
possible keys,
@@ -102,7 +119,8 @@ public class RocksDBTimeOrderedSessionSegmentedBytesStore<S
extends Segment> ext
},
binaryFrom,
binaryTo,
- true);
+ true,
+ isolationLevel);
}
@Override
@@ -133,7 +151,8 @@ public class RocksDBTimeOrderedSessionSegmentedBytesStore<S
extends Segment> ext
}
@Override
- protected IndexToBaseStoreIterator getIndexToBaseStoreIterator(final
SegmentIterator<S> segmentIterator) {
- return new SessionKeySchemaIndexToBaseStoreIterator(segmentIterator);
+ protected IndexToBaseStoreIterator getIndexToBaseStoreIterator(final
SegmentIterator<S> segmentIterator,
+ final
IsolationLevel isolationLevel) {
+ return new SessionKeySchemaIndexToBaseStoreIterator(segmentIterator,
isolationLevel);
}
}
\ No newline at end of file
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedSessionStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedSessionStore.java
index ab375907f5d..8fd0b736718 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedSessionStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedSessionStore.java
@@ -16,18 +16,22 @@
*/
package org.apache.kafka.streams.state.internals;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.kstream.Windowed;
import org.apache.kafka.streams.processor.StateStore;
import org.apache.kafka.streams.processor.StateStoreContext;
+import org.apache.kafka.streams.query.Position;
import org.apache.kafka.streams.query.PositionBound;
import org.apache.kafka.streams.query.Query;
import org.apache.kafka.streams.query.QueryConfig;
import org.apache.kafka.streams.query.QueryResult;
import org.apache.kafka.streams.state.KeyValueIterator;
+import org.apache.kafka.streams.state.ReadOnlySessionStore;
import org.apache.kafka.streams.state.SessionStore;
import
org.apache.kafka.streams.state.internals.PrefixedSessionKeySchemas.TimeFirstSessionKeySchema;
+import java.time.Instant;
import java.util.Objects;
public class RocksDBTimeOrderedSessionStore
@@ -52,12 +56,15 @@ public class RocksDBTimeOrderedSessionStore
final PositionBound positionBound,
final QueryConfig config) {
+ final Position queryPosition = config.getIsolationLevel() ==
IsolationLevel.READ_COMMITTED
+ ? wrapped().getCommittedPosition()
+ : getPosition();
return StoreQueryUtils.handleBasicQueries(
query,
positionBound,
config,
this,
- getPosition(),
+ queryPosition,
stateStoreContext
);
}
@@ -65,32 +72,21 @@ public class RocksDBTimeOrderedSessionStore
@Override
public KeyValueIterator<Windowed<Bytes>, byte[]> findSessions(final long
earliestSessionEndTime,
final long
latestSessionEndTime) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().fetchSessions(earliestSessionEndTime, latestSessionEndTime);
- return new WrappedSessionStoreIterator(bytesIterator,
TimeFirstSessionKeySchema::from);
+ return sessionIterator(wrapped().fetchSessions(earliestSessionEndTime,
latestSessionEndTime));
}
@Override
public KeyValueIterator<Windowed<Bytes>, byte[]> findSessions(final Bytes
key,
final long
earliestSessionEndTime,
final long
latestSessionStartTime) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator = wrapped().fetch(
- key,
- earliestSessionEndTime,
- latestSessionStartTime
- );
- return new WrappedSessionStoreIterator(bytesIterator,
TimeFirstSessionKeySchema::from);
+ return sessionIterator(wrapped().fetch(key, earliestSessionEndTime,
latestSessionStartTime));
}
@Override
public KeyValueIterator<Windowed<Bytes>, byte[]>
backwardFindSessions(final Bytes key,
final long earliestSessionEndTime,
final long latestSessionStartTime) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().backwardFetch(
- key,
- earliestSessionEndTime,
- latestSessionStartTime
- );
- return new WrappedSessionStoreIterator(bytesIterator,
TimeFirstSessionKeySchema::from);
+ return sessionIterator(wrapped().backwardFetch(key,
earliestSessionEndTime, latestSessionStartTime));
}
@Override
@@ -98,13 +94,7 @@ public class RocksDBTimeOrderedSessionStore
final Bytes
keyTo,
final long
earliestSessionEndTime,
final long
latestSessionStartTime) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator = wrapped().fetch(
- keyFrom,
- keyTo,
- earliestSessionEndTime,
- latestSessionStartTime
- );
- return new WrappedSessionStoreIterator(bytesIterator,
TimeFirstSessionKeySchema::from);
+ return sessionIterator(wrapped().fetch(keyFrom, keyTo,
earliestSessionEndTime, latestSessionStartTime));
}
@Override
@@ -112,24 +102,19 @@ public class RocksDBTimeOrderedSessionStore
final Bytes keyTo,
final long earliestSessionEndTime,
final long latestSessionStartTime) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().backwardFetch(
- keyFrom,
- keyTo,
- earliestSessionEndTime,
- latestSessionStartTime
- );
- return new WrappedSessionStoreIterator(bytesIterator,
TimeFirstSessionKeySchema::from);
+ return sessionIterator(wrapped().backwardFetch(keyFrom, keyTo,
earliestSessionEndTime, latestSessionStartTime));
}
@Override
public byte[] fetchSession(final Bytes key,
final long sessionStartTime,
final long sessionEndTime) {
- return wrapped().fetchSession(
- key,
- sessionStartTime,
- sessionEndTime
- );
+ return wrapped().fetchSession(key, sessionStartTime, sessionEndTime);
+ }
+
+ // Shared by the live methods above and the ReadOnlyView below; only the
iterator's source differs.
+ private static KeyValueIterator<Windowed<Bytes>, byte[]>
sessionIterator(final KeyValueIterator<Bytes, byte[]> bytesIterator) {
+ return new WrappedSessionStoreIterator(bytesIterator,
TimeFirstSessionKeySchema::from);
}
@Override
@@ -161,4 +146,80 @@ public class RocksDBTimeOrderedSessionStore
public void put(final Windowed<Bytes> sessionKey, final byte[] aggregate) {
wrapped().put(sessionKey, aggregate);
}
+
+ @Override
+ public ReadOnlySessionStore<Bytes, byte[]> readOnly(final IsolationLevel
isolationLevel) {
+ Objects.requireNonNull(isolationLevel, "isolationLevel cannot be
null");
+ return new ReadOnlyView(isolationLevel);
+ }
+
+ /** Read view; reads go through the segmented store's isolation view, so
READ_COMMITTED hides staged writes. */
+ private final class ReadOnlyView implements ReadOnlySessionStore<Bytes,
byte[]> {
+
+ private final
AbstractRocksDBTimeOrderedSegmentedBytesStore.ReadOnlyView segmented;
+
+ ReadOnlyView(final IsolationLevel isolationLevel) {
+ this.segmented = wrapped().readOnly(isolationLevel);
+ }
+
+ @Override
+ public byte[] fetchSession(final Bytes key, final long
sessionStartTime, final long sessionEndTime) {
+ return segmented.fetchSession(key, sessionStartTime,
sessionEndTime);
+ }
+
+ @Override
+ public byte[] fetchSession(final Bytes key, final Instant
sessionStartTime, final Instant sessionEndTime) {
+ return fetchSession(key, sessionStartTime.toEpochMilli(),
sessionEndTime.toEpochMilli());
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> findSessions(final
Bytes key,
+ final
long earliestSessionEndTime,
+ final
long latestSessionStartTime) {
+ return sessionIterator(segmented.fetch(key,
earliestSessionEndTime, latestSessionStartTime));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]>
backwardFindSessions(final Bytes key,
+
final long earliestSessionEndTime,
+
final long latestSessionStartTime) {
+ return sessionIterator(segmented.backwardFetch(key,
earliestSessionEndTime, latestSessionStartTime));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> findSessions(final
Bytes keyFrom,
+ final
Bytes keyTo,
+ final
long earliestSessionEndTime,
+ final
long latestSessionStartTime) {
+ return sessionIterator(segmented.fetch(keyFrom, keyTo,
earliestSessionEndTime, latestSessionStartTime));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]>
backwardFindSessions(final Bytes keyFrom,
+
final Bytes keyTo,
+
final long earliestSessionEndTime,
+
final long latestSessionStartTime) {
+ return sessionIterator(segmented.backwardFetch(keyFrom, keyTo,
earliestSessionEndTime, latestSessionStartTime));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> fetch(final Bytes
key) {
+ return findSessions(key, 0, Long.MAX_VALUE);
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> backwardFetch(final
Bytes key) {
+ return backwardFindSessions(key, 0, Long.MAX_VALUE);
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> fetch(final Bytes
keyFrom, final Bytes keyTo) {
+ return findSessions(keyFrom, keyTo, 0, Long.MAX_VALUE);
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> backwardFetch(final
Bytes keyFrom, final Bytes keyTo) {
+ return backwardFindSessions(keyFrom, keyTo, 0, Long.MAX_VALUE);
+ }
+ }
}
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedWindowSegmentedBytesStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedWindowSegmentedBytesStore.java
index 249933df1ff..f69562524a9 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedWindowSegmentedBytesStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedWindowSegmentedBytesStore.java
@@ -17,6 +17,7 @@
package org.apache.kafka.streams.state.internals;
import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.state.KeyValueIterator;
@@ -47,8 +48,9 @@ public class RocksDBTimeOrderedWindowSegmentedBytesStore<S
extends Segment> exte
* This can be reused by both window store implementations (with and
without headers).
*/
class WindowKeySchemaIndexToBaseStoreIterator extends
IndexToBaseStoreIterator {
- WindowKeySchemaIndexToBaseStoreIterator(final KeyValueIterator<Bytes,
byte[]> indexIterator) {
- super(indexIterator);
+ WindowKeySchemaIndexToBaseStoreIterator(final KeyValueIterator<Bytes,
byte[]> indexIterator,
+ final IsolationLevel
isolationLevel) {
+ super(indexIterator, isolationLevel);
}
@Override
@@ -103,7 +105,8 @@ public class RocksDBTimeOrderedWindowSegmentedBytesStore<S
extends Segment> exte
}
@Override
- protected IndexToBaseStoreIterator getIndexToBaseStoreIterator(final
SegmentIterator<S> segmentIterator) {
- return new WindowKeySchemaIndexToBaseStoreIterator(segmentIterator);
+ protected IndexToBaseStoreIterator getIndexToBaseStoreIterator(final
SegmentIterator<S> segmentIterator,
+ final
IsolationLevel isolationLevel) {
+ return new WindowKeySchemaIndexToBaseStoreIterator(segmentIterator,
isolationLevel);
}
}
\ No newline at end of file
diff --git
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedWindowStore.java
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedWindowStore.java
index ebe1f4c5614..deec8374762 100644
---
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedWindowStore.java
+++
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimeOrderedWindowStore.java
@@ -16,21 +16,25 @@
*/
package org.apache.kafka.streams.state.internals;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.kstream.Windowed;
import org.apache.kafka.streams.processor.StateStore;
import org.apache.kafka.streams.processor.StateStoreContext;
+import org.apache.kafka.streams.query.Position;
import org.apache.kafka.streams.query.PositionBound;
import org.apache.kafka.streams.query.Query;
import org.apache.kafka.streams.query.QueryConfig;
import org.apache.kafka.streams.query.QueryResult;
import org.apache.kafka.streams.state.KeyValueIterator;
+import org.apache.kafka.streams.state.ReadOnlyWindowStore;
import org.apache.kafka.streams.state.TimestampedBytesStore;
import org.apache.kafka.streams.state.WindowStore;
import org.apache.kafka.streams.state.WindowStoreIterator;
import
org.apache.kafka.streams.state.internals.PrefixedWindowKeySchemas.TimeFirstWindowKeySchema;
+import java.time.Instant;
import java.util.Map;
import java.util.Objects;
@@ -98,20 +102,12 @@ public class RocksDBTimeOrderedWindowStore<S extends
Segment>
@Override
public WindowStoreIterator<byte[]> fetch(final Bytes key, final long
timeFrom, final long timeTo) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().fetch(key, timeFrom, timeTo);
- return new WindowStoreIteratorWrapper(bytesIterator,
- windowSize,
- TimeFirstWindowKeySchema::extractStoreTimestamp,
- TimeFirstWindowKeySchema::fromStoreBytesKey).valuesIterator();
+ return valuesIterator(wrapped().fetch(key, timeFrom, timeTo));
}
@Override
public WindowStoreIterator<byte[]> backwardFetch(final Bytes key, final
long timeFrom, final long timeTo) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().backwardFetch(key, timeFrom, timeTo);
- return new WindowStoreIteratorWrapper(bytesIterator,
- windowSize,
- TimeFirstWindowKeySchema::extractStoreTimestamp,
- TimeFirstWindowKeySchema::fromStoreBytesKey).valuesIterator();
+ return valuesIterator(wrapped().backwardFetch(key, timeFrom, timeTo));
}
@Override
@@ -119,11 +115,7 @@ public class RocksDBTimeOrderedWindowStore<S extends
Segment>
final Bytes keyTo,
final long timeFrom,
final long timeTo) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().fetch(keyFrom, keyTo, timeFrom, timeTo);
- return new WindowStoreIteratorWrapper(bytesIterator,
- windowSize,
- TimeFirstWindowKeySchema::extractStoreTimestamp,
- TimeFirstWindowKeySchema::fromStoreBytesKey).keyValueIterator();
+ return keyValueIterator(wrapped().fetch(keyFrom, keyTo, timeFrom,
timeTo));
}
@Override
@@ -131,43 +123,38 @@ public class RocksDBTimeOrderedWindowStore<S extends
Segment>
final Bytes
keyTo,
final long
timeFrom,
final long
timeTo) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().backwardFetch(keyFrom, keyTo, timeFrom, timeTo);
- return new WindowStoreIteratorWrapper(bytesIterator,
- windowSize,
- TimeFirstWindowKeySchema::extractStoreTimestamp,
- TimeFirstWindowKeySchema::fromStoreBytesKey).keyValueIterator();
+ return keyValueIterator(wrapped().backwardFetch(keyFrom, keyTo,
timeFrom, timeTo));
}
@Override
public KeyValueIterator<Windowed<Bytes>, byte[]> all() {
- final KeyValueIterator<Bytes, byte[]> bytesIterator = wrapped().all();
- return new WindowStoreIteratorWrapper(bytesIterator,
- windowSize,
- TimeFirstWindowKeySchema::extractStoreTimestamp,
- TimeFirstWindowKeySchema::fromStoreBytesKey).keyValueIterator();
+ return keyValueIterator(wrapped().all());
}
@Override
public KeyValueIterator<Windowed<Bytes>, byte[]> backwardAll() {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().backwardAll();
- return new WindowStoreIteratorWrapper(bytesIterator,
- windowSize,
- TimeFirstWindowKeySchema::extractStoreTimestamp,
- TimeFirstWindowKeySchema::fromStoreBytesKey).keyValueIterator();
+ return keyValueIterator(wrapped().backwardAll());
}
@Override
public KeyValueIterator<Windowed<Bytes>, byte[]> fetchAll(final long
timeFrom, final long timeTo) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().fetchAll(timeFrom, timeTo);
+ return keyValueIterator(wrapped().fetchAll(timeFrom, timeTo));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> backwardFetchAll(final
long timeFrom, final long timeTo) {
+ return keyValueIterator(wrapped().backwardFetchAll(timeFrom, timeTo));
+ }
+
+ // Shared by the live methods above and the ReadOnlyView below; only the
iterator's source differs.
+ private WindowStoreIterator<byte[]> valuesIterator(final
KeyValueIterator<Bytes, byte[]> bytesIterator) {
return new WindowStoreIteratorWrapper(bytesIterator,
windowSize,
TimeFirstWindowKeySchema::extractStoreTimestamp,
- TimeFirstWindowKeySchema::fromStoreBytesKey).keyValueIterator();
+ TimeFirstWindowKeySchema::fromStoreBytesKey).valuesIterator();
}
- @Override
- public KeyValueIterator<Windowed<Bytes>, byte[]> backwardFetchAll(final
long timeFrom, final long timeTo) {
- final KeyValueIterator<Bytes, byte[]> bytesIterator =
wrapped().backwardFetchAll(timeFrom, timeTo);
+ private KeyValueIterator<Windowed<Bytes>, byte[]> keyValueIterator(final
KeyValueIterator<Bytes, byte[]> bytesIterator) {
return new WindowStoreIteratorWrapper(bytesIterator,
windowSize,
TimeFirstWindowKeySchema::extractStoreTimestamp,
@@ -178,17 +165,83 @@ public class RocksDBTimeOrderedWindowStore<S extends
Segment>
return wrapped().hasIndex();
}
+ @Override
+ public ReadOnlyWindowStore<Bytes, byte[]> readOnly(final IsolationLevel
isolationLevel) {
+ Objects.requireNonNull(isolationLevel, "isolationLevel cannot be
null");
+ return new ReadOnlyView(isolationLevel);
+ }
+
+ /** Read view; reads go through the segmented store's isolation view, so
READ_COMMITTED hides staged writes. */
+ private final class ReadOnlyView implements ReadOnlyWindowStore<Bytes,
byte[]> {
+
+ private final
AbstractRocksDBTimeOrderedSegmentedBytesStore.ReadOnlyView segmented;
+
+ ReadOnlyView(final IsolationLevel isolationLevel) {
+ this.segmented = wrapped().readOnly(isolationLevel);
+ }
+
+ @Override
+ public byte[] fetch(final Bytes key, final long time) {
+ return
segmented.get(TimeFirstWindowKeySchema.toStoreKeyBinary(key, time, seqnum));
+ }
+
+ @Override
+ public WindowStoreIterator<byte[]> fetch(final Bytes key, final
Instant timeFrom, final Instant timeTo) {
+ return valuesIterator(segmented.fetch(key,
timeFrom.toEpochMilli(), timeTo.toEpochMilli()));
+ }
+
+ @Override
+ public WindowStoreIterator<byte[]> backwardFetch(final Bytes key,
final Instant timeFrom, final Instant timeTo) {
+ return valuesIterator(segmented.backwardFetch(key,
timeFrom.toEpochMilli(), timeTo.toEpochMilli()));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> fetch(final Bytes
keyFrom, final Bytes keyTo,
+ final Instant
timeFrom, final Instant timeTo) {
+ return keyValueIterator(segmented.fetch(keyFrom, keyTo,
timeFrom.toEpochMilli(), timeTo.toEpochMilli()));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> backwardFetch(final
Bytes keyFrom, final Bytes keyTo,
+ final
Instant timeFrom, final Instant timeTo) {
+ return keyValueIterator(segmented.backwardFetch(keyFrom, keyTo,
timeFrom.toEpochMilli(), timeTo.toEpochMilli()));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> fetchAll(final
Instant timeFrom, final Instant timeTo) {
+ return
keyValueIterator(segmented.fetchAll(timeFrom.toEpochMilli(),
timeTo.toEpochMilli()));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]>
backwardFetchAll(final Instant timeFrom, final Instant timeTo) {
+ return
keyValueIterator(segmented.backwardFetchAll(timeFrom.toEpochMilli(),
timeTo.toEpochMilli()));
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> all() {
+ return keyValueIterator(segmented.all());
+ }
+
+ @Override
+ public KeyValueIterator<Windowed<Bytes>, byte[]> backwardAll() {
+ return keyValueIterator(segmented.backwardAll());
+ }
+ }
+
@Override
public <R> QueryResult<R> query(final Query<R> query,
final PositionBound positionBound,
final QueryConfig config) {
+ final Position queryPosition = config.getIsolationLevel() ==
IsolationLevel.READ_COMMITTED
+ ? wrapped().getCommittedPosition()
+ : getPosition();
return StoreQueryUtils.handleBasicQueries(
query,
positionBound,
config,
this,
- getPosition(),
+ queryPosition,
stateStoreContext
);
}
diff --git
a/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractDualSchemaRocksDBSegmentedBytesStoreTest.java
b/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractDualSchemaRocksDBSegmentedBytesStoreTest.java
index 9d2ac218f85..3e51e20a35d 100644
---
a/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractDualSchemaRocksDBSegmentedBytesStoreTest.java
+++
b/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractDualSchemaRocksDBSegmentedBytesStoreTest.java
@@ -17,6 +17,7 @@
package org.apache.kafka.streams.state.internals;
import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.IsolationLevel;
import org.apache.kafka.common.Metric;
import org.apache.kafka.common.MetricName;
import org.apache.kafka.common.header.Headers;
@@ -25,6 +26,7 @@ import org.apache.kafka.common.header.internals.RecordHeaders;
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.record.TimestampType;
import org.apache.kafka.common.record.internal.RecordBatch;
+import org.apache.kafka.common.serialization.LongDeserializer;
import org.apache.kafka.common.serialization.LongSerializer;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.common.utils.Bytes;
@@ -1682,6 +1684,120 @@ public abstract class
AbstractDualSchemaRocksDBSegmentedBytesStoreTest {
return Set.of(Objects.requireNonNull(windowDir.list()));
}
+ @Test
+ public void readCommittedShouldHideStagedWritesUntilCommit() {
+ initTransactional();
+ final String key = "a";
+ final Bytes storeKey = serializeKey(new Windowed<>(key, windows[0]));
+
+ bytesStore.put(storeKey, serializeValue(10L));
+ bytesStore.commit(Map.of());
+
+ // Stage an overwrite that is not yet committed.
+ bytesStore.put(storeKey, serializeValue(50L));
+
+ // Point get: READ_UNCOMMITTED sees the staged value; READ_COMMITTED
sees the last committed value.
+ assertEquals(50L,
deserializeValue(timeOrdered().readOnly(IsolationLevel.READ_UNCOMMITTED).get(storeKey)));
+ assertEquals(10L,
deserializeValue(timeOrdered().readOnly(IsolationLevel.READ_COMMITTED).get(storeKey)));
+
+ // Range fetch (exercises SegmentIterator's isolation routing) must
respect the same visibility.
+ assertEquals(
+ List.of(KeyValue.pair(new Windowed<>(key, windows[0]), 50L)),
+
toListAndCloseIterator(timeOrdered().readOnly(IsolationLevel.READ_UNCOMMITTED).fetch(Bytes.wrap(key.getBytes()),
0, windows[0].start())));
+ assertEquals(
+ List.of(KeyValue.pair(new Windowed<>(key, windows[0]), 10L)),
+
toListAndCloseIterator(timeOrdered().readOnly(IsolationLevel.READ_COMMITTED).fetch(Bytes.wrap(key.getBytes()),
0, windows[0].start())));
+ }
+
+ @Test
+ public void commitShouldMakeStagedWritesVisibleToReadCommitted() {
+ initTransactional();
+ final Bytes storeKey = serializeKey(new Windowed<>("a", windows[0]));
+
+ bytesStore.put(storeKey, serializeValue(50L));
+ // Before commit, READ_COMMITTED cannot see the staged write.
+
assertNull(timeOrdered().readOnly(IsolationLevel.READ_COMMITTED).get(storeKey));
+
+ bytesStore.commit(Map.of());
+
+ // After commit, both isolation levels converge on the now-durable
value.
+ assertEquals(50L,
deserializeValue(timeOrdered().readOnly(IsolationLevel.READ_COMMITTED).get(storeKey)));
+ assertEquals(50L,
deserializeValue(timeOrdered().readOnly(IsolationLevel.READ_UNCOMMITTED).get(storeKey)));
+ }
+
+ @Test
+ public void stagedWritesShouldNotAdvanceCommittedPositionUntilCommit() {
+ initTransactional();
+
+ context.setRecordContext(new ProcessorRecordContext(0, 1L, 0, "input",
new RecordHeaders()));
+ bytesStore.put(serializeKey(new Windowed<>("a", windows[0])),
serializeValue(10L));
+ bytesStore.commit(Map.of());
+
+ context.setRecordContext(new ProcessorRecordContext(0, 5L, 0, "input",
new RecordHeaders()));
+ bytesStore.put(serializeKey(new Windowed<>("b", windows[0])),
serializeValue(20L));
+
+ // committed position stays at the last committed offset; the merged
position reflects the staged write.
+ assertEquals(Map.of(0, 1L),
bytesStore.getCommittedPosition().getPartitionPositions("input"));
+ assertEquals(Map.of(0, 5L),
bytesStore.getPosition().getPartitionPositions("input"));
+
+ bytesStore.commit(Map.of());
+ assertEquals(Map.of(0, 5L),
bytesStore.getCommittedPosition().getPartitionPositions("input"));
+ }
+
+ @Test
+ public void approximateNumUncommittedBytesShouldReflectStagedWrites() {
+ initTransactional();
+ assertEquals(0, bytesStore.approximateNumUncommittedBytes());
+
+ bytesStore.put(serializeKey(new Windowed<>("a", windows[0])),
serializeValue(10L));
+ final long staged = bytesStore.approximateNumUncommittedBytes();
+ assertTrue(staged > 0, "a staged write should contribute uncommitted
bytes");
+
+ bytesStore.commit(Map.of());
+ // commit flushes the staged data, so uncommitted bytes drop below the
staged size (a small residual remains, as for any RocksDBStore).
+ assertTrue(bytesStore.approximateNumUncommittedBytes() < staged,
"commit should flush staged writes");
+ }
+
+ @Test
+ public void
nonTransactionalStoreShouldReadIdenticallyAcrossIsolationLevels() {
+ // before() already initialised the store under a non-transactional
context.
+ final Bytes storeKey = serializeKey(new Windowed<>("a", windows[0]));
+ bytesStore.put(storeKey, serializeValue(10L));
+
+ assertEquals(10L,
deserializeValue(timeOrdered().readOnly(IsolationLevel.READ_UNCOMMITTED).get(storeKey)));
+ assertEquals(10L,
deserializeValue(timeOrdered().readOnly(IsolationLevel.READ_COMMITTED).get(storeKey)));
+ }
+
+ @SuppressWarnings("unchecked")
+ private AbstractRocksDBTimeOrderedSegmentedBytesStore<KeyValueSegment>
timeOrdered() {
+ return
(AbstractRocksDBTimeOrderedSegmentedBytesStore<KeyValueSegment>) bytesStore;
+ }
+
+ private void initTransactional() {
+ // Re-open under a transactional EOS context so writes stage until
commit().
+ bytesStore.close();
+ bytesStore = getBytesStore();
+ context = getTransactionalEOSProcessorContext();
+ bytesStore.init(context, bytesStore);
+ }
+
+ private InternalMockProcessorContext<?, ?>
getTransactionalEOSProcessorContext() {
+ final Properties streamsProps = StreamsTestUtils.getStreamsConfig();
+ streamsProps.setProperty(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2);
+
streamsProps.setProperty(StreamsConfig.TRANSACTIONAL_STATE_STORES_CONFIG,
"true");
+ return new InternalMockProcessorContext<>(
+ stateDir,
+ Serdes.String(),
+ Serdes.Long(),
+ new MockRecordCollector(),
+ new ThreadCache(new LogContext("testCache "), 0, new
MockStreamsMetrics(new Metrics())),
+ new StreamsConfig(streamsProps));
+ }
+
+ private Long deserializeValue(final byte[] value) {
+ return new LongDeserializer().deserialize("", value);
+ }
+
private Bytes serializeKey(final Windowed<String> key) {
return serializeKey(key, false);
}