Caleb Rackliffe created CASSANDRA-21684:
-------------------------------------------

             Summary: Unbounded per-thread cache of read-ahead buffers can 
cause slow heap leak
                 Key: CASSANDRA-21684
                 URL: https://issues.apache.org/jira/browse/CASSANDRA-21684
             Project: Apache Cassandra
          Issue Type: Bug
          Components: Local/Compaction
            Reporter: Caleb Rackliffe
            Assignee: Caleb Rackliffe


{{ThreadLocalReadAheadBuffer}} caches one read-ahead Block per scanned file in 
a per-thread map (blockMap), keyed by file path, so a thread can reuse its 
buffer across repeated scans of the same SSTable. That map has no upper bound 
and entries are only removed when the same thread that populated one also 
happens to run the file's final close, which rarely lines up, since SSTables 
are typically scanned by several different pool threads (compaction, repair, 
reads) over their lifetime. The result: every long-lived thread accumulates a 
permanent stale entry for every distinct SSTable it has ever scanned, growing 
unbounded as compaction continuously creates new SSTable generations.

{noformat}
@Test
public void releasingScanResourcesShouldRemoveThreadLocalBlockMapEntry() 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("/scan_release_repro.db");
    File offsets = new File("/scan_release_repro.offset");
    File digest = new File("/scan_release_repro.digest");

    long longsToWrite = 600;
    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);
    }

    DatabaseDescriptor.setCompressedReadAheadBufferSizeInKb(256);

    ByteBuffer buffer = ByteBuffer.allocateDirect(metadata.chunkLength());
    try (ChannelProxy channel = new ChannelProxy(f);
         CompressedChunkReader reader = new 
CompressedChunkReader.Standard(channel, metadata, () -> 1.1);
         metadata)
    {
        reader.forScan();
        reader.readChunk(0, buffer);

        Assert.assertTrue("expected a read-ahead Block to be present for this 
file on this thread after a scan read",
                           threadLocalBlockMapContainsKey(channel.filePath()));

        // Mirrors what BufferManagingRebufferer.closeReader() does at the end 
of every scan.
        reader.releaseUnderlyingResources();

        Assert.assertFalse("a stale Block entry for this file should not remain 
in this thread's read-ahead " +
                            "blockMap once the scan has released its resources 
-- otherwise every distinct " +
                            "file this thread ever scans leaks a permanent 
entry",
                            threadLocalBlockMapContainsKey(channel.filePath()));
    }
    finally
    {
        FileUtils.clean(buffer);
    }
}

@SuppressWarnings("unchecked")
private static boolean threadLocalBlockMapContainsKey(String filePath) throws 
Exception
{
    Field field = ThreadLocalReadAheadBuffer.class.getDeclaredField("blockMap");
    field.setAccessible(true);
    FastThreadLocal<Map<String, ?>> blockMap = (FastThreadLocal<Map<String, 
?>>) field.get(null);
    return blockMap.get().containsKey(filePath);
}
{noformat}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to