Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial rewrite of MMapDirectory for JDK-16 preview (incubating) Panama APIs (>= JDK-16-ea-b32) #2176

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
190a853
Initial state of new jdk-foreign MMAP API
uschindler Jan 1, 2021
00d01a7
Workaround to prevent incorrect test files from being executed (copie…
uschindler Jan 2, 2021
22c3c4b
Fix the remaining TODOs: make sure we unmap all segments if exception…
uschindler Jan 3, 2021
f9ca335
Cleanup code duplication mess exception handling and rename all remai…
uschindler Jan 3, 2021
1a8a354
add missing ensureOpen() as NPE can't happen here
uschindler Jan 3, 2021
27fce4f
Cleanup messy duplicate methods
uschindler Jan 3, 2021
efcfccc
Add workaround for JDK-8259028
uschindler Jan 3, 2021
8ee976a
Make the JVM crush detector ready for heavy prime time!
uschindler Jan 3, 2021
fed48bd
Remove incorrect assert (won't work if page size is used like on linux)
uschindler Jan 3, 2021
50d9300
Apply @dweiss improvement
uschindler Jan 4, 2021
8dd5d90
Merge branch 'master' into draft/jdk-foreign-mmap
uschindler Jan 6, 2021
0245d3f
Add readLEFloats() introduced by LUCENE-9652 / #2175
uschindler Jan 6, 2021
ea188c1
Improve test to allow the following exception: "java.lang.IllegalStat…
uschindler Jan 6, 2021
01aca07
Add a new interface to Lucene's core to mark classes which are wrappi…
uschindler Jan 7, 2021
60200e8
Split and rewrite getBytes() and remove useless try-with-resources (h…
uschindler Jan 7, 2021
ba61072
Add static final boolean IS_LITTLE_ENDIAN and cleanup if statements
uschindler Jan 7, 2021
ec304ab
Merge branch 'master' into draft/jdk-foreign-mmap
uschindler Jan 15, 2021
5edcdf4
Remove hacks: JDK-16 EA b32 has now fixed the horrible bugs with zero…
uschindler Jan 15, 2021
b0eec7a
Merge branch 'master' into draft/jdk-foreign-mmap
uschindler Jan 15, 2021
7a3cf53
Improve close method to also null out the segments, so positional API…
uschindler Jan 16, 2021
d2c0be5
Merge branch 'master' into draft/jdk-foreign-mmap
uschindler Mar 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions gradle/defaults-java.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,14 @@ allprojects {
}
}
}

configure(project(":lucene:core")) {
uschindler marked this conversation as resolved.
Show resolved Hide resolved
plugins.withType(JavaPlugin) {
tasks.named('compileJava').configure {
sourceCompatibility = 16
targetCompatibility = 16
options.compilerArgs += ["--release", 16 as String, "--add-modules", "jdk.incubator.foreign"]
options.compilerArgs -= "-Werror"
}
}
}
4 changes: 4 additions & 0 deletions gradle/testing/defaults-tests.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,17 @@ allprojects {
workingDir testsCwd
useJUnit()

include '**/*Test.class', '**/Test*.class'
exclude '**/*$*'

minHeapSize = resolvedTestOption("tests.minheapsize")
maxHeapSize = resolvedTestOption("tests.heapsize")

ignoreFailures = resolvedTestOption("tests.haltonfailure").toBoolean() == false

jvmArgs Commandline.translateCommandline(resolvedTestOption("tests.jvmargs"))
jvmArgs '--illegal-access=deny'
jvmArgs "--add-modules", "jdk.incubator.foreign"

systemProperty 'java.util.logging.config.file', file("${resources}/logging.properties")
systemProperty 'java.awt.headless', 'true'
Expand Down
242 changes: 104 additions & 138 deletions lucene/core/src/java/org/apache/lucene/store/MMapDirectory.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,21 @@
*/
package org.apache.lucene.store;

import static java.lang.invoke.MethodHandles.*;
import static java.lang.invoke.MethodType.methodType;

import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.ClosedChannelException; // javadoc @link
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Future;
import org.apache.lucene.store.ByteBufferGuard.BufferCleaner;

import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.Constants;
import org.apache.lucene.util.SuppressForbidden;
import org.apache.lucene.util.IOUtils;

import jdk.incubator.foreign.MappedMemorySegments;
import jdk.incubator.foreign.MemorySegment;

/**
* File-based {@link Directory} implementation that uses mmap for reading, and {@link
Expand Down Expand Up @@ -85,9 +79,9 @@ public class MMapDirectory extends FSDirectory {
/**
* Default max chunk size.
*
* @see #MMapDirectory(Path, LockFactory, int)
* @see #MMapDirectory(Path, LockFactory, long)
*/
public static final int DEFAULT_MAX_CHUNK_SIZE = Constants.JRE_IS_64BIT ? (1 << 30) : (1 << 28);
public static final long DEFAULT_MAX_CHUNK_SIZE = Constants.JRE_IS_64BIT ? (1L << 34) : (1L << 28);

final int chunkSizePower;

Expand Down Expand Up @@ -119,11 +113,11 @@ public MMapDirectory(Path path) throws IOException {
* directory is created at the named location if it does not yet exist.
*
* @param path the path of the directory
* @param maxChunkSize maximum chunk size (default is 1 GiBytes for 64 bit JVMs and 256 MiBytes
* @param maxChunkSize maximum chunk size (default is 16 GiBytes for 64 bit JVMs and 256 MiBytes
* for 32 bit JVMs) used for memory mapping.
* @throws IOException if there is a low-level I/O error
*/
public MMapDirectory(Path path, int maxChunkSize) throws IOException {
public MMapDirectory(Path path, long maxChunkSize) throws IOException {
this(path, FSLockFactory.getDefault(), maxChunkSize);
}

Expand All @@ -134,25 +128,28 @@ public MMapDirectory(Path path, int maxChunkSize) throws IOException {
* <p>Especially on 32 bit platform, the address space can be very fragmented, so large index
* files cannot be mapped. Using a lower chunk size makes the directory implementation a little
* bit slower (as the correct chunk may be resolved on lots of seeks) but the chance is higher
* that mmap does not fail. On 64 bit Java platforms, this parameter should always be {@code 1 <<
* 30}, as the address space is big enough.
* that mmap does not fail. On 64 bit Java platforms, this parameter should always be large
* (like 16 GiBytes), as the address space is big enough. If it is larger, fragmentation of
* address space increases, but number of file handles and mappings is lower for huge
* installations with many open indexes.
*
* <p><b>Please note:</b> The chunk size is always rounded down to a power of 2.
*
* @param path the path of the directory
* @param lockFactory the lock factory to use, or null for the default ({@link
* NativeFSLockFactory});
* @param maxChunkSize maximum chunk size (default is 1 GiBytes for 64 bit JVMs and 256 MiBytes
* @param maxChunkSize maximum chunk size (default is 16 GiBytes for 64 bit JVMs and 256 MiBytes
* for 32 bit JVMs) used for memory mapping.
* @throws IOException if there is a low-level I/O error
*/
public MMapDirectory(Path path, LockFactory lockFactory, int maxChunkSize) throws IOException {
public MMapDirectory(Path path, LockFactory lockFactory, long maxChunkSize) throws IOException {
super(path, lockFactory);
if (maxChunkSize <= 0) {
if (maxChunkSize <= 0L) {
throw new IllegalArgumentException("Maximum chunk size for mmap must be >0");
}
this.chunkSizePower = 31 - Integer.numberOfLeadingZeros(maxChunkSize);
assert this.chunkSizePower >= 0 && this.chunkSizePower <= 30;
this.chunkSizePower = Long.SIZE - 1 - Long.numberOfLeadingZeros(maxChunkSize);
assert (1L << chunkSizePower) <= maxChunkSize;
assert (1L << chunkSizePower) > (maxChunkSize / 2);
}

/**
Expand Down Expand Up @@ -200,7 +197,7 @@ public boolean getUseUnmap() {
* Set to {@code true} to ask mapped pages to be loaded into physical memory on init. The behavior
* is best-effort and operating system dependent.
*
* @see MappedByteBuffer#load
* @see MappedMemorySegments#load
*/
public void setPreload(boolean preload) {
this.preload = preload;
Expand All @@ -218,10 +215,10 @@ public boolean getPreload() {
/**
* Returns the current mmap chunk size.
*
* @see #MMapDirectory(Path, LockFactory, int)
* @see #MMapDirectory(Path, LockFactory, long)
*/
public final int getMaxChunkSize() {
return 1 << chunkSizePower;
public final long getMaxChunkSize() {
return 1L << chunkSizePower;
}

/** Creates an IndexInput for the file with the given name. */
Expand All @@ -230,54 +227,96 @@ public IndexInput openInput(String name, IOContext context) throws IOException {
ensureOpen();
ensureCanRead(name);
Path path = directory.resolve(name);
try (FileChannel c = FileChannel.open(path, StandardOpenOption.READ)) {
final String resourceDescription = "MMapIndexInput(path=\"" + path.toString() + "\")";
final boolean useUnmap = getUseUnmap();
return ByteBufferIndexInput.newInstance(
resourceDescription,
map(resourceDescription, c, 0, c.size()),
c.size(),
chunkSizePower,
new ByteBufferGuard(resourceDescription, useUnmap ? CLEANER : null));
}
final String resourceDescription = "MemorySegmentIndexInput(path=\"" + path.toString() + "\")";
final long fileSize = Files.size(path);
MemorySegment[] segments = map(resourceDescription, path, fileSize);
return MemorySegmentIndexInput.newInstance(
resourceDescription,
segments,
fileSize,
chunkSizePower);
}

/** Maps a file into a set of buffers */
final ByteBuffer[] map(String resourceDescription, FileChannel fc, long offset, long length)
/** Maps a file into a set of segments */
final MemorySegment[] map(String resourceDescription, Path path, long length)
throws IOException {
if ((length >>> chunkSizePower) >= Integer.MAX_VALUE)
throw new IllegalArgumentException(
"RandomAccessFile too big for chunk size: " + resourceDescription);
"File too big for chunk size: " + resourceDescription);

final long chunkSize = 1L << chunkSizePower;

// we always allocate one more buffer, the last one may be a 0 byte one
final int nrBuffers = (int) (length >>> chunkSizePower) + 1;
// we always allocate one more segments, the last one may be a 0 byte one
final int nrSegments = (int) (length >>> chunkSizePower) + 1;

final MemorySegment segments[] = new MemorySegment[nrSegments];

ByteBuffer buffers[] = new ByteBuffer[nrBuffers];

long bufferStart = 0L;
for (int bufNr = 0; bufNr < nrBuffers; bufNr++) {
int bufSize =
(int) ((length > (bufferStart + chunkSize)) ? chunkSize : (length - bufferStart));
MappedByteBuffer buffer;
try {
buffer = fc.map(MapMode.READ_ONLY, offset + bufferStart, bufSize);
} catch (IOException ioe) {
throw convertMapFailedIOException(ioe, resourceDescription, bufSize);
boolean success = false;
try {
path = unwrapPath(path);
long startOffset = 0L;
for (int segNr = 0; segNr < nrSegments; segNr++) {
long segSize =
(length > (startOffset + chunkSize)) ? chunkSize : (length - startOffset);
final MemorySegment segment;
try {
segment = mapFileBugfix(path, startOffset, segSize, MapMode.READ_ONLY);
} catch (IOException ioe) {
throw convertMapFailedIOException(ioe, resourceDescription, segSize);
}
if (preload && segSize > 0L) {
MappedMemorySegments.load(segment);
}
segments[segNr] = segment.share();
startOffset += segSize;
}
if (preload) {
buffer.load();
success = true;
return segments;
} finally {
if (success == false) {
IOUtils.applyToAll(Arrays.asList(segments), MemorySegment::close);
}
buffers[bufNr] = buffer;
bufferStart += bufSize;
}

return buffers;
}

/** Work around for JDK-8259028: we need to unwrap our test-only file system layers
* @see "https://bugs.openjdk.java.net/browse/JDK-8259028"
*/
private static Path unwrapPath(Path path) {
for (;;) {
final Class<? extends Path> cls = path.getClass();
if (false == cls.getName().startsWith("org.apache.lucene.")) {
return path;
}
try {
path = (Path) cls.getMethod("getDelegate").invoke(path);
} catch (ReflectiveOperationException | ClassCastException e) {
return path;
}
}
}

private static MemorySegment mapFileBugfix(Path path, long bytesOffset, long bytesSize, MapMode mapMode) throws IOException {
// work around NPE bug:
// https://bugs.openjdk.java.net/browse/JDK-8259027
if (bytesSize == 0L) {
return MemorySegment.ofArray(BytesRef.EMPTY_BYTES);
}
// work around alignment bug by applying alignment on our own:
// https://bugs.openjdk.java.net/browse/JDK-8259032
// allocationGranularity: known maximum for Win64, on Linux it's page size
// We are just safe here and use a granularity that's huge enough for all platforms!
final long allocationGranularity = 65536L;
final long pageOffset = bytesOffset % allocationGranularity;
final long mapBytesOffset = bytesOffset - pageOffset;
final long mapBytesSize = bytesSize + pageOffset;
final MemorySegment seg = MemorySegment.mapFile(path, mapBytesOffset, mapBytesSize, mapMode);
assert (seg.address().toRawLongValue() % allocationGranularity == 0);
return (pageOffset == 0L) ? seg : seg.asSlice(pageOffset);
}

private IOException convertMapFailedIOException(
IOException ioe, String resourceDescription, int bufSize) {
private static IOException convertMapFailedIOException(
IOException ioe, String resourceDescription, long bufSize) {
final String originalMessage;
final Throwable originalCause;
if (ioe.getCause() instanceof OutOfMemoryError) {
Expand Down Expand Up @@ -319,85 +358,12 @@ private IOException convertMapFailedIOException(
}

/** <code>true</code>, if this platform supports unmapping mmapped files. */
public static final boolean UNMAP_SUPPORTED;
public static final boolean UNMAP_SUPPORTED = true; // nocommit: cleanup

/**
* if {@link #UNMAP_SUPPORTED} is {@code false}, this contains the reason why unmapping is not
* supported.
*/
public static final String UNMAP_NOT_SUPPORTED_REASON;

/** Reference to a BufferCleaner that does unmapping; {@code null} if not supported. */
private static final BufferCleaner CLEANER;

static {
final Object hack =
AccessController.doPrivileged((PrivilegedAction<Object>) MMapDirectory::unmapHackImpl);
if (hack instanceof BufferCleaner) {
CLEANER = (BufferCleaner) hack;
UNMAP_SUPPORTED = true;
UNMAP_NOT_SUPPORTED_REASON = null;
} else {
CLEANER = null;
UNMAP_SUPPORTED = false;
UNMAP_NOT_SUPPORTED_REASON = hack.toString();
}
}

@SuppressForbidden(
reason =
"Needs access to private APIs in DirectBuffer, sun.misc.Cleaner, and sun.misc.Unsafe to enable hack")
private static Object unmapHackImpl() {
final Lookup lookup = lookup();
try {
// *** sun.misc.Unsafe unmapping (Java 9+) ***
final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
// first check if Unsafe has the right method, otherwise we can give up
// without doing any security critical stuff:
final MethodHandle unmapper =
lookup.findVirtual(
unsafeClass, "invokeCleaner", methodType(void.class, ByteBuffer.class));
// fetch the unsafe instance and bind it to the virtual MH:
final Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
final Object theUnsafe = f.get(null);
return newBufferCleaner(ByteBuffer.class, unmapper.bindTo(theUnsafe));
} catch (SecurityException se) {
return "Unmapping is not supported, because not all required permissions are given to the Lucene JAR file: "
+ se
+ " [Please grant at least the following permissions: RuntimePermission(\"accessClassInPackage.sun.misc\") "
+ " and ReflectPermission(\"suppressAccessChecks\")]";
} catch (ReflectiveOperationException | RuntimeException e) {
return "Unmapping is not supported on this platform, because internal Java APIs are not compatible with this Lucene version: "
+ e;
}
}
public static final String UNMAP_NOT_SUPPORTED_REASON = null; // nocommit: cleanup

private static BufferCleaner newBufferCleaner(
final Class<?> unmappableBufferClass, final MethodHandle unmapper) {
assert Objects.equals(methodType(void.class, ByteBuffer.class), unmapper.type());
return (String resourceDescription, ByteBuffer buffer) -> {
if (!buffer.isDirect()) {
throw new IllegalArgumentException("unmapping only works with direct buffers");
}
if (!unmappableBufferClass.isInstance(buffer)) {
throw new IllegalArgumentException(
"buffer is not an instance of " + unmappableBufferClass.getName());
}
final Throwable error =
AccessController.doPrivileged(
(PrivilegedAction<Throwable>)
() -> {
try {
unmapper.invokeExact(buffer);
return null;
} catch (Throwable t) {
return t;
}
});
if (error != null) {
throw new IOException("Unable to unmap the mapped buffer: " + resourceDescription, error);
}
};
}
}