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 db8f66ae36c KAFKA-20490: Replace txn buffer skip-list with TreeMap 
(#22648)
db8f66ae36c is described below

commit db8f66ae36c9964f096d8f7fca37e09d4c6da1f4
Author: Nick Telford <[email protected]>
AuthorDate: Wed Jun 24 23:28:27 2026 +0100

    KAFKA-20490: Replace txn buffer skip-list with TreeMap (#22648)
    
    The transaction buffer's staging map (`pendingWrites`) is written
    exclusively by the owning `StreamThread`, but it must also serve
    point-in-time, snapshot-isolated reads from interactive-query (IQ)
    threads. It was a `ConcurrentSkipListMap` so that IQ readers could
    traverse it safely while the owner mutated it concurrently. That
    concurrency came at a cost: every owner `get`/`put` on the hot path paid
    skip-list overhead even though the owner is the sole mutator, regressing
    `StreamThread` throughput.
    
    This change replaces the `ConcurrentSkipListMap` with a plain `TreeMap`
    guarded by the buffer's existing `snapshotLock`. Owner writes take the
    write lock; non-owner (IQ) reads take the read lock; owner reads and
    scan-copies run lock-free, because the owner is the only thread that
    ever mutates the map. Because an owner scan iterates the same `TreeMap`
    it may concurrently mutate, the owner eagerly copies the bounded staging
    range up front to avoid a same-thread `ConcurrentModificationException`.
    The same locking discipline is applied across
    `RocksDBTransactionBuffer`'s `stage`, `stageDeleteRange`, `get`, and
    `scan` paths.
    
    It also fixes a pre-existing copy-on-write bug in the `rangeTombstones`
    handling. The shallow `TreeMap` copy shared its mutable `List<Bytes>`
    values with the original, so range-deleting the same `from` key twice
    mutated a list still being traversed by an in-flight non-owner iterator,
    corrupting its view. The affected value lists are now deep-copied on
    write so iterators retain a stable snapshot.
    
    New concurrency and `ConcurrentModificationException` regression tests
    in `AbstractTransactionBufferTest` and `RocksDBTransactionBufferTest`
    cover these paths.
    
    Reviewers: Bill Bejeck <[email protected]>
---
 .../state/internals/AbstractTransactionBuffer.java | 60 ++++++++++---
 .../state/internals/RocksDBTransactionBuffer.java  | 84 +++++++++++++-----
 .../internals/AbstractTransactionBufferTest.java   | 99 ++++++++++++++++++++++
 .../internals/RocksDBTransactionBufferTest.java    | 53 ++++++++++++
 4 files changed, 261 insertions(+), 35 deletions(-)

diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractTransactionBuffer.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractTransactionBuffer.java
index b67f0e7b191..865fe9fb07e 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractTransactionBuffer.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/AbstractTransactionBuffer.java
@@ -19,23 +19,36 @@ package org.apache.kafka.streams.state.internals;
 import java.util.NavigableMap;
 import java.util.Optional;
 import java.util.TreeMap;
-import java.util.concurrent.ConcurrentSkipListMap;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 /**
  * Base class for {@link TransactionBuffer} implementations. Provides the 
shared two-layer
- * staging design: a thread-safe {@link ConcurrentSkipListMap} for reads (any 
thread) and
- * backend-specific write accumulation for atomic commit.
+ * staging design: a {@link TreeMap} staging buffer for uncommitted writes 
plus backend-specific
+ * write accumulation for atomic commit.
  * <p>
- * Point lookups ({@link #get(Comparable)}) are lock-free. Scan methods 
automatically detect the
- * owner thread and use a lock-free fast path; non-owner threads acquire a 
read lock to
- * snapshot the staging map atomically with base iterator creation.
+ * Exactly one thread — the {@link #ownerThread} that constructs the buffer 
(the StreamThread) —
+ * ever mutates {@code pendingWrites}; Interactive Query (IQ) threads are 
read-only. The staging
+ * map is therefore a plain (non-thread-safe) {@link TreeMap} guarded by 
{@link #snapshotLock}:
+ * <ul>
+ *   <li>Owner structural mutations ({@code stage}/{@code commit}/{@code 
rollback}) hold the
+ *       <b>write</b> lock.</li>
+ *   <li>The owner reads ({@code get}) and scan-copies <b>lock-free</b>: 
because it is the sole
+ *       mutator and is single-threaded, a read is never concurrent with a 
structural change, and
+ *       IQ threads never mutate — so the tree is quiescent for the read.</li>
+ *   <li>Non-owner (IQ) reads/snapshots hold the <b>read</b> lock; owner 
writes become visible to
+ *       them via the write-unlock → read-lock happens-before edge.</li>
+ *   <li>Owner scans eagerly copy the bounded staging range into a private 
{@link TreeMap} so a
+ *       subsequent owner {@code stage} cannot invalidate an open iterator 
with a
+ *       {@link java.util.ConcurrentModificationException}.</li>
+ * </ul>
+ * {@code isEmpty()} and {@code approximateNumUncommittedBytes()} read 
owner-thread-only state and
+ * are not synchronized.
  *
  * @param <K> the key type, must be {@link Comparable}
  */
 abstract class AbstractTransactionBuffer<K extends Comparable<K>> implements 
TransactionBuffer<K> {
 
-    final ConcurrentSkipListMap<K, Optional<byte[]>> pendingWrites = new 
ConcurrentSkipListMap<>();
+    final NavigableMap<K, Optional<byte[]>> pendingWrites = new TreeMap<>();
     final ReentrantReadWriteLock snapshotLock = new ReentrantReadWriteLock();
     final Thread ownerThread;
     long pendingWritesBytes;
@@ -83,21 +96,40 @@ abstract class AbstractTransactionBuffer<K extends 
Comparable<K>> implements Tra
 
     @Override
     public void stage(final K key, final byte[] value) {
-        pendingWrites.put(key, Optional.ofNullable(value));
-        pendingWritesBytes += estimateKeySize(key) + (value != null ? 
value.length : 0);
-        stageToBackend(key, value);
+        snapshotLock.writeLock().lock();
+        try {
+            pendingWrites.put(key, Optional.ofNullable(value));
+            pendingWritesBytes += estimateKeySize(key) + (value != null ? 
value.length : 0);
+            stageToBackend(key, value);
+        } finally {
+            snapshotLock.writeLock().unlock();
+        }
     }
 
     @Override
     public Optional<byte[]> get(final K key) {
-        return pendingWrites.get(key);
+        // Owner reads lock-free: it is the sole mutator and single-threaded, 
so this read is never
+        // concurrent with a structural change. Non-owner (IQ) reads take the 
read lock to exclude a
+        // concurrent owner write on the non-thread-safe TreeMap.
+        if (Thread.currentThread() == ownerThread) {
+            return pendingWrites.get(key);
+        }
+        snapshotLock.readLock().lock();
+        try {
+            return pendingWrites.get(key);
+        } finally {
+            snapshotLock.readLock().unlock();
+        }
     }
 
     @Override
     public ManagedKeyValueIterator<K, byte[]> all(final boolean forward) {
         if (Thread.currentThread() == ownerThread) {
+            // Eager copy of the staging layer (lock-free for the sole 
mutator) so a later owner
+            // stage cannot invalidate this iterator; the base store is 
iterated live.
+            final NavigableMap<K, Optional<byte[]>> stagingSnapshot = new 
TreeMap<>(pendingWrites);
             final ManagedKeyValueIterator<K, byte[]> baseIter = 
newBaseIterator(null, null, forward, true);
-            return new StagedMergeIterator<>(pendingWrites, baseIter, forward);
+            return new StagedMergeIterator<>(stagingSnapshot, baseIter, 
forward);
         }
         return snapshotScan(null, null, forward, true);
     }
@@ -108,9 +140,9 @@ abstract class AbstractTransactionBuffer<K extends 
Comparable<K>> implements Tra
     @Override
     public ManagedKeyValueIterator<K, byte[]> range(final K from, final K to, 
final boolean forward, final boolean toInclusive) {
         if (Thread.currentThread() == ownerThread) {
-            final NavigableMap<K, Optional<byte[]>> stagingView = 
boundStaging(from, to, toInclusive);
+            final NavigableMap<K, Optional<byte[]>> stagingSnapshot = new 
TreeMap<>(boundStaging(from, to, toInclusive));
             final ManagedKeyValueIterator<K, byte[]> baseIter = 
newBaseIterator(from, to, forward, toInclusive);
-            return new StagedMergeIterator<>(stagingView, baseIter, forward);
+            return new StagedMergeIterator<>(stagingSnapshot, baseIter, 
forward);
         }
         return snapshotScan(from, to, forward, toInclusive);
     }
diff --git 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTransactionBuffer.java
 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTransactionBuffer.java
index 6b33217667a..5191b7f721b 100644
--- 
a/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTransactionBuffer.java
+++ 
b/streams/src/main/java/org/apache/kafka/streams/state/internals/RocksDBTransactionBuffer.java
@@ -39,9 +39,13 @@ import java.util.TreeMap;
 /**
  * A {@link TransactionBuffer} implementation for RocksDB-backed stores.
  * Uses a {@link WriteBatch} (without index) to accumulate writes for atomic 
commit.
- * Reads are handled entirely by the shared {@code ConcurrentSkipListMap} in
+ * Reads are handled entirely by the shared staging map in
  * {@link AbstractTransactionBuffer}, so a {@code WriteBatchWithIndex} is not 
needed.
  * <p>
+ * Follows the base class locking discipline: structural mutations of the 
staging map
+ * ({@link #stage}, {@link #stageDeleteRange}) hold the {@code snapshotLock} 
write lock; owner
+ * reads/scan-copies are lock-free; non-owner (IQ) reads hold the read lock.
+ * <p>
  * Range deletions ({@link #stageDeleteRange}) are only supported by 
RocksDB-backed stores
  * and are owned entirely by this class; {@link AbstractTransactionBuffer} 
carries no
  * tombstone state.
@@ -82,16 +86,21 @@ class RocksDBTransactionBuffer extends 
AbstractTransactionBuffer<Bytes> {
      * under {@code cf}, so every staged CF is committed atomically on {@link 
#commit()}.
      */
     void stage(final ColumnFamilyHandle cf, final Bytes key, final byte[] 
value) {
-        pendingWrites.put(key, Optional.ofNullable(value));
-        pendingWritesBytes += estimateKeySize(key) + (value != null ? 
value.length : 0);
+        snapshotLock.writeLock().lock();
         try {
-            if (value != null) {
-                writeBatch.put(cf, key.get(), value);
-            } else {
-                writeBatch.delete(cf, key.get());
+            pendingWrites.put(key, Optional.ofNullable(value));
+            pendingWritesBytes += estimateKeySize(key) + (value != null ? 
value.length : 0);
+            try {
+                if (value != null) {
+                    writeBatch.put(cf, key.get(), value);
+                } else {
+                    writeBatch.delete(cf, key.get());
+                }
+            } catch (final RocksDBException e) {
+                throw new ProcessorStateException("Error staging write in 
transaction buffer for store " + storeName, e);
             }
-        } catch (final RocksDBException e) {
-            throw new ProcessorStateException("Error staging write in 
transaction buffer for store " + storeName, e);
+        } finally {
+            snapshotLock.writeLock().unlock();
         }
     }
 
@@ -102,21 +111,49 @@ class RocksDBTransactionBuffer extends 
AbstractTransactionBuffer<Bytes> {
      * {@link WriteBatch} under {@code cf}.
      */
     void stageDeleteRange(final ColumnFamilyHandle cf, final Bytes from, final 
Bytes to) {
-        pendingWrites.subMap(from, true, to, false).clear();
-        final TreeMap<Bytes, List<Bytes>> copy = new 
TreeMap<>(rangeTombstones);
-        copy.computeIfAbsent(from, k -> new ArrayList<>()).add(to);
-        rangeTombstones = copy;
-        pendingWritesBytes += estimateKeySize(from) + estimateKeySize(to);
+        snapshotLock.writeLock().lock();
         try {
-            writeBatch.deleteRange(cf, from.get(), to.get());
-        } catch (final RocksDBException e) {
-            throw new ProcessorStateException("Error staging delete range in 
transaction buffer for store " + storeName, e);
+            // Multi-node subMap().clear() on the (non-thread-safe) TreeMap 
staging buffer must hold
+            // the write lock; doing it together with the rangeTombstones swap 
also makes the two
+            // updates atomic with respect to a non-owner snapshot scan.
+            pendingWrites.subMap(from, true, to, false).clear();
+            // Copy-on-write: clone the map AND the affected key's list, so a 
non-owner iterator
+            // holding the previous rangeTombstones map (and its lists) is 
never mutated underneath
+            // it. A shallow map copy alone would share the List<Bytes> values 
and corrupt in-flight
+            // iterators when the same `from` is deleted twice.
+            final TreeMap<Bytes, List<Bytes>> copy = new 
TreeMap<>(rangeTombstones);
+            final List<Bytes> existing = copy.get(from);
+            final List<Bytes> updated = existing == null ? new ArrayList<>() : 
new ArrayList<>(existing);
+            updated.add(to);
+            copy.put(from, updated);
+            rangeTombstones = copy;
+            pendingWritesBytes += estimateKeySize(from) + estimateKeySize(to);
+            try {
+                writeBatch.deleteRange(cf, from.get(), to.get());
+            } catch (final RocksDBException e) {
+                throw new ProcessorStateException("Error staging delete range 
in transaction buffer for store " + storeName, e);
+            }
+        } finally {
+            snapshotLock.writeLock().unlock();
         }
     }
 
     @Override
     public Optional<byte[]> get(final Bytes key) {
-        final Optional<byte[]> staged = pendingWrites.get(key);
+        // Owner reads lock-free (sole, single-threaded mutator); non-owner 
(IQ) reads take the read
+        // lock to exclude a concurrent owner write on the TreeMap. The 
volatile rangeTombstones
+        // reference is read lock-free either way.
+        final Optional<byte[]> staged;
+        if (Thread.currentThread() == ownerThread) {
+            staged = pendingWrites.get(key);
+        } else {
+            snapshotLock.readLock().lock();
+            try {
+                staged = pendingWrites.get(key);
+            } finally {
+                snapshotLock.readLock().unlock();
+            }
+        }
         if (staged != null) {
             return staged;
         }
@@ -133,8 +170,11 @@ class RocksDBTransactionBuffer extends 
AbstractTransactionBuffer<Bytes> {
 
     ManagedKeyValueIterator<Bytes, byte[]> all(final ColumnFamilyHandle cf, 
final boolean forward) {
         if (Thread.currentThread() == ownerThread) {
+            // Eager copy of the staging layer (lock-free for the sole 
mutator) so a later owner
+            // stage cannot invalidate this iterator; the base store is 
iterated live.
+            final NavigableMap<Bytes, Optional<byte[]>> stagingSnapshot = new 
TreeMap<>(pendingWrites);
             final ManagedKeyValueIterator<Bytes, byte[]> baseIter = 
newBaseIterator(cf, null, null, forward, true);
-            return new StagedMergeIterator<>(pendingWrites, baseIter, forward);
+            return new StagedMergeIterator<>(stagingSnapshot, baseIter, 
forward);
         }
         return snapshotScan(cf, null, null, forward, true);
     }
@@ -143,9 +183,9 @@ class RocksDBTransactionBuffer extends 
AbstractTransactionBuffer<Bytes> {
                                                  final Bytes from, final Bytes 
to,
                                                  final boolean forward, final 
boolean toInclusive) {
         if (Thread.currentThread() == ownerThread) {
-            final NavigableMap<Bytes, Optional<byte[]>> stagingView = 
boundStaging(from, to, toInclusive);
+            final NavigableMap<Bytes, Optional<byte[]>> stagingSnapshot = new 
TreeMap<>(boundStaging(from, to, toInclusive));
             final ManagedKeyValueIterator<Bytes, byte[]> baseIter = 
newBaseIterator(cf, from, to, forward, toInclusive);
-            return new StagedMergeIterator<>(stagingView, baseIter, forward);
+            return new StagedMergeIterator<>(stagingSnapshot, baseIter, 
forward);
         }
         return snapshotScan(cf, from, to, forward, toInclusive);
     }
@@ -182,6 +222,8 @@ class RocksDBTransactionBuffer extends 
AbstractTransactionBuffer<Bytes> {
         final boolean forward,
         final boolean toInclusive
     ) {
+        // A RocksDB iterator already exposes a point-in-time consistent view 
of the base store
+        // as of its creation, so no extra snapshotting is required beyond the 
live base iterator.
         return newBaseIterator(cfHandle, from, to, forward, toInclusive);
     }
 
diff --git 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractTransactionBufferTest.java
 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractTransactionBufferTest.java
index 86d871f6201..571b3c84dd0 100644
--- 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractTransactionBufferTest.java
+++ 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/AbstractTransactionBufferTest.java
@@ -31,6 +31,7 @@ import java.util.TreeMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -339,6 +340,104 @@ public class AbstractTransactionBufferTest {
         }
     }
 
+    // -- owner mutate-during-iteration (no CME on the TreeMap staging buffer) 
--
+
+    @Test
+    void ownerStageDuringOwnAllIterationDoesNotThrow() {
+        buffer.stage(1, new byte[]{1});
+        buffer.stage(3, new byte[]{3});
+
+        final ManagedKeyValueIterator<Integer, byte[]> iter = buffer.all(true);
+        assertEquals(1, iter.next().key);
+
+        // Owner structurally mutates the staging buffer while its own 
iterator is open.
+        buffer.stage(2, new byte[]{2});
+        buffer.stage(4, new byte[]{4});
+
+        // No ConcurrentModificationException, and the iterator reflects the 
snapshot at creation
+        // (the keys staged after the iterator was opened are absent).
+        assertEquals(List.of(3), drainKeys(iter));
+    }
+
+    @Test
+    void ownerStageDuringOwnRangeIterationDoesNotThrow() {
+        buffer.stage(1, new byte[]{1});
+        buffer.stage(5, new byte[]{5});
+
+        final ManagedKeyValueIterator<Integer, byte[]> iter = buffer.range(1, 
9, true, true);
+        assertEquals(1, iter.next().key);
+
+        buffer.stage(3, new byte[]{3});
+
+        assertEquals(List.of(5), drainKeys(iter));
+    }
+
+    // -- non-owner snapshot survives a concurrent owner commit --
+
+    @Test
+    void nonOwnerAllSurvivesConcurrentOwnerCommit() throws Exception {
+        buffer.base.put(2, new byte[]{2});
+        buffer.stage(1, new byte[]{1});
+
+        final ExecutorService exec = Executors.newSingleThreadExecutor();
+        try {
+            final ManagedKeyValueIterator<Integer, byte[]> iter =
+                exec.submit(() -> buffer.all(true)).get();
+
+            // Owner commits (flushes staging to base and clears 
pendingWrites) while the non-owner
+            // snapshot iterator is open.
+            buffer.commit();
+
+            // Snapshot captured at creation time: staged key 1 plus base key 
2.
+            assertEquals(List.of(1, 2), drainKeys(iter));
+        } finally {
+            exec.shutdown();
+        }
+    }
+
+    // -- concurrency stress: owner writer + non-owner readers on the TreeMap 
staging buffer --
+
+    @Test
+    void concurrentOwnerWritesAndNonOwnerReadsAreConsistent() throws Exception 
{
+        final int readerCount = 4;
+        final ExecutorService exec = Executors.newFixedThreadPool(readerCount);
+        final AtomicBoolean stop = new AtomicBoolean(false);
+        final List<Future<?>> readers = new ArrayList<>();
+        try {
+            for (int r = 0; r < readerCount; r++) {
+                readers.add(exec.submit(() -> {
+                    while (!stop.get()) {
+                        buffer.get(7);
+                        // Non-owner scan → snapshotScan: must never throw and 
must be sorted.
+                        final List<Integer> keys = drainKeys(buffer.all(true));
+                        for (int i = 1; i < keys.size(); i++) {
+                            if (keys.get(i - 1) >= keys.get(i)) {
+                                throw new AssertionError("non-owner scan 
returned unsorted keys: " + keys);
+                            }
+                        }
+                    }
+                    return null;
+                }));
+            }
+
+            for (int i = 0; i < 10_000; i++) {
+                buffer.stage(i % 64, new byte[]{(byte) i});
+                if (i % 200 == 0) {
+                    buffer.commit();
+                }
+                if (i % 500 == 0) {
+                    buffer.rollback();
+                }
+            }
+            stop.set(true);
+            for (final Future<?> reader : readers) {
+                reader.get(); // surfaces any AssertionError / 
ConcurrentModificationException
+            }
+        } finally {
+            exec.shutdownNow();
+        }
+    }
+
     // -- Helpers --
 
     private static List<Integer> drainKeys(final 
ManagedKeyValueIterator<Integer, byte[]> iter) {
diff --git 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBTransactionBufferTest.java
 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBTransactionBufferTest.java
index 0fab3b44e91..b0f7f11e37b 100644
--- 
a/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBTransactionBufferTest.java
+++ 
b/streams/src/test/java/org/apache/kafka/streams/state/internals/RocksDBTransactionBufferTest.java
@@ -39,6 +39,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -260,6 +261,58 @@ public class RocksDBTransactionBufferTest {
         assertEquals(List.of("a", "b", "c", "d"), result.get());
     }
 
+    @Test
+    public void shouldHandleConcurrentStageDeleteRangeAndNonOwnerReads() 
throws Exception {
+        final int readerCount = 4;
+        final AtomicBoolean stop = new AtomicBoolean(false);
+        final AtomicReference<Throwable> failure = new AtomicReference<>();
+        final List<Thread> readers = new ArrayList<>();
+        for (int r = 0; r < readerCount; r++) {
+            final Thread reader = new Thread(() -> {
+                try {
+                    while (!stop.get()) {
+                        buffer.get(key("k50"));
+                        // Non-owner scan → snapshotScan: must never throw and 
must be sorted, even
+                        // while the owner concurrently runs stageDeleteRange 
(subMap().clear()).
+                        try (KeyValueIterator<Bytes, byte[]> iter = 
buffer.all(true)) {
+                            Bytes prev = null;
+                            while (iter.hasNext()) {
+                                final Bytes k = iter.next().key;
+                                if (prev != null && prev.compareTo(k) >= 0) {
+                                    throw new AssertionError("non-owner scan 
returned unsorted keys: " + prev + " >= " + k);
+                                }
+                                prev = k;
+                            }
+                        }
+                    }
+                } catch (final Throwable t) {
+                    failure.compareAndSet(null, t);
+                }
+            });
+            readers.add(reader);
+            reader.start();
+        }
+        try {
+            for (int i = 0; i < 5_000; i++) {
+                buffer.stage(key(String.format("k%02d", i % 80)), val("v" + 
i));
+                if (i % 50 == 0) {
+                    buffer.stageDeleteRange(cfHandle, key("k10"), key("k30"));
+                }
+                if (i % 200 == 0) {
+                    buffer.commit();
+                }
+            }
+        } finally {
+            stop.set(true);
+            for (final Thread reader : readers) {
+                reader.join();
+            }
+        }
+        if (failure.get() != null) {
+            throw new AssertionError("concurrent non-owner reader failed", 
failure.get());
+        }
+    }
+
     @Test
     public void shouldNotShowStagedWritesInBaseAfterRollback() throws 
RocksDBException {
         buffer.stage(key("x"), val("staged-x"));

Reply via email to