From da41215a678f7c1a72cef558594031a99db90d88 Mon Sep 17 00:00:00 2001
From: Chris Hegarty <62058229+ChrisHegarty@users.noreply.github.com>
Date: Wed, 10 Jul 2024 09:39:35 +0100
Subject: [PATCH] Use a confined Arena for IOContext.READONCE (#13535)
Use a confined Arena for IOContext.READONCE.
This change will require inputs opened with READONCE to be consumed and closed on the creating thread. Further testing and assertions can be added as a follow up.
---
.../simpletext/SimpleTextDocValuesReader.java | 2 +-
.../simpletext/SimpleTextPointsReader.java | 2 +-
.../org/apache/lucene/store/IOContext.java | 7 ++-
.../lucene/store/MemorySegmentIndexInput.java | 43 +++++++++++----
.../MemorySegmentIndexInputProvider.java | 6 +-
.../lucene/store/TestMMapDirectory.java | 55 +++++++++++++++++++
.../tests/store/MockDirectoryWrapper.java | 11 ++--
.../tests/store/MockIndexInputWrapper.java | 39 ++++++++++++-
.../SlowClosingMockIndexInputWrapper.java | 4 +-
.../SlowOpeningMockIndexInputWrapper.java | 5 +-
.../lucene/tests/util/LuceneTestCase.java | 12 ++--
11 files changed, 153 insertions(+), 33 deletions(-)
diff --git a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextDocValuesReader.java b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextDocValuesReader.java
index f58ff0873ca..435c2f73fdf 100644
--- a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextDocValuesReader.java
+++ b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextDocValuesReader.java
@@ -829,7 +829,7 @@ class SimpleTextDocValuesReader extends DocValuesProducer {
clone.seek(0);
// checksum is fixed-width encoded with 20 bytes, plus 1 byte for newline (the space is included
// in SimpleTextUtil.CHECKSUM):
- long footerStartPos = data.length() - (SimpleTextUtil.CHECKSUM.length + 21);
+ long footerStartPos = clone.length() - (SimpleTextUtil.CHECKSUM.length + 21);
ChecksumIndexInput input = new BufferedChecksumIndexInput(clone);
while (true) {
SimpleTextUtil.readLine(input, scratch);
diff --git a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPointsReader.java b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPointsReader.java
index be0e98f906a..5d6c41663ca 100644
--- a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPointsReader.java
+++ b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPointsReader.java
@@ -227,7 +227,7 @@ class SimpleTextPointsReader extends PointsReader {
// checksum is fixed-width encoded with 20 bytes, plus 1 byte for newline (the space is included
// in SimpleTextUtil.CHECKSUM):
- long footerStartPos = dataIn.length() - (SimpleTextUtil.CHECKSUM.length + 21);
+ long footerStartPos = clone.length() - (SimpleTextUtil.CHECKSUM.length + 21);
ChecksumIndexInput input = new BufferedChecksumIndexInput(clone);
while (true) {
SimpleTextUtil.readLine(input, scratch);
diff --git a/lucene/core/src/java/org/apache/lucene/store/IOContext.java b/lucene/core/src/java/org/apache/lucene/store/IOContext.java
index b2d82af20f8..f318b3a9015 100644
--- a/lucene/core/src/java/org/apache/lucene/store/IOContext.java
+++ b/lucene/core/src/java/org/apache/lucene/store/IOContext.java
@@ -55,7 +55,12 @@ public record IOContext(
*/
public static final IOContext DEFAULT = new IOContext(Constants.DEFAULT_READADVICE);
- /** A default context for reads with {@link ReadAdvice#SEQUENTIAL}. */
+ /**
+ * A default context for reads with {@link ReadAdvice#SEQUENTIAL}.
+ *
+ *
This context should only be used when the read operations will be performed in the same
+ * thread as the thread that opens the underlying storage.
+ */
public static final IOContext READONCE = new IOContext(ReadAdvice.SEQUENTIAL);
@SuppressWarnings("incomplete-switch")
diff --git a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java
index 68f1e771195..e9805f0f7a6 100644
--- a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java
+++ b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java
@@ -53,6 +53,7 @@ abstract class MemorySegmentIndexInput extends IndexInput
final long length;
final long chunkSizeMask;
final int chunkSizePower;
+ final boolean confined;
final Arena arena;
final MemorySegment[] segments;
@@ -67,12 +68,15 @@ abstract class MemorySegmentIndexInput extends IndexInput
Arena arena,
MemorySegment[] segments,
long length,
- int chunkSizePower) {
+ int chunkSizePower,
+ boolean confined) {
assert Arrays.stream(segments).map(MemorySegment::scope).allMatch(arena.scope()::equals);
if (segments.length == 1) {
- return new SingleSegmentImpl(resourceDescription, arena, segments[0], length, chunkSizePower);
+ return new SingleSegmentImpl(
+ resourceDescription, arena, segments[0], length, chunkSizePower, confined);
} else {
- return new MultiSegmentImpl(resourceDescription, arena, segments, 0, length, chunkSizePower);
+ return new MultiSegmentImpl(
+ resourceDescription, arena, segments, 0, length, chunkSizePower, confined);
}
}
@@ -81,12 +85,14 @@ abstract class MemorySegmentIndexInput extends IndexInput
Arena arena,
MemorySegment[] segments,
long length,
- int chunkSizePower) {
+ int chunkSizePower,
+ boolean confined) {
super(resourceDescription);
this.arena = arena;
this.segments = segments;
this.length = length;
this.chunkSizePower = chunkSizePower;
+ this.confined = confined;
this.chunkSizeMask = (1L << chunkSizePower) - 1L;
this.curSegment = segments[0];
}
@@ -97,6 +103,12 @@ abstract class MemorySegmentIndexInput extends IndexInput
}
}
+ void ensureAccessible() {
+ if (confined && curSegment.isAccessibleBy(Thread.currentThread()) == false) {
+ throw new IllegalStateException("confined");
+ }
+ }
+
// the unused parameter is just to silence javac about unused variables
RuntimeException handlePositionalIOOBE(RuntimeException unused, String action, long pos)
throws IOException {
@@ -570,6 +582,7 @@ abstract class MemorySegmentIndexInput extends IndexInput
/** Builds the actual sliced IndexInput (may apply extra offset in subclasses). * */
MemorySegmentIndexInput buildSlice(String sliceDescription, long offset, long length) {
ensureOpen();
+ ensureAccessible();
final long sliceEnd = offset + length;
final int startIndex = (int) (offset >>> chunkSizePower);
@@ -591,7 +604,8 @@ abstract class MemorySegmentIndexInput extends IndexInput
null, // clones don't have an Arena, as they can't close)
slices[0].asSlice(offset, length),
length,
- chunkSizePower);
+ chunkSizePower,
+ confined);
} else {
return new MultiSegmentImpl(
newResourceDescription,
@@ -599,7 +613,8 @@ abstract class MemorySegmentIndexInput extends IndexInput
slices,
offset,
length,
- chunkSizePower);
+ chunkSizePower,
+ confined);
}
}
@@ -643,8 +658,15 @@ abstract class MemorySegmentIndexInput extends IndexInput
Arena arena,
MemorySegment segment,
long length,
- int chunkSizePower) {
- super(resourceDescription, arena, new MemorySegment[] {segment}, length, chunkSizePower);
+ int chunkSizePower,
+ boolean confined) {
+ super(
+ resourceDescription,
+ arena,
+ new MemorySegment[] {segment},
+ length,
+ chunkSizePower,
+ confined);
this.curSegmentIndex = 0;
}
@@ -740,8 +762,9 @@ abstract class MemorySegmentIndexInput extends IndexInput
MemorySegment[] segments,
long offset,
long length,
- int chunkSizePower) {
- super(resourceDescription, arena, segments, length, chunkSizePower);
+ int chunkSizePower,
+ boolean confined) {
+ super(resourceDescription, arena, segments, length, chunkSizePower, confined);
this.offset = offset;
try {
seek(0L);
diff --git a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInputProvider.java b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInputProvider.java
index e1655101d75..08f6149746b 100644
--- a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInputProvider.java
+++ b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInputProvider.java
@@ -45,7 +45,8 @@ final class MemorySegmentIndexInputProvider implements MMapDirectory.MMapIndexIn
path = Unwrappable.unwrapAll(path);
boolean success = false;
- final Arena arena = Arena.ofShared();
+ final boolean confined = context == IOContext.READONCE;
+ final Arena arena = confined ? Arena.ofConfined() : Arena.ofShared();
try (var fc = FileChannel.open(path, StandardOpenOption.READ)) {
final long fileSize = fc.size();
final IndexInput in =
@@ -61,7 +62,8 @@ final class MemorySegmentIndexInputProvider implements MMapDirectory.MMapIndexIn
preload,
fileSize),
fileSize,
- chunkSizePower);
+ chunkSizePower,
+ confined);
success = true;
return in;
} finally {
diff --git a/lucene/core/src/test/org/apache/lucene/store/TestMMapDirectory.java b/lucene/core/src/test/org/apache/lucene/store/TestMMapDirectory.java
index 39d3dbda9ac..f7c49c9b661 100644
--- a/lucene/core/src/test/org/apache/lucene/store/TestMMapDirectory.java
+++ b/lucene/core/src/test/org/apache/lucene/store/TestMMapDirectory.java
@@ -19,9 +19,14 @@ package org.apache.lucene.store;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Random;
+import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import org.apache.lucene.tests.store.BaseDirectoryTestCase;
import org.apache.lucene.util.Constants;
+import org.apache.lucene.util.NamedThreadFactory;
/** Tests MMapDirectory */
// See: https://issues.apache.org/jira/browse/SOLR-12028 Tests cannot remove files on Windows
@@ -117,4 +122,54 @@ public class TestMMapDirectory extends BaseDirectoryTestCase {
}
}
}
+
+ // Opens the input with ReadAdvice.READONCE to ensure slice and clone are appropriately confined
+ public void testConfined() throws Exception {
+ final int size = 16;
+ byte[] bytes = new byte[size];
+ random().nextBytes(bytes);
+
+ try (Directory dir = new MMapDirectory(createTempDir("testConfined"))) {
+ try (IndexOutput out = dir.createOutput("test", IOContext.DEFAULT)) {
+ out.writeBytes(bytes, 0, bytes.length);
+ }
+
+ try (var in = dir.openInput("test", IOContext.READONCE);
+ var executor = Executors.newFixedThreadPool(1, new NamedThreadFactory("testConfined"))) {
+ // ensure accessible
+ assertEquals(16L, in.slice("test", 0, in.length()).length());
+ assertEquals(15L, in.slice("test", 1, in.length() - 1).length());
+
+ // ensure not accessible
+ Callable