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 704fcb26554 KAFKA-20768: Add transactional and IQ isolation to RocksDB 
window and session stores (#22754)
704fcb26554 is described below

commit 704fcb265543cad94e04eac9d637133735e2298a
Author: Nick Telford <[email protected]>
AuthorDate: Tue Jul 14 14:02:19 2026 +0100

    KAFKA-20768: Add transactional and IQ isolation to RocksDB window and 
session stores (#22754)
    
    The persistent RocksDB window and session stores were the last KIP-892
    state stores without transactional support or isolation-level
    interactive queries: a `READ_COMMITTED` IQ against them returned writes
    that were still staged in an uncommitted transaction.
    
    Because each segment is itself a `RocksDBStore`, the write and commit
    path already stages writes and flushes them per segment. This change
    fills the two remaining gaps in `AbstractRocksDBSegmentedBytesStore`:
    
    - **Staged position** — `put` now stages the `Position` of an
    uncommitted write in a `pendingPosition`, which is 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 `RocksDBWindowStore`/`RocksDBSessionStore`
    so their `readOnly(...)` views (and `StoreQueryUtils` dispatch) hide
    uncommitted writes under READ_COMMITTED.
    
    Reads follow the same convention as `RocksDBStore` and the in-memory
    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 to read committed data directly. Consequently no new isolation
    surface is added to the `SegmentedBytesStore` interface — the isolation
    methods are private utilities shared between the vanilla methods and the
    read view — and `RocksDBWindowStore`/`RocksDBSessionStore` wrap the
    concrete `AbstractRocksDBSegmentedBytesStore` so they reach that view
    without casting.
    
    This covers the standard and timestamped window stores, the session
    store, and their `WithHeaders` variants by inheritance. The time-ordered
    / dual-schema segmented stores (used by stream-stream joins) are out of
    scope and remain non-transactional; they will be addressed as a
    follow-up.
    
    Testing: `AbstractRocksDBSegmentedBytesStoreTest` gains transactional
    cases parameterised over both the window and session key schemas (so
    they run against `RocksDBSegmentedBytesStore`,
    `RocksDBTimestampedSegmentedBytesStore`, and the with-headers variant),
    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. The end-to-end
    IQv1/IQv2 isolation-level integration tests live on a separate branch
    and pass with this change.
    
    Reviewers: kkgounder95-code (github:kkgounder95-code), Bill Bejeck
     <[email protected]>
---
 .../AbstractRocksDBSegmentedBytesStore.java        | 188 +++++++++++++++------
 .../state/internals/RocksDBSessionStore.java       | 132 +++++++++++----
 .../internals/RocksDBSessionStoreWithHeaders.java  |   2 +-
 .../streams/state/internals/RocksDBStore.java      |   1 +
 .../internals/RocksDBTimestampedWindowStore.java   |   2 +-
 .../RocksDBTimestampedWindowStoreWithHeaders.java  |   2 +-
 .../state/internals/RocksDBWindowStore.java        | 109 ++++++++++--
 .../streams/state/internals/SegmentIterator.java   |  23 ++-
 .../AbstractRocksDBSegmentedBytesStoreTest.java    | 118 +++++++++++++
 .../PlainToHeadersWindowStoreAdapterTest.java      |   2 +-
 .../RocksDBSessionStoreWithHeadersTest.java        |   2 +-
 ...cksDBTimestampedWindowStoreWithHeadersTest.java |   2 +-
 ...TimestampedToHeadersWindowStoreAdapterTest.java |   2 +-
 13 files changed, 473 insertions(+), 112 deletions(-)

diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStore.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStore.java
index 9357583ec53..0d3c25783ae 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStore.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStore.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;
@@ -44,6 +45,7 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import static 
org.apache.kafka.streams.StreamsConfig.InternalConfig.IQ_CONSISTENCY_OFFSET_VECTOR_ENABLED;
 import static 
org.apache.kafka.streams.processor.internals.ProcessorContextUtils.asInternalProcessorContext;
@@ -61,6 +63,9 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
     private long observedStreamTime = ConsumerRecord.NO_TIMESTAMP;
     private boolean consistencyEnabled = false;
     private 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;
 
     AbstractRocksDBSegmentedBytesStore(final String name,
@@ -82,20 +87,21 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
     public KeyValueIterator<Bytes, byte[]> fetch(final Bytes key,
                                                  final long from,
                                                  final long to) {
-        return fetch(key, from, to, true);
+        return fetch(key, from, to, true, null);
     }
 
     @Override
     public KeyValueIterator<Bytes, byte[]> backwardFetch(final Bytes key,
                                                          final long from,
                                                          final long to) {
-        return fetch(key, from, to, false);
+        return fetch(key, from, to, false, null);
     }
 
-    KeyValueIterator<Bytes, byte[]> fetch(final Bytes key,
-                                          final long from,
-                                          final long to,
-                                          final boolean forward) {
+    private KeyValueIterator<Bytes, byte[]> fetch(final Bytes key,
+                                                  final long from,
+                                                  final long to,
+                                                  final boolean forward,
+                                                  final IsolationLevel 
isolationLevel) {
         final long actualFrom = getActualFrom(from);
 
         if (keySchema instanceof WindowKeySchema && to < actualFrom) {
@@ -113,7 +119,8 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
                 keySchema.hasNextCondition(key, key, actualFrom, to, forward),
                 binaryFrom,
                 binaryTo,
-                forward);
+                forward,
+                isolationLevel);
     }
 
     private long getActualFrom(final long from) {
@@ -125,7 +132,7 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
                                                  final Bytes keyTo,
                                                  final long from,
                                                  final long to) {
-        return fetch(keyFrom, keyTo, from, to, true);
+        return fetch(keyFrom, keyTo, from, to, true, null);
     }
 
     @Override
@@ -133,14 +140,15 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
                                                          final Bytes keyTo,
                                                          final long from,
                                                          final long to) {
-        return fetch(keyFrom, keyTo, from, to, false);
+        return fetch(keyFrom, keyTo, from, to, false, null);
     }
 
-    KeyValueIterator<Bytes, byte[]> fetch(final Bytes keyFrom,
-                                          final Bytes keyTo,
-                                          final long from,
-                                          final long to,
-                                          final boolean forward) {
+    private 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, " +
@@ -166,59 +174,49 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
                 keySchema.hasNextCondition(keyFrom, keyTo, actualFrom, to, 
forward),
                 binaryFrom,
                 binaryTo,
-                forward);
+                forward,
+                isolationLevel);
     }
 
     @Override
     public KeyValueIterator<Bytes, byte[]> all() {
-        final long actualFrom = getActualFrom(0);
-        final List<S> searchSpace = keySchema.segmentsToSearch(segments, 
actualFrom, Long.MAX_VALUE, true);
-
-        return new SegmentIterator<>(
-                searchSpace.iterator(),
-                keySchema.hasNextCondition(null, null, actualFrom, 
Long.MAX_VALUE, true),
-                null,
-                null,
-                true);
+        return all(true, null);
     }
 
     @Override
     public KeyValueIterator<Bytes, byte[]> backwardAll() {
-        final long actualFrom = getActualFrom(0);
+        return all(false, null);
+    }
 
-        final List<S> searchSpace = keySchema.segmentsToSearch(segments, 
actualFrom, Long.MAX_VALUE, false);
+    private KeyValueIterator<Bytes, byte[]> all(final boolean forward, final 
IsolationLevel isolationLevel) {
+        final long actualFrom = getActualFrom(0);
+        final List<S> searchSpace = keySchema.segmentsToSearch(segments, 
actualFrom, Long.MAX_VALUE, forward);
 
         return new SegmentIterator<>(
                 searchSpace.iterator(),
-                keySchema.hasNextCondition(null, null, actualFrom, 
Long.MAX_VALUE, false),
+                keySchema.hasNextCondition(null, null, actualFrom, 
Long.MAX_VALUE, forward),
                 null,
                 null,
-                false);
+                forward,
+                isolationLevel);
     }
 
     @Override
     public KeyValueIterator<Bytes, byte[]> fetchAll(final long timeFrom,
                                                     final long timeTo) {
-        final long actualFrom = getActualFrom(timeFrom);
-
-        if (keySchema instanceof WindowKeySchema && timeTo < actualFrom) {
-            LOG.debug("Returning no records for as timeTo ({}) < actualFrom 
({}) ", timeTo, actualFrom);
-            return KeyValueIterators.emptyIterator();
-        }
-
-        final List<S> searchSpace = segments.segments(actualFrom, timeTo, 
true);
-
-        return new SegmentIterator<>(
-                searchSpace.iterator(),
-                keySchema.hasNextCondition(null, null, actualFrom, timeTo, 
true),
-                null,
-                null,
-                true);
+        return fetchAll(timeFrom, timeTo, true, null);
     }
 
     @Override
     public KeyValueIterator<Bytes, byte[]> backwardFetchAll(final long 
timeFrom,
                                                             final long timeTo) 
{
+        return fetchAll(timeFrom, timeTo, false, null);
+    }
+
+    private KeyValueIterator<Bytes, byte[]> fetchAll(final long timeFrom,
+                                                     final long timeTo,
+                                                     final boolean forward,
+                                                     final IsolationLevel 
isolationLevel) {
         final long actualFrom = getActualFrom(timeFrom);
 
         if (keySchema instanceof WindowKeySchema && timeTo < actualFrom) {
@@ -226,14 +224,15 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
             return KeyValueIterators.emptyIterator();
         }
 
-        final List<S> searchSpace = segments.segments(actualFrom, timeTo, 
false);
+        final List<S> searchSpace = segments.segments(actualFrom, timeTo, 
forward);
 
         return new SegmentIterator<>(
                 searchSpace.iterator(),
-                keySchema.hasNextCondition(null, null, actualFrom, timeTo, 
false),
+                keySchema.hasNextCondition(null, null, actualFrom, timeTo, 
forward),
                 null,
                 null,
-                false);
+                forward,
+                isolationLevel);
     }
 
     @Override
@@ -258,7 +257,8 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
             expiredRecordSensor.record(1.0d, 
internalProcessorContext.currentSystemTimeMs());
         } 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);
                 segment.put(key, value);
             }
         }
@@ -266,6 +266,10 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
 
     @Override
     public byte[] get(final Bytes key) {
+        return get(key, null);
+    }
+
+    private byte[] get(final Bytes key, final IsolationLevel isolationLevel) {
         final long timestampFromKey = keySchema.segmentTimestamp(key);
         // check if timestamp is expired
         if (timestampFromKey < observedStreamTime - retentionPeriod + 1) {
@@ -277,7 +281,14 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
         if (segment == null) {
             return null;
         }
-        return segment.get(key);
+        // Live read for the vanilla path; interactive queries read through 
the segment's isolation view.
+        return isolationLevel == null ? segment.get(key) : 
segment.readOnly(isolationLevel).get(key);
+    }
+
+    /** An isolation-bound read view; the level is resolved once here and 
reads share the store's private utilities. */
+    ReadOnlyView readOnly(final IsolationLevel isolationLevel) {
+        Objects.requireNonNull(isolationLevel, "isolationLevel cannot be 
null");
+        return new ReadOnlyView(this, isolationLevel);
     }
 
     @Override
@@ -301,6 +312,7 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
 
         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
@@ -316,11 +328,23 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
                 stateStoreContext.appConfigs(),
                 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")
@@ -347,6 +371,8 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
     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
@@ -415,6 +441,68 @@ public class AbstractRocksDBSegmentedBytesStore<S extends 
Segment> implements Se
 
     @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;
     }
-}
\ No newline at end of file
+
+    Position getCommittedPosition() {
+        if (transactional) {
+            synchronized (position) {
+                return position.copy();
+            }
+        }
+        return position;
+    }
+
+    /** Read view bound to an {@link IsolationLevel}, delegating to the 
store's private read utilities. */
+    static final class ReadOnlyView {
+        private final AbstractRocksDBSegmentedBytesStore<?> store;
+        private final IsolationLevel isolationLevel;
+
+        private ReadOnlyView(final AbstractRocksDBSegmentedBytesStore<?> 
store, final IsolationLevel isolationLevel) {
+            this.store = store;
+            this.isolationLevel = isolationLevel;
+        }
+
+        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[]> all() {
+            return store.all(true, isolationLevel);
+        }
+
+        KeyValueIterator<Bytes, byte[]> backwardAll() {
+            return store.all(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);
+        }
+
+        byte[] get(final Bytes key) {
+            return store.get(key, isolationLevel);
+        }
+    }
+}
diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBSessionStore.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBSessionStore.java
index 80af230c300..da52bac22be 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBSessionStore.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBSessionStore.java
@@ -16,25 +16,31 @@
  */
 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 java.time.Instant;
+import java.util.Objects;
+
 
 public class RocksDBSessionStore
-    extends WrappedStateStore<SegmentedBytesStore, Object, Object>
+    extends WrappedStateStore<AbstractRocksDBSegmentedBytesStore<?>, Object, 
Object>
     implements SessionStore<Bytes, byte[]> {
 
     private StateStoreContext stateStoreContext;
 
-    RocksDBSessionStore(final SegmentedBytesStore bytesStore) {
+    RocksDBSessionStore(final AbstractRocksDBSegmentedBytesStore<?> 
bytesStore) {
         super(bytesStore);
     }
 
@@ -49,12 +55,15 @@ public class RocksDBSessionStore
                                     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
         );
     }
@@ -63,24 +72,14 @@ public class RocksDBSessionStore
     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);
+        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);
+        return sessionIterator(wrapped().backwardFetch(key, 
earliestSessionEndTime, latestSessionStartTime));
     }
 
     @Override
@@ -88,13 +87,7 @@ public class RocksDBSessionStore
                                                                   final Bytes 
keyTo,
                                                                   final long 
earliestSessionEndTime,
                                                                   final long 
latestSessionStartTime) {
-        final KeyValueIterator<Bytes, byte[]> bytesIterator = wrapped().fetch(
-            keyFrom,
-            keyTo,
-            earliestSessionEndTime,
-            latestSessionStartTime
-        );
-        return new WrappedSessionStoreIterator(bytesIterator);
+        return sessionIterator(wrapped().fetch(keyFrom, keyTo, 
earliestSessionEndTime, latestSessionStartTime));
     }
 
     @Override
@@ -102,24 +95,19 @@ public class RocksDBSessionStore
                                                                           
final Bytes keyTo,
                                                                           
final long earliestSessionEndTime,
                                                                           
final long latestSessionStartTime) {
-        final KeyValueIterator<Bytes, byte[]> bytesIterator = 
wrapped().backwardFetch(
-            keyFrom,
-            keyTo,
-            earliestSessionEndTime,
-            latestSessionStartTime
-        );
-        return new WrappedSessionStoreIterator(bytesIterator);
+        return sessionIterator(wrapped().backwardFetch(keyFrom, keyTo, 
earliestSessionEndTime, latestSessionStartTime));
     }
 
     @Override
     public byte[] fetchSession(final Bytes key,
                                final long earliestSessionEndTime,
                                final long latestSessionStartTime) {
-        return wrapped().get(SessionKeySchema.toBinary(
-            key,
-            earliestSessionEndTime,
-            latestSessionStartTime
-        ));
+        return wrapped().get(SessionKeySchema.toBinary(key, 
earliestSessionEndTime, latestSessionStartTime));
+    }
+
+    // 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);
     }
 
     @Override
@@ -151,4 +139,80 @@ public class RocksDBSessionStore
     public void put(final Windowed<Bytes> sessionKey, final byte[] aggregate) {
         wrapped().put(SessionKeySchema.toBinary(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 AbstractRocksDBSegmentedBytesStore.ReadOnlyView 
segmented;
+
+        ReadOnlyView(final IsolationLevel isolationLevel) {
+            this.segmented = wrapped().readOnly(isolationLevel);
+        }
+
+        @Override
+        public byte[] fetchSession(final Bytes key, final long 
earliestSessionEndTime, final long latestSessionStartTime) {
+            return segmented.get(SessionKeySchema.toBinary(key, 
earliestSessionEndTime, latestSessionStartTime));
+        }
+
+        @Override
+        public byte[] fetchSession(final Bytes key, final Instant 
earliestSessionEndTime, final Instant latestSessionStartTime) {
+            return fetchSession(key, earliestSessionEndTime.toEpochMilli(), 
latestSessionStartTime.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/RocksDBSessionStoreWithHeaders.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBSessionStoreWithHeaders.java
index 7e171ba952a..91fd584523d 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBSessionStoreWithHeaders.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBSessionStoreWithHeaders.java
@@ -37,7 +37,7 @@ import org.apache.kafka.streams.state.HeadersBytesStore;
  */
 class RocksDBSessionStoreWithHeaders extends RocksDBSessionStore implements 
HeadersBytesStore {
 
-    RocksDBSessionStoreWithHeaders(final SegmentedBytesStore bytesStore) {
+    RocksDBSessionStoreWithHeaders(final AbstractRocksDBSegmentedBytesStore<?> 
bytesStore) {
         super(bytesStore);
     }
 
diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBStore.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBStore.java
index e7b2eef3a4c..398ce2f88d0 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBStore.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBStore.java
@@ -756,6 +756,7 @@ public class RocksDBStore implements KeyValueStore<Bytes, 
byte[]>, BatchWritingS
 
     @Override
     public ReadOnlyKeyValueStore<Bytes, byte[]> readOnly(final IsolationLevel 
isolationLevel) {
+        validateStoreOpen();
         return new ReadOnlyView(dbAccessor.readOnly(isolationLevel));
     }
 
diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStore.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStore.java
index b96748eed1a..f6d3ef1017d 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStore.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStore.java
@@ -20,7 +20,7 @@ import org.apache.kafka.streams.state.TimestampedBytesStore;
 
 class RocksDBTimestampedWindowStore extends RocksDBWindowStore implements 
TimestampedBytesStore {
 
-    RocksDBTimestampedWindowStore(final SegmentedBytesStore bytesStore,
+    RocksDBTimestampedWindowStore(final AbstractRocksDBSegmentedBytesStore<?> 
bytesStore,
                                   final boolean retainDuplicates,
                                   final long windowSize) {
         super(bytesStore, retainDuplicates, windowSize);
diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStoreWithHeaders.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStoreWithHeaders.java
index 37b1994b02f..fa22aa50907 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStoreWithHeaders.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStoreWithHeaders.java
@@ -47,7 +47,7 @@ import org.apache.kafka.streams.state.TimestampedBytesStore;
  */
 class RocksDBTimestampedWindowStoreWithHeaders extends RocksDBWindowStore 
implements TimestampedBytesStore, HeadersBytesStore {
 
-    RocksDBTimestampedWindowStoreWithHeaders(final SegmentedBytesStore 
bytesStore,
+    RocksDBTimestampedWindowStoreWithHeaders(final 
AbstractRocksDBSegmentedBytesStore<?> bytesStore,
                                              final boolean retainDuplicates,
                                              final long windowSize) {
         super(bytesStore, retainDuplicates, windowSize);
diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBWindowStore.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBWindowStore.java
index 61823c3f261..ede47b4b76a 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBWindowStore.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBWindowStore.java
@@ -16,20 +16,26 @@
  */
 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.ReadOnlyWindowStore;
 import org.apache.kafka.streams.state.WindowStore;
 import org.apache.kafka.streams.state.WindowStoreIterator;
 
+import java.time.Instant;
+import java.util.Objects;
+
 public class RocksDBWindowStore
-    extends WrappedStateStore<SegmentedBytesStore, Object, Object>
+    extends WrappedStateStore<AbstractRocksDBSegmentedBytesStore<?>, Object, 
Object>
     implements WindowStore<Bytes, byte[]> {
 
     private final boolean retainDuplicates;
@@ -39,7 +45,7 @@ public class RocksDBWindowStore
 
     private StateStoreContext stateStoreContext;
 
-    RocksDBWindowStore(final SegmentedBytesStore bytesStore,
+    RocksDBWindowStore(final AbstractRocksDBSegmentedBytesStore<?> bytesStore,
                        final boolean retainDuplicates,
                        final long windowSize) {
         super(bytesStore);
@@ -69,14 +75,12 @@ public class RocksDBWindowStore
 
     @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).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).valuesIterator();
+        return valuesIterator(wrapped().backwardFetch(key, timeFrom, timeTo));
     }
 
     @Override
@@ -84,8 +88,7 @@ public class RocksDBWindowStore
                                                            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).keyValueIterator();
+        return keyValueIterator(wrapped().fetch(keyFrom, keyTo, timeFrom, 
timeTo));
     }
 
     @Override
@@ -93,45 +96,115 @@ public class RocksDBWindowStore
                                                                    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).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).keyValueIterator();
+        return keyValueIterator(wrapped().all());
     }
 
     @Override
     public KeyValueIterator<Windowed<Bytes>, byte[]> backwardAll() {
-        final KeyValueIterator<Bytes, byte[]> bytesIterator = 
wrapped().backwardAll();
-        return new WindowStoreIteratorWrapper(bytesIterator, 
windowSize).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 new WindowStoreIteratorWrapper(bytesIterator, 
windowSize).keyValueIterator();
+        return keyValueIterator(wrapped().fetchAll(timeFrom, timeTo));
     }
 
     @Override
     public KeyValueIterator<Windowed<Bytes>, byte[]> backwardFetchAll(final 
long timeFrom, final long timeTo) {
-        final KeyValueIterator<Bytes, byte[]> bytesIterator = 
wrapped().backwardFetchAll(timeFrom, 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).valuesIterator();
+    }
+
+    private KeyValueIterator<Windowed<Bytes>, byte[]> keyValueIterator(final 
KeyValueIterator<Bytes, byte[]> bytesIterator) {
         return new WindowStoreIteratorWrapper(bytesIterator, 
windowSize).keyValueIterator();
     }
 
+    @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 AbstractRocksDBSegmentedBytesStore.ReadOnlyView 
segmented;
+
+        ReadOnlyView(final IsolationLevel isolationLevel) {
+            this.segmented = wrapped().readOnly(isolationLevel);
+        }
+
+        @Override
+        public byte[] fetch(final Bytes key, final long time) {
+            return segmented.get(WindowKeySchema.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/main/java/org/apache/kafka/streams/state/internals/SegmentIterator.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/SegmentIterator.java
index a0c52db873b..d45e950b535 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/SegmentIterator.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/SegmentIterator.java
@@ -16,10 +16,12 @@
  */
 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.KeyValue;
 import org.apache.kafka.streams.errors.InvalidStateStoreException;
 import org.apache.kafka.streams.state.KeyValueIterator;
+import org.apache.kafka.streams.state.ReadOnlyKeyValueStore;
 
 import java.util.Iterator;
 import java.util.NoSuchElementException;
@@ -32,6 +34,9 @@ public class SegmentIterator<S extends Segment> implements 
KeyValueIterator<Byte
     private final Bytes from;
     private final Bytes to;
     private final boolean forward;
+    // Non-null only for interactive-query iteration, where reads go through 
the segment's isolation view;
+    // null means read the segment live (the owner's own path during 
processing).
+    private final IsolationLevel isolationLevel;
     protected final Iterator<S> segments;
     protected final HasNextCondition hasNextCondition;
 
@@ -43,11 +48,21 @@ public class SegmentIterator<S extends Segment> implements 
KeyValueIterator<Byte
                     final Bytes from,
                     final Bytes to,
                     final boolean forward) {
+        this(segments, hasNextCondition, from, to, forward, null);
+    }
+
+    SegmentIterator(final Iterator<S> segments,
+                    final HasNextCondition hasNextCondition,
+                    final Bytes from,
+                    final Bytes to,
+                    final boolean forward,
+                    final IsolationLevel isolationLevel) {
         this.segments = segments;
         this.hasNextCondition = hasNextCondition;
         this.from = from;
         this.to = to;
         this.forward = forward;
+        this.isolationLevel = isolationLevel;
     }
 
     @Override
@@ -74,10 +89,12 @@ public class SegmentIterator<S extends Segment> implements 
KeyValueIterator<Byte
             close();
             currentSegment = segments.next();
             try {
-                if (forward) {
-                    currentIterator = currentSegment.range(from, to);
+                if (isolationLevel == null) {
+                    currentIterator = forward ? currentSegment.range(from, to) 
: currentSegment.reverseRange(from, to);
                 } else {
-                    currentIterator = currentSegment.reverseRange(from, to);
+                    // Interactive query: read through the segment's isolation 
view.
+                    final ReadOnlyKeyValueStore<Bytes, byte[]> segmentView = 
currentSegment.readOnly(isolationLevel);
+                    currentIterator = forward ? segmentView.range(from, to) : 
segmentView.reverseRange(from, to);
                 }
             } catch (final InvalidStateStoreException e) {
                 // segment may have been closed so we ignore it.
diff --git 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStoreTest.java
 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStoreTest.java
index f1712f2bf90..acd6d219cc9 100644
--- 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStoreTest.java
+++ 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractRocksDBSegmentedBytesStoreTest.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;
@@ -26,6 +27,7 @@ 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.Deserializer;
+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.serialization.Serializer;
@@ -94,6 +96,7 @@ import static org.hamcrest.Matchers.hasEntry;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -1006,6 +1009,121 @@ public abstract class 
AbstractRocksDBSegmentedBytesStoreTest<S extends Segment>
         return Set.of(Objects.requireNonNull(windowDir.list()));
     }
 
+    @ParameterizedTest
+    @MethodSource("getKeySchemas")
+    public void readCommittedShouldHideStagedWritesUntilCommit(final 
SegmentedBytesStore.KeySchema schema) {
+        initTransactional(schema);
+        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(bytesStore.readOnly(IsolationLevel.READ_UNCOMMITTED).get(storeKey)));
+        assertEquals(10L, 
deserializeValue(bytesStore.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(bytesStore.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(bytesStore.readOnly(IsolationLevel.READ_COMMITTED).fetch(Bytes.wrap(key.getBytes()),
 0, windows[0].start())));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getKeySchemas")
+    public void commitShouldMakeStagedWritesVisibleToReadCommitted(final 
SegmentedBytesStore.KeySchema schema) {
+        initTransactional(schema);
+        final Bytes storeKey = serializeKey(new Windowed<>("a", windows[0]));
+
+        bytesStore.put(storeKey, serializeValue(50L));
+        // Before commit, READ_COMMITTED cannot see the staged write.
+        
assertNull(bytesStore.readOnly(IsolationLevel.READ_COMMITTED).get(storeKey));
+
+        bytesStore.commit(Map.of());
+
+        // After commit, both isolation levels converge on the now-durable 
value.
+        assertEquals(50L, 
deserializeValue(bytesStore.readOnly(IsolationLevel.READ_COMMITTED).get(storeKey)));
+        assertEquals(50L, 
deserializeValue(bytesStore.readOnly(IsolationLevel.READ_UNCOMMITTED).get(storeKey)));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getKeySchemas")
+    public void stagedWritesShouldNotAdvanceCommittedPositionUntilCommit(final 
SegmentedBytesStore.KeySchema schema) {
+        initTransactional(schema);
+
+        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"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getKeySchemas")
+    public void approximateNumUncommittedBytesShouldReflectStagedWrites(final 
SegmentedBytesStore.KeySchema schema) {
+        initTransactional(schema);
+        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");
+    }
+
+    @ParameterizedTest
+    @MethodSource("getKeySchemas")
+    public void 
nonTransactionalStoreShouldReadIdenticallyAcrossIsolationLevels(final 
SegmentedBytesStore.KeySchema schema) {
+        before(schema);
+        final Bytes storeKey = serializeKey(new Windowed<>("a", windows[0]));
+        bytesStore.put(storeKey, serializeValue(10L));
+
+        assertEquals(10L, 
deserializeValue(bytesStore.readOnly(IsolationLevel.READ_UNCOMMITTED).get(storeKey)));
+        assertEquals(10L, 
deserializeValue(bytesStore.readOnly(IsolationLevel.READ_COMMITTED).get(storeKey)));
+    }
+
+    private void initTransactional(final SegmentedBytesStore.KeySchema schema) 
{
+        before(schema);
+        // 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) {
         final StateSerdes<String, Long> stateSerdes = 
StateSerdes.withBuiltinTypes("dummy", String.class, Long.class);
         if (schema instanceof SessionKeySchema) {
diff --git 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/PlainToHeadersWindowStoreAdapterTest.java
 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/PlainToHeadersWindowStoreAdapterTest.java
index 6c3400c344e..24bc67dc00e 100644
--- 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/PlainToHeadersWindowStoreAdapterTest.java
+++ 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/PlainToHeadersWindowStoreAdapterTest.java
@@ -75,7 +75,7 @@ public class PlainToHeadersWindowStoreAdapterTest {
             new StreamsConfig(props)
         );
 
-        final SegmentedBytesStore segmentedBytesStore = new 
RocksDBSegmentedBytesStore(
+        final RocksDBSegmentedBytesStore segmentedBytesStore = new 
RocksDBSegmentedBytesStore(
             "iqv2-test-store",
             "test-metrics-scope",
             RETENTION_PERIOD,
diff --git 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBSessionStoreWithHeadersTest.java
 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBSessionStoreWithHeadersTest.java
index 9acbdfef326..2e04ab05852 100644
--- 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBSessionStoreWithHeadersTest.java
+++ 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBSessionStoreWithHeadersTest.java
@@ -63,7 +63,7 @@ public class RocksDBSessionStoreWithHeadersTest {
             new StreamsConfig(props)
         );
 
-        final SegmentedBytesStore segmentedBytesStore = new 
RocksDBSegmentedBytesStore(
+        final RocksDBSegmentedBytesStore segmentedBytesStore = new 
RocksDBSegmentedBytesStore(
             STORE_NAME,
             "test-metrics-scope",
             RETENTION_PERIOD,
diff --git 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStoreWithHeadersTest.java
 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStoreWithHeadersTest.java
index 554f5559865..04ee23555a7 100644
--- 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStoreWithHeadersTest.java
+++ 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBTimestampedWindowStoreWithHeadersTest.java
@@ -66,7 +66,7 @@ public class RocksDBTimestampedWindowStoreWithHeadersTest {
                 new StreamsConfig(props)
         );
 
-        final SegmentedBytesStore segmentedBytesStore = new 
RocksDBSegmentedBytesStore(
+        final RocksDBSegmentedBytesStore segmentedBytesStore = new 
RocksDBSegmentedBytesStore(
                 STORE_NAME,
                 "test-metrics-scope",
                 RETENTION_PERIOD,
diff --git 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersWindowStoreAdapterTest.java
 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersWindowStoreAdapterTest.java
index cf1649b144c..4b757785c42 100644
--- 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersWindowStoreAdapterTest.java
+++ 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/TimestampedToHeadersWindowStoreAdapterTest.java
@@ -76,7 +76,7 @@ public class TimestampedToHeadersWindowStoreAdapterTest {
                 new StreamsConfig(props)
         );
 
-        final SegmentedBytesStore segmentedBytesStore = new 
RocksDBSegmentedBytesStore(
+        final RocksDBSegmentedBytesStore segmentedBytesStore = new 
RocksDBSegmentedBytesStore(
                 "iqv2-test-store",
                 "test-metrics-scope",
                 RETENTION_PERIOD,

Reply via email to