From 46d723004da7478bc471a9ae8398f4c0440d08d2 Mon Sep 17 00:00:00 2001 From: Mykyta Bozhenko <21245729+cheeeee@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:49:43 -0400 Subject: [PATCH] Preserve offsets when splitting large cache eviction ranges Advance the offset after each bounded native request and preserve the requested starting offset when length zero means the remainder of a file. Cover multi-chunk ranges and the zero-length boundary at the native call interface. CASSANDRA-21085 Generated-by: Claude (Anthropic) --- CHANGES.txt | 1 + .../apache/cassandra/utils/NativeLibrary.java | 15 +++- .../cassandra/utils/NativeLibraryTest.java | 71 +++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index aa356a2da163..3e14b3d94a75 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 7.0 + * Preserve offsets when splitting cache eviction requests larger than 2 GiB and when advising through EOF (CASSANDRA-21085) * Allow CQLSSTableWriter to specify SSTable id generator to use (CASSANDRA-21012) * Reject LIKE patterns with a wildcard (%) anywhere other than the start or end (CASSANDRA-21068) * Support pluggable default role initialization (CASSANDRA-21546) diff --git a/src/java/org/apache/cassandra/utils/NativeLibrary.java b/src/java/org/apache/cassandra/utils/NativeLibrary.java index ec00162e195f..6984ca3c212d 100644 --- a/src/java/org/apache/cassandra/utils/NativeLibrary.java +++ b/src/java/org/apache/cassandra/utils/NativeLibrary.java @@ -23,6 +23,7 @@ import java.nio.channels.FileChannel; import java.util.concurrent.TimeUnit; +import com.google.common.annotations.VisibleForTesting; import com.sun.jna.LastErrorException; import org.slf4j.Logger; @@ -73,6 +74,11 @@ public enum OSType private static final int POSIX_FADV_DONTNEED = 4; /* fadvise.h */ private static final int POSIX_FADV_NOREUSE = 5; /* fadvise.h */ + // POSIX_FADV_DONTNEED only discards fully covered pages, so chunk on a boundary that is a multiple of + // every common page size (4K, 16K, 64K) instead of the unaligned Integer.MAX_VALUE. + @VisibleForTesting + static final int FADVISE_MAX_CHUNK = Integer.MAX_VALUE & ~((1 << 21) - 1); + private static final NativeLibraryWrapper wrappedLibrary; private static boolean jnaLockable = false; @@ -228,14 +234,17 @@ public static void trySkipCache(String path, long offset, long len) public static void trySkipCache(int fd, long offset, long len, String path) { if (len == 0) - trySkipCache(fd, 0, 0, path); + { + trySkipCache(fd, offset, 0, path); + return; + } while (len > 0) { - int sublen = (int) Math.min(Integer.MAX_VALUE, len); + int sublen = (int) Math.min(FADVISE_MAX_CHUNK, len); trySkipCache(fd, offset, sublen, path); len -= sublen; - offset -= sublen; + offset += sublen; } } diff --git a/test/unit/org/apache/cassandra/utils/NativeLibraryTest.java b/test/unit/org/apache/cassandra/utils/NativeLibraryTest.java index 1856af1a645b..7d1d4fc819f4 100644 --- a/test/unit/org/apache/cassandra/utils/NativeLibraryTest.java +++ b/test/unit/org/apache/cassandra/utils/NativeLibraryTest.java @@ -18,15 +18,25 @@ */ package org.apache.cassandra.utils; +import java.util.ArrayList; +import java.util.List; +import org.jboss.byteman.contrib.bmunit.BMRule; +import org.jboss.byteman.contrib.bmunit.BMUnitRunner; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; +import static org.junit.Assume.assumeTrue; + +@RunWith(BMUnitRunner.class) public class NativeLibraryTest { + private static final ThreadLocal> advisedRanges = new ThreadLocal<>(); + @Test public void testSkipCache() { @@ -35,6 +45,67 @@ public void testSkipCache() NativeLibrary.trySkipCache(file.path(), 0, 0); } + @Test + @BMRule(name = "record large skip-cache ranges", + targetClass = "org.apache.cassandra.utils.NativeLibraryLinux", + targetMethod = "callPosixFadvise(int, long, int, int)", + targetLocation = "AT ENTRY", + condition = "org.apache.cassandra.utils.NativeLibraryTest.isRecording()", + action = "return org.apache.cassandra.utils.NativeLibraryTest.recordAdvice($2, $3)") + public void testSkipCacheLargeRange() + { + long offset = 4096; + // Chunks are aligned down to 2 MiB so no page straddles a boundary and is left in cache. + long chunk = NativeLibrary.FADVISE_MAX_CHUNK; + assertAdvisedRanges(offset, 2 * chunk + 17, + new long[][] { { offset, chunk }, + { offset + chunk, chunk }, + { offset + 2 * chunk, 17 } }); + } + + @Test + @BMRule(name = "record zero-length skip-cache range", + targetClass = "org.apache.cassandra.utils.NativeLibraryLinux", + targetMethod = "callPosixFadvise(int, long, int, int)", + targetLocation = "AT ENTRY", + condition = "org.apache.cassandra.utils.NativeLibraryTest.isRecording()", + action = "return org.apache.cassandra.utils.NativeLibraryTest.recordAdvice($2, $3)") + public void testSkipCacheZeroLength() + { + long offset = 4096; + assertAdvisedRanges(offset, 0L, new long[][] { { offset, 0 } }); + } + + private static void assertAdvisedRanges(long offset, long length, long[][] expected) + { + assumeTrue("Range cache advice is Linux-only", FBUtilities.isLinux); + List actual = new ArrayList<>(); + advisedRanges.set(actual); + try + { + // Record at the native wrapper, without allocating a file or issuing a syscall. + NativeLibrary.trySkipCache(Integer.MAX_VALUE, offset, length, "recorded-skip-cache"); + Assert.assertEquals("Native advice call count", expected.length, actual.size()); + for (int i = 0; i < expected.length; i++) + Assert.assertArrayEquals("Native advice range " + i, expected[i], actual.get(i)); + } + finally + { + advisedRanges.remove(); + } + } + + public static boolean isRecording() + { + return advisedRanges.get() != null; + } + + public static int recordAdvice(long offset, int length) + { + advisedRanges.get().add(new long[] { offset, length }); + return 0; + } + @Test public void getPid() {