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 59596de362b KAFKA-20760: Stage position updates in 
InMemorySessionStore transaction buffer (#22745)
59596de362b is described below

commit 59596de362ba4d3b6e492681e2c8381393c63328
Author: Nick Telford <[email protected]>
AuthorDate: Fri Jul 3 01:49:46 2026 +0100

    KAFKA-20760: Stage position updates in InMemorySessionStore transaction 
buffer (#22745)
    
    `InMemorySessionStore`, when transactional, did not correctly track its
    `Position` across the transaction boundary, breaking interactive-query
    position consistency in two ways. The root cause is the same as the
    parallel fix for `InMemoryKeyValueStore`.
    
    **Staging.** `put` called `StoreQueryUtils.updatePosition` directly on
    the store's committed `Position` on every write, bypassing the
    transaction boundary, so IQv2 position-bound queries could observe
    position advances corresponding to records not yet visible. Position
    updates are now staged into a `pendingPosition` field on
    `InMemorySessionTransactionBuffer` under the existing `snapshotLock`,
    and merged into the committed position at commit time via
    `mergePendingPositionInto`. `discardPendingBatch` resets the pending
    position on abort. Under READ_UNCOMMITTED, `query` merges the pending
    deltas so staged writes remain visible, mirroring `RocksDBStore`.
    
    **Changelog consistency vector.** `getPosition()` returned only the
    committed position. Because `ChangeLoggingSessionBytesStore` stamps each
    changelog record's consistency vector with `getPosition()` at put time,
    a transactional store's changelog records carried a stale (empty)
    vector, so the position could not be rebuilt when the store was restored
    from its changelog. `getPosition()` now returns the uncommitted
    position, matching the behaviour `RocksDBStore` already had.
    
    End-to-end restart coverage is added separately via the parameterized
    `PositionRestartIntegrationTest`.
    
    Part of a series of fixes for interactive-query position consistency
    with transactional state stores.
    
    Reviewers: Bill Bejeck <[email protected]>
    
    Co-authored-by: Claude Sonnet 4.6 (1M context) <[email protected]>
---
 .../state/internals/InMemorySessionStore.java      | 49 +++++++++++++++++-----
 .../InMemorySessionTransactionBuffer.java          | 33 ++++++++++++++-
 2 files changed, 71 insertions(+), 11 deletions(-)

diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemorySessionStore.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemorySessionStore.java
index 0b36f5b11c4..fa81a903dfa 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemorySessionStore.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemorySessionStore.java
@@ -181,6 +181,16 @@ public class InMemorySessionStore implements 
SessionStore<Bytes, byte[]>, WithRe
 
     @Override
     public Position getPosition() {
+        // Mirror RocksDBStore#getPosition: report the uncommitted position 
(committed + staged) so the
+        // changelog consistency vector, which ChangeLogging*BytesStore writes 
at put() time via
+        // getPosition(), reflects the input position of the staged write. 
Otherwise a transactional
+        // store's changelog records carry the stale committed position and 
the position cannot be
+        // rebuilt when the store is restored from its changelog.
+        if (transactionBuffer != null) {
+            synchronized (position) {
+                return 
position.copy().merge(transactionBuffer.pendingPosition());
+            }
+        }
         return position;
     }
 
@@ -205,7 +215,11 @@ public class InMemorySessionStore implements 
SessionStore<Bytes, byte[]>, WithRe
                 putInternal(sessionKey, aggregate);
             }
 
-            StoreQueryUtils.updatePosition(position, stateStoreContext);
+            if (transactionBuffer != null) {
+                transactionBuffer.updatePosition(stateStoreContext);
+            } else {
+                StoreQueryUtils.updatePosition(position, stateStoreContext);
+            }
         }
     }
 
@@ -462,14 +476,26 @@ public class InMemorySessionStore implements 
SessionStore<Bytes, byte[]>, WithRe
                                     final PositionBound positionBound,
                                     final QueryConfig config) {
 
-        return StoreQueryUtils.handleBasicQueries(
-            query,
-            positionBound,
-            config,
-            this,
-            position,
-            context
-        );
+        synchronized (position) {
+            // Mirror RocksDBStore#query: under READ_UNCOMMITTED, expose the 
writes staged in the
+            // transaction buffer since the last commit by merging the 
buffer's pending position
+            // deltas into a copy of the committed position. READ_COMMITTED 
(and the
+            // non-transactional store) query the committed position directly.
+            final Position queryPosition;
+            if (transactionBuffer != null && config.getIsolationLevel() == 
IsolationLevel.READ_UNCOMMITTED) {
+                queryPosition = 
position.copy().merge(transactionBuffer.pendingPosition());
+            } else {
+                queryPosition = position;
+            }
+            return StoreQueryUtils.handleBasicQueries(
+                query,
+                positionBound,
+                config,
+                this,
+                queryPosition,
+                context
+            );
+        }
     }
 
     @Override
@@ -563,7 +589,10 @@ public class InMemorySessionStore implements 
SessionStore<Bytes, byte[]>, WithRe
     @Override
     public void commit(final Map<TopicPartition, Long> changelogOffsets) {
         if (transactionBuffer != null) {
-            transactionBuffer.commit();
+            synchronized (position) {
+                transactionBuffer.mergePendingPositionInto(position);
+                transactionBuffer.commit();
+            }
         }
     }
 
diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemorySessionTransactionBuffer.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemorySessionTransactionBuffer.java
index 00d4e8246fd..cfaf79a274b 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemorySessionTransactionBuffer.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/InMemorySessionTransactionBuffer.java
@@ -19,6 +19,8 @@ package org.apache.kafka.streams.state.internals;
 import org.apache.kafka.common.utils.Bytes;
 import org.apache.kafka.streams.KeyValue;
 import org.apache.kafka.streams.kstream.Windowed;
+import org.apache.kafka.streams.processor.StateStoreContext;
+import org.apache.kafka.streams.query.Position;
 import org.apache.kafka.streams.state.KeyValueIterator;
 
 import java.util.Map;
@@ -34,6 +36,7 @@ import java.util.concurrent.ConcurrentSkipListMap;
 class InMemorySessionTransactionBuffer extends 
AbstractTransactionBuffer<InMemorySessionTransactionBuffer.SessionEntryKey> {
 
     private final ConcurrentNavigableMap<Long, ConcurrentNavigableMap<Bytes, 
ConcurrentNavigableMap<Long, byte[]>>> endTimeMap;
+    private Position pendingPosition = Position.emptyPosition();
 
     InMemorySessionTransactionBuffer(
             final ConcurrentNavigableMap<Long, ConcurrentNavigableMap<Bytes, 
ConcurrentNavigableMap<Long, byte[]>>> endTimeMap) {
@@ -226,6 +229,34 @@ class InMemorySessionTransactionBuffer extends 
AbstractTransactionBuffer<InMemor
                 forward));
     }
 
+    void updatePosition(final StateStoreContext stateStoreContext) {
+        snapshotLock.writeLock().lock();
+        try {
+            StoreQueryUtils.updatePosition(pendingPosition, stateStoreContext);
+        } finally {
+            snapshotLock.writeLock().unlock();
+        }
+    }
+
+    Position pendingPosition() {
+        snapshotLock.readLock().lock();
+        try {
+            return pendingPosition.copy();
+        } finally {
+            snapshotLock.readLock().unlock();
+        }
+    }
+
+    void mergePendingPositionInto(final Position committed) {
+        snapshotLock.writeLock().lock();
+        try {
+            committed.merge(pendingPosition);
+            pendingPosition = Position.emptyPosition();
+        } finally {
+            snapshotLock.writeLock().unlock();
+        }
+    }
+
     @Override
     void flushToBase() {
         for (final Map.Entry<SessionEntryKey, Optional<byte[]>> entry : 
pendingWrites.entrySet()) {
@@ -256,7 +287,7 @@ class InMemorySessionTransactionBuffer extends 
AbstractTransactionBuffer<InMemor
 
     @Override
     void discardPendingBatch() {
-        // no-op — no backend batch to discard
+        pendingPosition = Position.emptyPosition();
     }
 
     /**

Reply via email to