LUCENE-6989: Add preliminary support for MMapDirectory unmapping in Java 9

This commit is contained in:
Uwe Schindler 2016-02-15 19:57:39 +01:00
parent a9dc40294e
commit 7b6df2542d
6 changed files with 103 additions and 43 deletions

View File

@ -61,6 +61,9 @@ New Features
* LUCENE-6975: Add ExactPointQuery, to match a single N-dimensional * LUCENE-6975: Add ExactPointQuery, to match a single N-dimensional
point (Robert Muir, Mike McCandless) point (Robert Muir, Mike McCandless)
* LUCENE-6989: Add preliminary support for MMapDirectory unmapping in Java 9.
(Uwe Schindler, Chris Hegarty, Peter Levart)
API Changes API Changes
* LUCENE-6067: Accountable.getChildResources has a default * LUCENE-6067: Accountable.getChildResources has a default

View File

@ -1055,7 +1055,11 @@ public class IndexWriter implements Closeable, TwoPhaseCommit, Accountable {
"index=" + segString() + "\n" + "index=" + segString() + "\n" +
"version=" + Version.LATEST.toString() + "\n" + "version=" + Version.LATEST.toString() + "\n" +
config.toString()); config.toString());
infoStream.message("IW", "MMapDirectory.UNMAP_SUPPORTED=" + MMapDirectory.UNMAP_SUPPORTED); final StringBuilder unmapInfo = new StringBuilder(Boolean.toString(MMapDirectory.UNMAP_SUPPORTED));
if (!MMapDirectory.UNMAP_SUPPORTED) {
unmapInfo.append(" (").append(MMapDirectory.UNMAP_NOT_SUPPORTED_REASON).append(")");
}
infoStream.message("IW", "MMapDirectory.UNMAP_SUPPORTED=" + unmapInfo);
} }
} }

View File

@ -17,6 +17,9 @@
package org.apache.lucene.store; package org.apache.lucene.store;
import static java.lang.invoke.MethodHandles.*;
import static java.lang.invoke.MethodType.methodType;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer; import java.nio.MappedByteBuffer;
@ -27,10 +30,10 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.security.AccessController; import java.security.AccessController;
import java.security.PrivilegedAction; import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.apache.lucene.store.ByteBufferIndexInput.BufferCleaner; import org.apache.lucene.store.ByteBufferIndexInput.BufferCleaner;
@ -160,24 +163,6 @@ public class MMapDirectory extends FSDirectory {
assert this.chunkSizePower >= 0 && this.chunkSizePower <= 30; assert this.chunkSizePower >= 0 && this.chunkSizePower <= 30;
} }
/**
* <code>true</code>, if this platform supports unmapping mmapped files.
*/
public static final boolean UNMAP_SUPPORTED =
AccessController.doPrivileged((PrivilegedAction<Boolean>) MMapDirectory::checkUnmapSupported);
@SuppressForbidden(reason = "Java 9 Jigsaw whitelists access to sun.misc.Cleaner, so setAccessible works")
private static boolean checkUnmapSupported() {
try {
Class<?> clazz = Class.forName("java.nio.DirectByteBuffer");
Method method = clazz.getMethod("cleaner");
method.setAccessible(true);
return true;
} catch (Exception e) {
return false;
}
}
/** /**
* This method enables the workaround for unmapping the buffers * This method enables the workaround for unmapping the buffers
* from address space after closing {@link IndexInput}, that is * from address space after closing {@link IndexInput}, that is
@ -191,8 +176,9 @@ public class MMapDirectory extends FSDirectory {
* is <code>false</code> and the workaround cannot be enabled. * is <code>false</code> and the workaround cannot be enabled.
*/ */
public void setUseUnmap(final boolean useUnmapHack) { public void setUseUnmap(final boolean useUnmapHack) {
if (useUnmapHack && !UNMAP_SUPPORTED) if (useUnmapHack && !UNMAP_SUPPORTED) {
throw new IllegalArgumentException("Unmap hack not supported on this platform!"); throw new IllegalArgumentException(UNMAP_NOT_SUPPORTED_REASON);
}
this.useUnmapHack=useUnmapHack; this.useUnmapHack=useUnmapHack;
} }
@ -310,23 +296,87 @@ public class MMapDirectory extends FSDirectory {
return newIoe; return newIoe;
} }
private static final BufferCleaner CLEANER = (final ByteBufferIndexInput parent, final ByteBuffer buffer) -> { /**
try { * <code>true</code>, if this platform supports unmapping mmapped files.
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() { */
@Override public static final boolean UNMAP_SUPPORTED;
@SuppressForbidden(reason = "Java 9 Jigsaw whitelists access to sun.misc.Cleaner, so setAccessible works")
public Void run() throws Exception { /**
final Method getCleanerMethod = buffer.getClass().getMethod("cleaner"); * if {@link #UNMAP_SUPPORTED} is {@code false}, this contains the reason why unmapping is not supported.
getCleanerMethod.setAccessible(true); */
final Object cleaner = getCleanerMethod.invoke(buffer); public static final String UNMAP_NOT_SUPPORTED_REASON;
if (cleaner != null) {
cleaner.getClass().getMethod("clean").invoke(cleaner); /** Reference to a BufferCleaner that does unmapping; {@code null} if not supported. */
} private static final BufferCleaner CLEANER;
return null;
} static {
}); final Object hack = AccessController.doPrivileged((PrivilegedAction<Object>) MMapDirectory::initUnmapHack);
} catch (PrivilegedActionException e) { if (hack instanceof BufferCleaner) {
throw new IOException("Unable to unmap the mapped buffer: " + parent.toString(), e.getCause()); 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 and sun.misc.Cleaner to enable hack")
private static Object initUnmapHack() {
final Lookup lookup = lookup();
try {
final Class<?> directBufferClass = Class.forName("java.nio.DirectByteBuffer");
final Method m = directBufferClass.getMethod("cleaner");
m.setAccessible(true);
final MethodHandle directBufferCleanerMethod = lookup.unreflect(m);
final Class<?> cleanerClass = directBufferCleanerMethod.type().returnType();
final MethodHandle cleanMethod;
if (Runnable.class.isAssignableFrom(cleanerClass)) {
// early Java 9 impl using Runnable (we do the security check early that the Runnable does at runtime):
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess("jdk.internal.ref");
}
cleanMethod = explicitCastArguments(lookup.findVirtual(Runnable.class, "run", methodType(void.class)),
methodType(void.class, cleanerClass));
} else {
// can be either the old internal "sun.misc.Cleaner" or
// the new Java 9 "java.lang.ref.Cleaner$Cleanable":
cleanMethod = lookup.findVirtual(cleanerClass, "clean", methodType(void.class));
}
final MethodHandle nonNullTest = explicitCastArguments(lookup.findStatic(Objects.class, "nonNull", methodType(boolean.class, Object.class)),
methodType(boolean.class, cleanerClass));
final MethodHandle noop = dropArguments(explicitCastArguments(constant(Void.class, null), methodType(void.class)), 0, cleanerClass);
final MethodHandle unmapper = explicitCastArguments(filterReturnValue(directBufferCleanerMethod, guardWithTest(nonNullTest, cleanMethod, noop)),
methodType(void.class, ByteBuffer.class));
return (BufferCleaner) (ByteBufferIndexInput parent, ByteBuffer buffer) -> {
if (directBufferClass.isInstance(buffer)) {
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: " + parent.toString(), error);
}
}
};
} catch (ReflectiveOperationException e) {
return "Unmapping is not supported on this platform, because internal Java APIs are not compatible to this Lucene version: " + e;
} catch (SecurityException e) {
return "Unmapping is not supported, because not all required permissions are given to the Lucene JAR file. " +
"Please grant at least the following permissions: RuntimePermission(\"accessClassInPackage.sun.misc\"), " +
"RuntimePermission(\"accessClassInPackage.jdk.internal.ref\"), and " +
"ReflectPermission(\"suppressAccessChecks\")";
}
}
} }

View File

@ -35,7 +35,8 @@ public class TestMmapDirectory extends BaseDirectoryTestCase {
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
assumeTrue("test requires a jre that supports unmapping", MMapDirectory.UNMAP_SUPPORTED); assumeTrue("test requires a jre that supports unmapping: " + MMapDirectory.UNMAP_NOT_SUPPORTED_REASON,
MMapDirectory.UNMAP_SUPPORTED);
} }
} }

View File

@ -46,7 +46,8 @@ public class TestMultiMMap extends BaseDirectoryTestCase {
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
assumeTrue("test requires a jre that supports unmapping", MMapDirectory.UNMAP_SUPPORTED); assumeTrue("test requires a jre that supports unmapping: " + MMapDirectory.UNMAP_NOT_SUPPORTED_REASON,
MMapDirectory.UNMAP_SUPPORTED);
} }
public void testCloneSafety() throws Exception { public void testCloneSafety() throws Exception {

View File

@ -64,6 +64,7 @@ grant {
permission java.lang.RuntimePermission "createClassLoader"; permission java.lang.RuntimePermission "createClassLoader";
// needed to test unmap hack on platforms that support it // needed to test unmap hack on platforms that support it
permission java.lang.RuntimePermission "accessClassInPackage.sun.misc"; permission java.lang.RuntimePermission "accessClassInPackage.sun.misc";
permission java.lang.RuntimePermission "accessClassInPackage.jdk.internal.ref";
// needed by cyberneko usage by benchmarks on J9 // needed by cyberneko usage by benchmarks on J9
permission java.lang.RuntimePermission "accessClassInPackage.org.apache.xerces.util"; permission java.lang.RuntimePermission "accessClassInPackage.org.apache.xerces.util";
// needed by jacoco to dump coverage // needed by jacoco to dump coverage