This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch cassandra-5.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/cassandra-5.0 by this push:
new de8fe4b5d9 Fix compressed scan read-ahead buffer Block leak and
chunk-cache pollution by scans
de8fe4b5d9 is described below
commit de8fe4b5d931c37a261912e11d994a6a45c9ef7e
Author: Jon Haddad <[email protected]>
AuthorDate: Mon Sep 14 19:23:44 2026 -0700
Fix compressed scan read-ahead buffer Block leak and chunk-cache pollution
by scans
A shared thread-local read-ahead buffer allowed concurrent scans of one
SSTable to collide and leak. Each compressed scan reader
now owns a per-scan buffer that it frees on close. Scans also polluted the
chunk cache with one-shot chunks that evicted hot data.
patch by Jon Haddad; reviewed by Caleb Rackliffe, Francisco Guerrero, and
Sam Lightfoot for CASSANDRA-21671
Co-authored-by: Jon Haddad <[email protected]>
Co-authored-by: Caleb Rackliffe <[email protected]>
Co-Authored-By: Francisco Guerrero <[email protected]>
Assisted-by: Claude Code:claude-opus-4-8
---
CHANGES.txt | 1 +
.../org/apache/cassandra/cache/ChunkCache.java | 3 +-
.../index/sai/StorageAttachedIndexBuilder.java | 2 +-
.../cassandra/index/sasi/SASIIndexBuilder.java | 2 +-
.../cassandra/io/sstable/format/SSTableReader.java | 6 +
.../io/sstable/format/SortedTableScrubber.java | 4 +-
.../io/sstable/format/SortedTableVerifier.java | 4 +-
.../io/sstable/format/bti/PartitionIterator.java | 2 +-
.../cassandra/io/util/CompressedChunkReader.java | 65 +++--
.../org/apache/cassandra/io/util/FileHandle.java | 22 +-
...alReadAheadBuffer.java => ReadAheadBuffer.java} | 130 +++++-----
.../cassandra/io/util/SimpleChunkReader.java | 2 +-
.../io/sstable/format/ForwardingSSTableReader.java | 12 +
.../cassandra/cache/ChunkCacheScanBypassTest.java | 225 +++++++++++++++++
.../io/util/CompressedChunkReaderTest.java | 187 ++++++++++----
.../cassandra/io/util/ReadAheadBufferTest.java | 276 +++++++++++++++++++++
.../io/util/ThreadLocalReadAheadBufferTest.java | 207 ----------------
17 files changed, 782 insertions(+), 368 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index fe6da8ff9f..dcac3d5e07 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
5.0.10
+ * Fix compressed scan read-ahead buffer Block leak and chunk-cache pollution
by scans (CASSANDRA-21671)
* A read on a replica that does not know the index yet fails with a
NullPointerException (CASSANDRA-21658)
* Release hints write buffer when the write executor shuts down
(CASSANDRA-21657)
* Fix concurrent SAI vector inserts failing once jvector's per-graph pool
limit is exceeded (CASSANDRA-21644)
diff --git a/src/java/org/apache/cassandra/cache/ChunkCache.java
b/src/java/org/apache/cassandra/cache/ChunkCache.java
index 5fb0bc6a2f..3454c5c333 100644
--- a/src/java/org/apache/cassandra/cache/ChunkCache.java
+++ b/src/java/org/apache/cassandra/cache/ChunkCache.java
@@ -261,7 +261,8 @@ public class ChunkCache
@Override
public Rebufferer instantiateRebufferer(boolean isScan)
{
- return this;
+ // Bypass the cache for scans, so they don't evict hot data with
chunks that may never be revisited.
+ return isScan ? source.instantiateRebufferer(isScan) : this;
}
@Override
diff --git
a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java
b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java
index dd94272700..c7ee124063 100644
--- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java
+++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndexBuilder.java
@@ -142,7 +142,7 @@ public class StorageAttachedIndexBuilder extends
SecondaryIndexBuilder
return false;
}
- try (RandomAccessReader dataFile = sstable.openDataReader();
+ try (RandomAccessReader dataFile = sstable.openDataReaderForScan();
LifecycleTransaction txn =
LifecycleTransaction.offline(OperationType.INDEX_BUILD, sstable))
{
perSSTableFileLock = shouldWritePerSSTableFiles(sstable);
diff --git a/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java
b/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java
index 4e71538e41..c31f1aca16 100644
--- a/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java
+++ b/src/java/org/apache/cassandra/index/sasi/SASIIndexBuilder.java
@@ -80,7 +80,7 @@ class SASIIndexBuilder extends SecondaryIndexBuilder
SSTableReader sstable = e.getKey();
Map<ColumnMetadata, ColumnIndex> indexes = e.getValue();
- try (RandomAccessReader dataFile = sstable.openDataReader())
+ try (RandomAccessReader dataFile = sstable.openDataReaderForScan())
{
PerSSTableIndexWriter indexWriter =
SASIIndex.newWriter(keyValidator, sstable.descriptor, indexes,
OperationType.COMPACTION);
targetDirectory = indexWriter.getDescriptor().directory.path();
diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java
b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java
index 96010ded32..854a1b486e 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java
@@ -1385,6 +1385,12 @@ public abstract class SSTableReader extends SSTable
implements UnfilteredSource,
return dfile.createReaderForScan();
}
+ public RandomAccessReader openDataReaderForScan(RateLimiter limiter)
+ {
+ assert limiter != null;
+ return dfile.createReaderForScan(limiter);
+ }
+
public void trySkipFileCacheBefore(DecoratedKey key)
{
long position = getPosition(key, SSTableReader.Operator.GE);
diff --git
a/src/java/org/apache/cassandra/io/sstable/format/SortedTableScrubber.java
b/src/java/org/apache/cassandra/io/sstable/format/SortedTableScrubber.java
index a83527509f..66a7a43108 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/SortedTableScrubber.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTableScrubber.java
@@ -153,8 +153,8 @@ public abstract class SortedTableScrubber<R extends
SSTableReaderWithFilter> imp
// partition header (key or data size) is corrupt. (This means our
position in the index file will be one
// partition "ahead" of the data file.)
this.dataFile = transaction.isOffline()
- ? sstable.openDataReader()
- :
sstable.openDataReader(CompactionManager.instance.getRateLimiter());
+ ? sstable.openDataReaderForScan()
+ :
sstable.openDataReaderForScan(CompactionManager.instance.getRateLimiter());
this.scrubInfo = new ScrubInfo(dataFile, sstable,
fileAccessLock.readLock());
diff --git
a/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java
b/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java
index a45dc585a9..6abb1d42b4 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java
@@ -95,8 +95,8 @@ public abstract class SortedTableVerifier<R extends
SSTableReaderWithFilter> imp
this.fileAccessLock = new ReentrantReadWriteLock();
this.dataFile = isOffline
- ? sstable.openDataReader()
- :
sstable.openDataReader(CompactionManager.instance.getRateLimiter());
+ ? sstable.openDataReaderForScan()
+ :
sstable.openDataReaderForScan(CompactionManager.instance.getRateLimiter());
this.verifyInfo = new VerifyInfo(dataFile, sstable,
fileAccessLock.readLock());
this.options = options;
this.isOffline = isOffline;
diff --git
a/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIterator.java
b/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIterator.java
index 17d1041ee2..605262a9c4 100644
--- a/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIterator.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/bti/PartitionIterator.java
@@ -232,7 +232,7 @@ class PartitionIterator extends
PartitionIndex.IndexPosIterator implements KeyRe
private void seekDataInput(long pos) throws IOException
{
if (dataInput == null)
- dataInput = dataFile.createReader(pos);
+ dataInput = dataFile.createReader(pos, true);
else
dataInput.seek(pos);
}
diff --git a/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java
b/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java
index b6b3c9a6a2..5156e263f0 100644
--- a/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java
+++ b/src/java/org/apache/cassandra/io/util/CompressedChunkReader.java
@@ -49,6 +49,14 @@ public abstract class CompressedChunkReader extends
AbstractReaderFileProxy impl
assert Integer.bitCount(metadata.chunkLength()) == 1; //must be a
power of two
}
+ protected CompressedChunkReader(CompressedChunkReader parent)
+ {
+ super(parent.channel, parent.metadata.dataLength);
+ this.metadata = parent.metadata;
+ this.maxCompressedLength = parent.maxCompressedLength;
+ this.crcCheckChanceSupplier = parent.crcCheckChanceSupplier;
+ }
+
protected CompressedChunkReader forScan()
{
return this;
@@ -97,25 +105,18 @@ public abstract class CompressedChunkReader extends
AbstractReaderFileProxy impl
protected interface CompressedReader extends Closeable
{
- default void allocateResources()
- {
- }
-
default void deallocateResources()
{
}
- default boolean allocated()
- {
- return false;
- }
-
default void close()
{
}
-
+ /**
+ * The returned buffer is only valid until the next call to read().
Callers must consume the data immediately.
+ */
ByteBuffer read(CompressionMetadata.Chunk chunk, boolean
shouldCheckCrc) throws CorruptBlockException;
}
@@ -157,13 +158,13 @@ public abstract class CompressedChunkReader extends
AbstractReaderFileProxy impl
{
private final ChannelProxy channel;
private final ThreadLocalByteBufferHolder bufferHolder;
- private final ThreadLocalReadAheadBuffer readAheadBuffer;
+ private final ReadAheadBuffer readAheadBuffer;
private ScanCompressedReader(ChannelProxy channel, CompressionMetadata
metadata, int readAheadBufferSize)
{
this.channel = channel;
this.bufferHolder = new
ThreadLocalByteBufferHolder(metadata.compressor().preferredBufferType());
- this.readAheadBuffer = new ThreadLocalReadAheadBuffer(channel,
readAheadBufferSize, metadata.compressor().preferredBufferType());
+ this.readAheadBuffer = new ReadAheadBuffer(channel,
readAheadBufferSize, metadata.compressor().preferredBufferType());
}
@Override
@@ -198,24 +199,13 @@ public abstract class CompressedChunkReader extends
AbstractReaderFileProxy impl
return compressed;
}
- @Override
- public void allocateResources()
- {
- readAheadBuffer.allocateBuffer();
- }
-
@Override
public void deallocateResources()
{
- readAheadBuffer.clear(true);
+ readAheadBuffer.close();
}
@Override
- public boolean allocated()
- {
- return readAheadBuffer.hasBuffer();
- }
-
public void close()
{
readAheadBuffer.close();
@@ -224,26 +214,35 @@ public abstract class CompressedChunkReader extends
AbstractReaderFileProxy impl
public static class Standard extends CompressedChunkReader
{
-
private final CompressedReader reader;
private final CompressedReader scanReader;
+ private final int readAheadBufferSize;
public Standard(ChannelProxy channel, CompressionMetadata metadata,
Supplier<Double> crcCheckChanceSupplier)
{
super(channel, metadata, crcCheckChanceSupplier);
- reader = new RandomAccessCompressedReader(channel, metadata);
+ this.reader = new RandomAccessCompressedReader(channel, metadata);
+ this.scanReader = null;
+ int size = DatabaseDescriptor.getCompressedReadAheadBufferSize();
+ this.readAheadBufferSize = (size > 0 && size >
metadata.chunkLength()) ? size : 0;
+ }
- int readAheadBufferSize =
DatabaseDescriptor.getCompressedReadAheadBufferSize();
- scanReader = (readAheadBufferSize > 0 && readAheadBufferSize >
metadata.chunkLength())
- ? new ScanCompressedReader(channel, metadata,
readAheadBufferSize) : null;
+ private Standard(Standard parent, CompressedReader scanReader)
+ {
+ super(parent);
+ this.reader = parent.reader;
+ this.scanReader = scanReader;
+ this.readAheadBufferSize = 0;
}
+ @Override
protected CompressedChunkReader forScan()
{
- if (scanReader != null)
- scanReader.allocateResources();
+ if (readAheadBufferSize == 0)
+ return this;
- return this;
+ ScanCompressedReader scan = new ScanCompressedReader(channel,
metadata, readAheadBufferSize);
+ return new Standard(this, scan);
}
@Override
@@ -265,7 +264,7 @@ public abstract class CompressedChunkReader extends
AbstractReaderFileProxy impl
CompressionMetadata.Chunk chunk = metadata.chunkFor(position);
boolean shouldCheckCrc = shouldCheckCrc();
- CompressedReader readFrom = (scanReader != null &&
scanReader.allocated()) ? scanReader : reader;
+ CompressedReader readFrom = scanReader != null ? scanReader :
reader;
if (chunk.length < maxCompressedLength)
{
ByteBuffer compressed = readFrom.read(chunk,
shouldCheckCrc);
diff --git a/src/java/org/apache/cassandra/io/util/FileHandle.java
b/src/java/org/apache/cassandra/io/util/FileHandle.java
index 7e4b214128..b9e3f233fa 100644
--- a/src/java/org/apache/cassandra/io/util/FileHandle.java
+++ b/src/java/org/apache/cassandra/io/util/FileHandle.java
@@ -136,7 +136,12 @@ public class FileHandle extends SharedCloseableImpl
public RandomAccessReader createReaderForScan()
{
- return createReader(null, true);
+ return createReaderForScan(null);
+ }
+
+ public RandomAccessReader createReaderForScan(RateLimiter limiter)
+ {
+ return createReader(limiter, true);
}
/**
@@ -151,14 +156,19 @@ public class FileHandle extends SharedCloseableImpl
return createReader(limiter, false);
}
- public RandomAccessReader createReader(RateLimiter limiter, boolean
forScan)
+ public RandomAccessReader createReader(RateLimiter limiter, boolean isScan)
{
- return new RandomAccessReader(instantiateRebufferer(limiter, forScan));
+ return new RandomAccessReader(instantiateRebufferer(limiter, isScan));
}
public FileDataInput createReader(long position)
{
- RandomAccessReader reader = createReader();
+ return createReader(position, false);
+ }
+
+ public FileDataInput createReader(long position, boolean isScan)
+ {
+ RandomAccessReader reader = createReader(null, isScan);
try
{
reader.seek(position);
@@ -199,9 +209,9 @@ public class FileHandle extends SharedCloseableImpl
return instantiateRebufferer(limiter, false);
}
- public Rebufferer instantiateRebufferer(RateLimiter limiter, boolean
forScan)
+ public Rebufferer instantiateRebufferer(RateLimiter limiter, boolean
isScan)
{
- Rebufferer rebufferer =
rebuffererFactory.instantiateRebufferer(forScan);
+ Rebufferer rebufferer =
rebuffererFactory.instantiateRebufferer(isScan);
if (limiter != null)
rebufferer = new LimitingRebufferer(rebufferer, limiter,
DiskOptimizationStrategy.MAX_BUFFER_SIZE);
diff --git
a/src/java/org/apache/cassandra/io/util/ThreadLocalReadAheadBuffer.java
b/src/java/org/apache/cassandra/io/util/ReadAheadBuffer.java
similarity index 53%
rename from
src/java/org/apache/cassandra/io/util/ThreadLocalReadAheadBuffer.java
rename to src/java/org/apache/cassandra/io/util/ReadAheadBuffer.java
index 5e78697686..2e85757275 100644
--- a/src/java/org/apache/cassandra/io/util/ThreadLocalReadAheadBuffer.java
+++ b/src/java/org/apache/cassandra/io/util/ReadAheadBuffer.java
@@ -19,39 +19,31 @@
package org.apache.cassandra.io.util;
import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.Map;
-import io.netty.util.concurrent.FastThreadLocal;
+import com.google.common.annotations.VisibleForTesting;
+
import org.apache.cassandra.io.compress.BufferType;
import org.apache.cassandra.io.compress.CorruptBlockException;
-import org.apache.cassandra.io.sstable.CorruptSSTableException;
+import org.apache.cassandra.utils.Closeable;
-public final class ThreadLocalReadAheadBuffer
-{
- private static class Block
- {
- ByteBuffer buffer = null;
- int index = -1;
- }
+import javax.annotation.concurrent.NotThreadSafe;
+/**
+ * A read-ahead buffer for sequential scans of a single file.
+ */
+@NotThreadSafe
+public class ReadAheadBuffer implements Closeable
+{
private final ChannelProxy channel;
-
private final BufferType bufferType;
-
- private static final FastThreadLocal<Map<String, Block>> blockMap = new
FastThreadLocal<>()
- {
- @Override
- protected Map<String, Block> initialValue()
- {
- return new HashMap<>();
- }
- };
-
- private final int bufferSize;
private final long channelSize;
+ private final int bufferSize;
+
+ private ByteBuffer buffer;
+ private int index = -1;
+ private boolean released;
- public ThreadLocalReadAheadBuffer(ChannelProxy channel, int bufferSize,
BufferType bufferType)
+ public ReadAheadBuffer(ChannelProxy channel, int bufferSize, BufferType
bufferType)
{
this.channel = channel;
this.channelSize = channel.size();
@@ -61,42 +53,50 @@ public final class ThreadLocalReadAheadBuffer
public boolean hasBuffer()
{
- return block().buffer != null;
+ return buffer != null;
+ }
+
+ @VisibleForTesting
+ int bufferSize()
+ {
+ return bufferSize;
}
- /**
- * Safe to call only if {@link #hasBuffer()} is true
- */
public int remaining()
{
- return getBlock().buffer.remaining();
+ return allocatedBuffer().remaining();
}
- public void allocateBuffer()
+ @VisibleForTesting
+ void allocateBuffer()
{
- getBlock();
+ getBuffer();
}
- private Block getBlock()
+ private ByteBuffer getBuffer()
{
- Block block = block();
- if (block.buffer == null)
+ if (released)
+ throw new IllegalStateException("read-ahead buffer for " +
channel.filePath() + " has been released");
+
+ if (buffer == null)
{
- block.buffer = bufferType.allocate(bufferSize);
- block.buffer.clear();
+ buffer = bufferType.allocate(bufferSize);
+ buffer.clear();
}
- return block;
+ return buffer;
}
- private Block block()
+ private ByteBuffer allocatedBuffer()
{
- return blockMap.get().computeIfAbsent(channel.filePath(), k -> new
Block());
+ if (buffer == null)
+ throw new IllegalStateException("read-ahead buffer for " +
channel.filePath() + " is not allocated");
+
+ return buffer;
}
public void fill(long position) throws CorruptBlockException
{
- Block block = getBlock();
- ByteBuffer blockBuffer = block.buffer;
+ ByteBuffer blockBuffer = getBuffer();
if (position >= channelSize)
throw new CorruptBlockException(channel.filePath(), position,
bufferSize);
@@ -105,14 +105,11 @@ public final class ThreadLocalReadAheadBuffer
long remaining = channelSize - blockPosition;
int sizeToRead = (int) Math.min(remaining, bufferSize);
- if (block.index != blockNo)
+ if (index != blockNo)
{
blockBuffer.flip();
- blockBuffer.limit(sizeToRead);
- if (channel.read(blockBuffer, blockPosition) != sizeToRead)
- throw new CorruptSSTableException(null, channel.filePath());
-
- block.index = blockNo;
+ loadBlock(blockBuffer, blockPosition, sizeToRead);
+ index = blockNo;
}
blockBuffer.flip();
@@ -120,10 +117,16 @@ public final class ThreadLocalReadAheadBuffer
blockBuffer.position((int) (position - blockPosition));
}
+ protected void loadBlock(ByteBuffer blockBuffer, long blockPosition, int
sizeToRead) throws CorruptBlockException
+ {
+ blockBuffer.limit(sizeToRead);
+ if (channel.read(blockBuffer, blockPosition) != sizeToRead)
+ throw new CorruptBlockException(channel.filePath(), blockPosition,
sizeToRead);
+ }
+
public int read(ByteBuffer dest, int length)
{
- Block block = getBlock();
- ByteBuffer blockBuffer = block.buffer;
+ ByteBuffer blockBuffer = allocatedBuffer();
ByteBuffer tmp = blockBuffer.duplicate();
tmp.limit(tmp.position() + length);
dest.put(tmp);
@@ -132,29 +135,16 @@ public final class ThreadLocalReadAheadBuffer
return length;
}
- public void clear(boolean deallocate)
+ @Override
+ public void close()
{
- // avoid calling block() here to reduce unintended allocations
- Block block = blockMap.get().get(channel.filePath());
- if (block == null)
+ released = true;
+ if (buffer == null)
return;
- block.index = -1;
- if (block.buffer == null)
- return;
-
- ByteBuffer blockBuffer = block.buffer;
- blockBuffer.clear();
- if (deallocate)
- {
- FileUtils.clean(blockBuffer);
- block.buffer = null;
- }
- }
-
- public void close()
- {
- clear(true);
- blockMap.get().remove(channel.filePath());
+ index = -1;
+ buffer.clear();
+ FileUtils.clean(buffer);
+ buffer = null;
}
}
diff --git a/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java
b/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java
index fec1216bf4..3daa19c37a 100644
--- a/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java
+++ b/src/java/org/apache/cassandra/io/util/SimpleChunkReader.java
@@ -55,7 +55,7 @@ class SimpleChunkReader extends AbstractReaderFileProxy
implements ChunkReader
}
@Override
- public Rebufferer instantiateRebufferer(boolean forScan)
+ public Rebufferer instantiateRebufferer(boolean isScan)
{
if (Integer.bitCount(bufferSize) == 1)
return new BufferManagingRebufferer.Aligned(this);
diff --git
a/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java
b/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java
index 710a426647..0d538d731a 100644
---
a/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java
+++
b/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java
@@ -537,6 +537,18 @@ public abstract class ForwardingSSTableReader extends
SSTableReader
return delegate.openDataReader();
}
+ @Override
+ public RandomAccessReader openDataReaderForScan()
+ {
+ return delegate.openDataReaderForScan();
+ }
+
+ @Override
+ public RandomAccessReader openDataReaderForScan(RateLimiter limiter)
+ {
+ return delegate.openDataReaderForScan(limiter);
+ }
+
@Override
public void trySkipFileCacheBefore(DecoratedKey key)
{
diff --git a/test/unit/org/apache/cassandra/cache/ChunkCacheScanBypassTest.java
b/test/unit/org/apache/cassandra/cache/ChunkCacheScanBypassTest.java
new file mode 100644
index 0000000000..8a25967827
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cache/ChunkCacheScanBypassTest.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.cache;
+
+import java.lang.reflect.Constructor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.After;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DataStorageSpec;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.io.compress.CompressedSequentialWriter;
+import org.apache.cassandra.io.compress.CompressionMetadata;
+import org.apache.cassandra.io.filesystem.ListenableFileSystem;
+import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
+import org.apache.cassandra.io.util.ChannelProxy;
+import org.apache.cassandra.io.util.CompressedChunkReader;
+import org.apache.cassandra.io.util.File;
+import org.apache.cassandra.io.util.FileSystems;
+import org.apache.cassandra.io.util.Rebufferer;
+import org.apache.cassandra.io.util.RebuffererFactory;
+import org.apache.cassandra.io.util.SequentialWriterOption;
+import org.apache.cassandra.schema.CompressionParams;
+import org.apache.cassandra.utils.memory.BufferPool;
+import org.apache.cassandra.utils.memory.BufferPools;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ChunkCacheScanBypassTest
+{
+ /** The minimum legal read-ahead buffer size. */
+ private static final int READ_AHEAD_BUFFER_SIZE_IN_KB = 256;
+
+ static
+ {
+ DatabaseDescriptor.clientInitialization();
+ // Client initialization leaves the cache size at zero, which would
make a point read spin forever.
+ DatabaseDescriptor.getRawConfig().file_cache_size = new
DataStorageSpec.IntMebibytesBound(64);
+ }
+
+ private final int originalReadAheadBufferSizeInKb =
DatabaseDescriptor.getCompressedReadAheadBufferSizeInKB();
+
+ @After
+ public void restoreReadAhead()
+ {
+
DatabaseDescriptor.setCompressedReadAheadBufferSizeInKb(originalReadAheadBufferSizeInKb);
+ }
+
+ @Test(timeout = 60_000)
+ public void scanBypassesCacheWhilePointReadPopulatesIt() throws Exception
+ {
+ FileSystems.newGlobalInMemoryFileSystem();
+ File f = new File("/chunk_cache_scan_bypass.db");
+ CompressionMetadata metadata = writeCompressedFile(f, new
File("/chunk_cache_scan_bypass.offset"), new
File("/chunk_cache_scan_bypass.digest"), 200_000);
+ DatabaseDescriptor.setCompressedReadAheadBufferSizeInKb(256);
+
+ ChunkCache cache = newChunkCache();
+ try (ChannelProxy channel = new ChannelProxy(f);
+ CompressedChunkReader reader = new
CompressedChunkReader.Standard(channel, metadata, () -> 1d);
+ CompressionMetadata ignored = metadata)
+ {
+ RebuffererFactory factory = cache.wrap(reader);
+
+ Rebufferer scan = factory.instantiateRebufferer(true);
+ assertThat(scan).isNotSameAs(factory);
+ try
+ {
+ rebufferWholeFile(scan, reader.fileLength(),
reader.chunkSize());
+ assertThat(cache.size()).as("a scan must not populate the
chunk cache").isZero();
+ }
+ finally
+ {
+ // Only release the scan's own buffer. The shared reader stays
open for the point read below.
+ scan.closeReader();
+ }
+
+ cache.close();
+
+ Rebufferer point = factory.instantiateRebufferer(false);
+ assertThat(point).isSameAs(factory);
+ rebufferWholeFile(point, reader.fileLength(), reader.chunkSize());
+ assertThat(cache.size()).as("a point read must populate the chunk
cache").isGreaterThan(0);
+ }
+ finally
+ {
+ cache.close();
+ }
+ }
+
+ /**
+ * A scan bypasses the cache, but it must still batch its reads through
its own read-ahead buffer.
+ */
+ @Test(timeout = 60_000)
+ public void scanUnderCacheUsesReadAheadBuffer() throws Exception
+ {
+ ListenableFileSystem fs = FileSystems.newGlobalInMemoryFileSystem();
+ File f = new File("/chunk_cache_scan_readahead.db");
+ CompressionMetadata metadata = writeCompressedFile(f, new
File("/chunk_cache_scan_readahead.offset"), new
File("/chunk_cache_scan_readahead.digest"), 200_000);
+ DatabaseDescriptor.setCompressedReadAheadBufferSizeInKb(256);
+
+ AtomicInteger reads = new AtomicInteger();
+ fs.onPostRead(f.toPath()::equals, (p, c, pos, dst, r) ->
reads.incrementAndGet());
+
+ ChunkCache cache = newChunkCache();
+ try (ChannelProxy channel = new ChannelProxy(f);
+ CompressedChunkReader reader = new
CompressedChunkReader.Standard(channel, metadata, () -> 1d);
+ CompressionMetadata ignored = metadata)
+ {
+ RebuffererFactory factory = cache.wrap(reader);
+
+ long fileLength = reader.fileLength();
+ int chunkSize = reader.chunkSize();
+ long chunkCount = (fileLength + chunkSize - 1) / chunkSize;
+
+ Rebufferer scan = factory.instantiateRebufferer(true);
+ try
+ {
+ rebufferWholeFile(scan, fileLength, chunkSize);
+ }
+ finally
+ {
+ scan.closeReader();
+ }
+
+ assertThat((long) reads.get()).as("the scan must batch reads
through the read-ahead buffer").isLessThan(chunkCount);
+ assertThat(cache.size()).as("a scan must not populate the chunk
cache").isZero();
+ }
+ finally
+ {
+ cache.close();
+ }
+ }
+
+ @Test
+ public void scanBypassesCacheEvenWithoutReadAhead() throws Exception
+ {
+ // 0 disables read-ahead outright.
+ assertScanBypassesCache("/chunk_cache_scan_no_readahead", 0,
CompressionParams.snappy(4096, 1.1));
+
+ // Standard requires the buffer to be strictly larger than a chunk, so
a chunk length equal to the
+ // read-ahead buffer size also gets no read-ahead, even with the
setting untouched.
+ assertScanBypassesCache("/chunk_cache_scan_big_chunks",
READ_AHEAD_BUFFER_SIZE_IN_KB,
CompressionParams.snappy(READ_AHEAD_BUFFER_SIZE_IN_KB * 1024, 1.1));
+ }
+
+ private void assertScanBypassesCache(String fileNamePrefix, int
readAheadBufferSizeInKb, CompressionParams params) throws Exception
+ {
+ FileSystems.newGlobalInMemoryFileSystem();
+ File f = new File(fileNamePrefix + ".db");
+ CompressionMetadata metadata = writeCompressedFile(f, new
File(fileNamePrefix + ".offset"), new File(fileNamePrefix + ".digest"),
200_000, params);
+
DatabaseDescriptor.setCompressedReadAheadBufferSizeInKb(readAheadBufferSizeInKb);
+
+ ChunkCache cache = newChunkCache();
+ try (ChannelProxy channel = new ChannelProxy(f);
+ CompressedChunkReader reader = new
CompressedChunkReader.Standard(channel, metadata, () -> 1d);
+ CompressionMetadata ignored = metadata)
+ {
+ RebuffererFactory factory = cache.wrap(reader);
+ Rebufferer scan = factory.instantiateRebufferer(true);
+ assertThat(scan).as("a scan must bypass the chunk
cache").isNotSameAs(factory);
+
+ rebufferWholeFile(scan, reader.fileLength(), reader.chunkSize());
+ assertThat(cache.size()).as("a scan must not populate the chunk
cache").isZero();
+ }
+ finally
+ {
+ cache.close();
+ }
+ }
+
+ private static ChunkCache newChunkCache() throws Exception
+ {
+ Constructor<ChunkCache> ctor =
ChunkCache.class.getDeclaredConstructor(BufferPool.class);
+ ctor.setAccessible(true);
+ return ctor.newInstance(BufferPools.forChunkCache());
+ }
+
+ private static CompressionMetadata writeCompressedFile(File f, File
offsets, File digest, long longsToWrite) throws Exception
+ {
+ return writeCompressedFile(f, offsets, digest, longsToWrite,
CompressionParams.snappy(4096, 1.1));
+ }
+
+ private static CompressionMetadata writeCompressedFile(File f, File
offsets, File digest, long longsToWrite, CompressionParams params) throws
Exception
+ {
+ SequentialWriterOption option =
SequentialWriterOption.newBuilder().finishOnClose(false).bufferSize(1 <<
10).build();
+
+ try (CompressedSequentialWriter writer = new
CompressedSequentialWriter(f, offsets, digest, option, params, new
MetadataCollector(new ClusteringComparator())))
+ {
+ for (long i = 0; i < longsToWrite; i++)
+ writer.writeLong(i);
+
+ writer.sync();
+ return writer.open(0);
+ }
+ }
+
+ private static void rebufferWholeFile(Rebufferer rebufferer, long
fileLength, int chunkSize)
+ {
+ long position = 0;
+ do
+ {
+ Rebufferer.BufferHolder holder = rebufferer.rebuffer(position);
+ holder.release();
+ position += chunkSize;
+ }
+ while (position < fileLength);
+ }
+}
diff --git
a/test/unit/org/apache/cassandra/io/util/CompressedChunkReaderTest.java
b/test/unit/org/apache/cassandra/io/util/CompressedChunkReaderTest.java
index 6d1cc35821..7b705483ad 100644
--- a/test/unit/org/apache/cassandra/io/util/CompressedChunkReaderTest.java
+++ b/test/unit/org/apache/cassandra/io/util/CompressedChunkReaderTest.java
@@ -18,8 +18,26 @@
package org.apache.cassandra.io.util;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
+import org.junit.Test;
+
import accord.utils.Gen;
import accord.utils.Gens;
+
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ClusteringComparator;
import org.apache.cassandra.io.compress.CompressedSequentialWriter;
@@ -28,16 +46,6 @@ import
org.apache.cassandra.io.filesystem.ListenableFileSystem;
import org.apache.cassandra.io.sstable.CorruptSSTableException;
import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
import org.apache.cassandra.schema.CompressionParams;
-import org.assertj.core.api.Assertions;
-
-import org.junit.Assert;
-import org.junit.Test;
-
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.file.StandardOpenOption;
-import java.nio.file.Files;
-import java.util.concurrent.atomic.AtomicInteger;
import static accord.utils.Property.qt;
@@ -49,19 +57,18 @@ public class CompressedChunkReaderTest
}
@Test
- public void scanReaderReadsLessThanRAReader()
+ public void scanReaderIssuesFewerPhysicalReadsThanRandomAccessReader()
{
var optionGen = options();
var paramsGen = params();
var lengthGen = Gens.longs().between(1, 1 << 16);
- qt().withSeed(-1871070464864118891L).forAll(Gens.random(), optionGen,
paramsGen).check((rs, option, params) -> {
+
+ qt().forAll(Gens.random(), optionGen, paramsGen).check((rs, option,
params) -> {
ListenableFileSystem fs =
FileSystems.newGlobalInMemoryFileSystem();
- File f = new File("/file.db");
+ File f = new File("/file.bin");
AtomicInteger reads = new AtomicInteger();
- fs.onPostRead(f.path::equals, (p, c, pos, dst, r) -> {
- reads.incrementAndGet();
- });
+ fs.onPostRead(f.path::equals, (p, c, pos, dst, r) ->
reads.incrementAndGet());
long length = lengthGen.nextLong(rs);
CompressionMetadata metadata1, metadata2;
try (CompressedSequentialWriter writer = new
CompressedSequentialWriter(f, new File("/file.offset"), new
File("/file.digest"), option, params, new MetadataCollector(new
ClusteringComparator())))
@@ -74,14 +81,14 @@ public class CompressedChunkReaderTest
metadata2 = writer.open(0);
}
- doReads(f, metadata1, length, true);
+ doReads(f, metadata1, length, false);
+ int randomAccessReads = reads.getAndSet(0);
+
+ doReads(f, metadata2, length, true);
int scanReads = reads.getAndSet(0);
- doReads(f, metadata2, length, false);
- int raReads = reads.getAndSet(0);
-
if (Files.size(f.toPath()) >
DatabaseDescriptor.getCompressedReadAheadBufferSize())
- Assert.assertTrue(scanReads < raReads);
+ Assert.assertTrue(scanReads <= randomAccessReads);
});
}
@@ -89,33 +96,42 @@ public class CompressedChunkReaderTest
{
ByteBuffer buffer = ByteBuffer.allocateDirect(metadata.chunkLength());
- try (ChannelProxy channel = new ChannelProxy(f);
- CompressedChunkReader reader = new
CompressedChunkReader.Standard(channel, metadata, () -> 1.1);
- metadata)
+ try (ChannelProxy channel = new ChannelProxy(f))
{
- if (useReadAhead)
- reader.forScan();
-
- long offset = 0;
- long maxOffset = length * Long.BYTES;
- do
+ try (CompressedChunkReader reader = new
CompressedChunkReader.Standard(channel, metadata, () -> 1d);
+ metadata)
{
- reader.readChunk(offset, buffer);
- for (long expected = offset / Long.BYTES;
buffer.hasRemaining(); expected++)
-
Assertions.assertThat(buffer.getLong()).isEqualTo(expected);
+ CompressedChunkReader scanReader = useReadAhead ?
reader.forScan() : reader;
+ try
+ {
+ long offset = 0;
+ long maxOffset = length * Long.BYTES;
+ do
+ {
+ scanReader.readChunk(offset, buffer);
+ for (long expected = offset / Long.BYTES;
buffer.hasRemaining(); expected++)
+
Assertions.assertThat(buffer.getLong()).isEqualTo(expected);
- offset += metadata.chunkLength();
+ offset += metadata.chunkLength();
+ }
+ while (offset < maxOffset);
+ }
+ finally
+ {
+ if (scanReader != reader)
+ scanReader.close();
+ }
}
- while (offset < maxOffset);
}
finally
{
FileUtils.clean(buffer);
- }}
+ }
+ }
private static Gen<SequentialWriterOption> options()
{
- Gen<Integer> bufferSizes = Gens.constant(1 << 10); //.pickInt(1 << 4,
1 << 10, 1 << 15);
+ Gen<Integer> bufferSizes = Gens.constant(1 << 10);
return rs -> SequentialWriterOption.newBuilder()
.finishOnClose(false)
.bufferSize(bufferSizes.next(rs))
@@ -181,15 +197,100 @@ public class CompressedChunkReaderTest
CompressedChunkReader reader = new
CompressedChunkReader.Standard(channel, metadata, () -> 1.1);
metadata)
{
- reader.forScan();
-
- Assertions.assertThatThrownBy(() ->
reader.readChunk(lastChunkUncompressedStart, buffer))
- .as("readChunk() reading past truncated EOF via the scan
path")
- .isInstanceOf(CorruptSSTableException.class);
+ CompressedChunkReader scanReader = reader.forScan();
+ try
+ {
+ Assertions.assertThatThrownBy(() ->
scanReader.readChunk(lastChunkUncompressedStart, buffer))
+ .as("readChunk() reading past truncated EOF via the
scan path")
+ .isInstanceOf(CorruptSSTableException.class);
+ }
+ finally
+ {
+ if (scanReader != reader)
+ scanReader.close();
+ }
}
finally
{
FileUtils.clean(buffer);
}
}
-}
\ No newline at end of file
+
+ /*
+ * Two or more concurrent scans of one file must not corrupt each other or
double-free a shared buffer.
+ */
+ @Test(timeout = 60_000)
+ public void concurrentScansOfOneReaderAreIndependent() throws Exception
+ {
+
+ SequentialWriterOption writerOption =
SequentialWriterOption.newBuilder().finishOnClose(false).bufferSize(1 <<
10).build();
+ CompressionParams params = CompressionParams.snappy(4096, 1.1);
+
+ FileSystems.newGlobalInMemoryFileSystem();
+ File f = new File("/concurrent_scans.db");
+ File offsets = new File("/concurrent_scans.offset");
+ File digest = new File("/concurrent_scans.digest");
+
+ long longsToWrite = 200_000; // spans many read-ahead blocks
+ CompressionMetadata metadata;
+ try (CompressedSequentialWriter writer = new
CompressedSequentialWriter(f, offsets, digest, writerOption, params, new
MetadataCollector(new ClusteringComparator())))
+ {
+ for (long i = 0; i < longsToWrite; i++)
+ writer.writeLong(i);
+ writer.sync();
+ metadata = writer.open(0);
+ }
+
+ // Minium legal buffer size. (This should span many blocks over the
test file.)
+ DatabaseDescriptor.setCompressedReadAheadBufferSizeInKb(256);
+
+ int threads = 4;
+ long maxOffset = longsToWrite * Long.BYTES;
+ try (ChannelProxy channel = new ChannelProxy(f);
+ CompressedChunkReader reader = new
CompressedChunkReader.Standard(channel, metadata, () -> 1d);
+ metadata)
+ {
+ ExecutorService pool = Executors.newFixedThreadPool(threads);
+ CyclicBarrier barrier = new CyclicBarrier(threads);
+ List<Future<?>> futures = new ArrayList<>();
+ try
+ {
+ for (int t = 0; t < threads; t++)
+ {
+ futures.add(pool.submit(() -> {
+ ByteBuffer buffer =
ByteBuffer.allocateDirect(metadata.chunkLength());
+ CompressedChunkReader scanReader = reader.forScan();
+ try
+ {
+ barrier.await(); // start all scans together to
maximise overlap
+ long offset = 0;
+ do
+ {
+ scanReader.readChunk(offset, buffer);
+ for (long expected = offset / Long.BYTES;
buffer.hasRemaining(); expected++)
+
Assertions.assertThat(buffer.getLong()).isEqualTo(expected);
+
+ offset += metadata.chunkLength();
+ }
+ while (offset < maxOffset);
+ return null;
+ }
+ finally
+ {
+ if (scanReader != reader)
+ scanReader.close();
+ FileUtils.clean(buffer);
+ }
+ }));
+ }
+
+ for (Future<?> future : futures)
+ future.get(45, TimeUnit.SECONDS);
+ }
+ finally
+ {
+ pool.shutdownNow();
+ }
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/io/util/ReadAheadBufferTest.java
b/test/unit/org/apache/cassandra/io/util/ReadAheadBufferTest.java
new file mode 100644
index 0000000000..b8182135aa
--- /dev/null
+++ b/test/unit/org/apache/cassandra/io/util/ReadAheadBufferTest.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.io.util;
+
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Random;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.quicktheories.WithQuickTheories;
+import org.quicktheories.core.Gen;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DataStorageSpec;
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.compress.CorruptBlockException;
+import org.apache.cassandra.io.sstable.CorruptSSTableException;
+import org.apache.cassandra.utils.Pair;
+
+import static java.lang.Math.max;
+import static
org.apache.cassandra.config.CassandraRelevantProperties.JAVA_IO_TMPDIR;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ReadAheadBufferTest implements WithQuickTheories
+{
+ private static final Logger logger =
LoggerFactory.getLogger(ReadAheadBufferTest.class);
+
+ private static final int BUFFER_SIZE = new
DataStorageSpec.IntKibibytesBound("256KiB").toBytes();
+ private static final int NUM_FILES = 5;
+ protected static final File[] files = new File[NUM_FILES];
+ protected static Integer seed;
+
+ @BeforeClass
+ public static void setup()
+ {
+ seed = new Random().nextInt();
+ logger.info("Seed: {}", seed);
+
+ Random seeded = new Random(seed);
+ for (int i = 0; i < NUM_FILES; i++)
+ {
+ int size = seeded.nextInt((Integer.MAX_VALUE - 1) / 8);
+ files[i] = writeFile(seed, size);
+ }
+ }
+
+ @AfterClass
+ public static void cleanup()
+ {
+ for (File f : files)
+ {
+ try
+ {
+ f.delete();
+ }
+ catch (Exception e)
+ {
+ // ignore
+ }
+ }
+ }
+
+ @Test
+ public void testLastBlockReads()
+ {
+
qt().withFixedSeed(seed).forAll(lastBlockReads()).checkAssert(this::testReads);
+ }
+
+ @Test
+ public void testReadsLikeChannelProxy()
+ {
+ qt().withFixedSeed(seed).forAll(reads()).checkAssert(this::testReads);
+ }
+
+ @Test
+ public void allocateInitialisesBufferSizeFromCapacity()
+ {
+ try (ChannelProxy channel = new ChannelProxy(files[0]))
+ {
+ try (ReadAheadBuffer buffer = new ReadAheadBuffer(channel,
BUFFER_SIZE, BufferType.OFF_HEAP))
+ {
+ assertFalse(buffer.hasBuffer());
+
+ buffer.allocateBuffer();
+
+ // Allocation must record the actual buffer size, not leave it
unset.
+ assertTrue(buffer.hasBuffer());
+ assertEquals("allocate must initialise bufferSize from
capacity", BUFFER_SIZE, buffer.bufferSize());
+ }
+ }
+ }
+
+ @Test
+ public void independentInstancesDoNotShareBuffer() throws
CorruptBlockException
+ {
+
+ File file = files[0];
+ try (ChannelProxy channel = new ChannelProxy(file))
+ {
+ try (ReadAheadBuffer a = new ReadAheadBuffer(channel, BUFFER_SIZE,
BufferType.OFF_HEAP); ReadAheadBuffer b = new ReadAheadBuffer(channel,
BUFFER_SIZE, BufferType.OFF_HEAP))
+ {
+ a.fill(0);
+
+ // Advance b to a different block if the file is large enough;
else keep it on block 0.
+ long secondBlock = BUFFER_SIZE;
+ b.fill(channel.size() > secondBlock ? secondBlock : 0);
+
+ // a must still read block 0. A shared buffer would return b's
block here.
+ int readSize = Math.min(100, (int) channel.size());
+ ByteBuffer expected = ByteBuffer.allocate(readSize);
+ channel.read(expected, 0);
+ expected.flip();
+
+ ByteBuffer actual = ByteBuffer.allocate(readSize);
+ a.read(actual, readSize);
+ actual.flip();
+
+ assertEquals(expected, actual);
+ }
+ }
+ }
+
+ @Test
+ public void closeFreesBufferAndIsIdempotent()
+ {
+ try (ChannelProxy channel = new ChannelProxy(files[0]))
+ {
+ ReadAheadBuffer buffer = new ReadAheadBuffer(channel, BUFFER_SIZE,
BufferType.OFF_HEAP);
+ buffer.allocateBuffer();
+ assertTrue(buffer.hasBuffer());
+
+ buffer.close();
+ assertFalse("close must free the owned buffer",
buffer.hasBuffer());
+
+ // A second close must not double-free.
+ buffer.close();
+ assertFalse(buffer.hasBuffer());
+ }
+ }
+
+ protected void testReads(InputData propertyInputs)
+ {
+ try (ChannelProxy channel = new ChannelProxy(propertyInputs.file);
+ ReadAheadBuffer rab = new ReadAheadBuffer(channel, BUFFER_SIZE,
BufferType.OFF_HEAP); )
+ {
+ for (Pair<Long, Integer> read : propertyInputs.positionsAndLengths)
+ {
+ testRead(read, channel, rab);
+ }
+ }
+ }
+
+ protected static void testRead(Pair<Long, Integer> read, ChannelProxy
bufferedChannel, ReadAheadBuffer rab)
+ {
+ int readSize = Math.min(read.right, (int) (bufferedChannel.size() -
read.left));
+ ByteBuffer buf1 = ByteBuffer.allocate(readSize);
+ bufferedChannel.read(buf1, read.left);
+
+ ByteBuffer buf2 = ByteBuffer.allocate(readSize);
+ try
+ {
+ int copied = 0;
+ while (copied < readSize)
+ {
+ rab.fill(read.left + copied);
+ int leftToRead = readSize - copied;
+ if (rab.remaining() >= leftToRead)
+ copied += rab.read(buf2, leftToRead);
+ else
+ copied += rab.read(buf2, rab.remaining());
+ }
+ }
+ catch (CorruptSSTableException | CorruptBlockException e)
+ {
+ throw new RuntimeException(e);
+ }
+
+ assertEquals(buf1, buf2);
+ }
+
+ protected Gen<InputData> reads()
+ {
+ return arbitrary().pick(List.of(files))
+ .flatMap((file) ->
+ lists().of(longs().between(0,
fileSize(file)).zip(integers().between(1, 100), Pair::create))
+ .ofSizeBetween(5, 10)
+ .map(positionsAndLengths -> new
InputData(file, positionsAndLengths)));
+ }
+
+ protected Gen<InputData> lastBlockReads()
+ {
+ return arbitrary().pick(List.of(files))
+ .flatMap((file) ->
+ lists().of(longs().between(max(0,
fileSize(file) - BUFFER_SIZE), fileSize(file)).zip(integers().between(1, 100),
Pair::create))
+ .ofSizeBetween(5, 10)
+ .map(positionsAndLengths -> new
InputData(file, positionsAndLengths)));
+ }
+
+ private long fileSize(File file)
+ {
+ try
+ {
+ return Files.size(file.toPath());
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected static class InputData
+ {
+
+ protected final File file;
+ protected final List<Pair<Long, Integer>> positionsAndLengths;
+
+ public InputData(File file, List<Pair<Long, Integer>>
positionsAndLengths)
+ {
+ this.file = file;
+ this.positionsAndLengths = positionsAndLengths;
+ }
+ }
+
+ private static File writeFile(int seed, int length)
+ {
+ String fileName = "data+" + length + ".bin";
+
+ byte[] dataChunk = new byte[4096 * 8];
+ java.util.Random random = new Random(seed);
+ int writtenData = 0;
+
+ File file = new File(JAVA_IO_TMPDIR.getString(), fileName);
+ try (FileOutputStream fos = new FileOutputStream(file.toJavaIOFile()))
+ {
+ while (writtenData < length)
+ {
+ random.nextBytes(dataChunk);
+ int toWrite = Math.min((length - writtenData),
dataChunk.length);
+ fos.write(dataChunk, 0, toWrite);
+ writtenData += toWrite;
+ }
+ fos.flush();
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+
+ return file;
+ }
+}
diff --git
a/test/unit/org/apache/cassandra/io/util/ThreadLocalReadAheadBufferTest.java
b/test/unit/org/apache/cassandra/io/util/ThreadLocalReadAheadBufferTest.java
deleted file mode 100644
index 2d29504d6f..0000000000
--- a/test/unit/org/apache/cassandra/io/util/ThreadLocalReadAheadBufferTest.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.cassandra.io.util;
-
-
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.file.Files;
-import java.util.List;
-import java.util.Random;
-
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.cassandra.config.DataStorageSpec;
-import org.apache.cassandra.io.compress.BufferType;
-import org.apache.cassandra.io.compress.CorruptBlockException;
-import org.apache.cassandra.io.sstable.CorruptSSTableException;
-import org.apache.cassandra.utils.Pair;
-import org.quicktheories.WithQuickTheories;
-import org.quicktheories.core.Gen;
-
-import static java.lang.Math.max;
-import static
org.apache.cassandra.config.CassandraRelevantProperties.JAVA_IO_TMPDIR;
-
-public class ThreadLocalReadAheadBufferTest implements WithQuickTheories
-{
- private static final int numFiles = 5;
- private static final File[] files = new File[numFiles];
- private static final Logger logger =
LoggerFactory.getLogger(ThreadLocalReadAheadBufferTest.class);
- private static Integer seed;
-
- @BeforeClass
- public static void setup()
- {
- seed = new Random().nextInt();
- logger.info("Seed: {}", seed);
-
- for (int i = 0; i < numFiles; i++)
- {
- int size = new Random(seed).nextInt((Integer.MAX_VALUE - 1) / 8);
- files[i] = writeFile(seed, size);
- }
- }
-
- @AfterClass
- public static void cleanup()
- {
- for (File f : files)
- {
- try
- {
- f.delete();
- }
- catch (Exception e)
- {
- // ignore
- }
- }
- }
-
- @Test
- public void testLastBlockReads()
- {
- qt().withFixedSeed(seed).forAll(lastBlockReads())
- .checkAssert(this::testReads);
- }
-
- @Test
- public void testReadsLikeChannelProxy()
- {
-
- qt().withFixedSeed(seed).forAll(reads())
- .checkAssert(this::testReads);
- }
-
- private void testReads(InputData propertyInputs)
- {
- try (ChannelProxy channel = new ChannelProxy(propertyInputs.file))
- {
- ThreadLocalReadAheadBuffer trlab = new
ThreadLocalReadAheadBuffer(channel, new
DataStorageSpec.IntKibibytesBound("256KiB").toBytes(), BufferType.OFF_HEAP);
- for (Pair<Long, Integer> read : propertyInputs.positionsAndLengths)
- {
- int readSize = Math.min(read.right,(int) (channel.size() -
read.left));
- ByteBuffer buf1 = ByteBuffer.allocate(readSize);
- channel.read(buf1, read.left);
-
- ByteBuffer buf2 = ByteBuffer.allocate(readSize);
- try
- {
- int copied = 0;
- while (copied < readSize) {
- trlab.fill(read.left + copied);
- int leftToRead = readSize - copied;
- if (trlab.remaining() >= leftToRead)
- copied += trlab.read(buf2, leftToRead);
- else
- copied += trlab.read(buf2, trlab.remaining());
- }
- }
- catch (CorruptSSTableException | CorruptBlockException e)
- {
- throw new RuntimeException(e);
- }
-
- Assert.assertEquals(buf1, buf2);
- }
- }
- }
-
- private Gen<InputData> reads()
- {
- return arbitrary().pick(List.of(files))
- .flatMap((file) ->
- lists().of(longs().between(0,
fileSize(file)).zip(integers().between(1, 100), Pair::create))
- .ofSizeBetween(5, 10)
- .map(positionsAndLengths -> new
InputData(file, positionsAndLengths)));
-
- }
-
- private Gen<InputData> lastBlockReads()
- {
- int blockSize = new
DataStorageSpec.IntKibibytesBound("256KiB").toBytes();
- return arbitrary().pick(List.of(files))
- .flatMap((file) ->
- lists().of(longs().between(max(0,
fileSize(file) - blockSize), fileSize(file)).zip(integers().between(1, 100),
Pair::create))
- .ofSizeBetween(5, 10)
- .map(positionsAndLengths -> new
InputData(file, positionsAndLengths)));
-
- }
-
- // need this becasue generators don't handle the IOException
- private long fileSize(File file)
- {
- try
- {
- return Files.size(file.toPath());
- } catch (IOException e)
- {
- throw new RuntimeException(e);
- }
- }
-
- private static class InputData
- {
-
- private final File file;
- private final List<Pair<Long, Integer>> positionsAndLengths;
-
- public InputData(File file, List<Pair<Long, Integer>>
positionsAndLengths)
- {
- this.file = file;
- this.positionsAndLengths = positionsAndLengths;
- }
- }
-
- private static File writeFile(int seed, int length)
- {
- String fileName = "data+" + length + ".bin";
-
- byte[] dataChunk = new byte[4096 * 8];
- java.util.Random random = new Random(seed);
- int writtenData = 0;
-
- File file = new File(JAVA_IO_TMPDIR.getString(), fileName);
- try (FileOutputStream fos = new FileOutputStream(file.toJavaIOFile()))
- {
- while (writtenData < length)
- {
- random.nextBytes(dataChunk);
- int toWrite = Math.min((length - writtenData),
dataChunk.length);
- fos.write(dataChunk, 0, toWrite);
- writtenData += toWrite;
- }
- fos.flush();
- }
- catch (IOException e)
- {
- throw new RuntimeException(e);
- }
-
- return file;
- }
-
-}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]