This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-mvnd.git
The following commit(s) were added to refs/heads/master by this push:
new e51416f Allow to the registry to be resized to avoid registry
corruption (#645) (#646)
e51416f is described below
commit e51416f7f2f6d95add36c06090ff9b5aaa2a705a
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jun 15 08:06:33 2022 +0200
Allow to the registry to be resized to avoid registry corruption (#645)
(#646)
---
.../org/mvndaemon/mvnd/common/BufferHelper.java | 265 +++++++++++++++++++++
.../org/mvndaemon/mvnd/common/DaemonRegistry.java | 52 +++-
.../mvndaemon/mvnd/common/DaemonRegistryTest.java | 43 ++++
3 files changed, 356 insertions(+), 4 deletions(-)
diff --git a/common/src/main/java/org/mvndaemon/mvnd/common/BufferHelper.java
b/common/src/main/java/org/mvndaemon/mvnd/common/BufferHelper.java
new file mode 100644
index 0000000..31f4635
--- /dev/null
+++ b/common/src/main/java/org/mvndaemon/mvnd/common/BufferHelper.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mvndaemon.mvnd.common;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.function.Consumer;
+
+/**
+ * Original code from
+ *
https://github.com/classgraph/classgraph/blob/latest/src/main/java/nonapi/io/github/classgraph/utils/FileUtils.java#L543
+ */
+public class BufferHelper {
+
+ private static boolean PRE_JAVA_9 =
System.getProperty("java.specification.version", "9").startsWith("1.");
+
+ /** The DirectByteBuffer.cleaner() method. */
+ private static Method directByteBufferCleanerMethod;
+
+ /** The Cleaner.clean() method. */
+ private static Method cleanerCleanMethod;
+
+ // /** The jdk.incubator.foreign.MemorySegment class (JDK14+). */
+ // private static Class<?> memorySegmentClass;
+ //
+ // /** The jdk.incubator.foreign.MemorySegment.ofByteBuffer method
(JDK14+). */
+ // private static Method memorySegmentOfByteBufferMethod;
+ //
+ // /** The jdk.incubator.foreign.MemorySegment.ofByteBuffer method
(JDK14+). */
+ // private static Method memorySegmentCloseMethod;
+
+ /** The attachment() method. */
+ private static Method attachmentMethod;
+
+ /** The Unsafe object. */
+ private static Object theUnsafe;
+
+ /**
+ * Get the clean() method, attachment() method, and theUnsafe field,
called inside doPrivileged.
+ */
+ static void lookupCleanMethodPrivileged() {
+ if (PRE_JAVA_9) {
+ try {
+ // See:
+ // https://stackoverflow.com/a/19447758/3950982
+ cleanerCleanMethod =
Class.forName("sun.misc.Cleaner").getDeclaredMethod("clean");
+ cleanerCleanMethod.setAccessible(true);
+ final Class<?> directByteBufferClass =
Class.forName("sun.nio.ch.DirectBuffer");
+ directByteBufferCleanerMethod =
directByteBufferClass.getDeclaredMethod("cleaner");
+ attachmentMethod =
directByteBufferClass.getMethod("attachment");
+ attachmentMethod.setAccessible(true);
+ } catch (final SecurityException e) {
+ throw new RuntimeException(
+ "You need to grant classgraph
RuntimePermission(\"accessClassInPackage.sun.misc\") "
+ + "and
ReflectPermission(\"suppressAccessChecks\")",
+ e);
+ } catch (final ReflectiveOperationException | LinkageError e) {
+ // Ignore
+ }
+ } else {
+ //boolean jdkSuccess = false;
+ // // TODO: This feature is in incubation now -- enable after
it leaves incubation.
+ // // To enable this feature, need to:
+ // // -- add whatever the "jdk.incubator.foreign" module name
is replaced with to <Import-Package>
+ // // in pom.xml, as an optional dependency
+ // // -- add the same module name to module-info.java as a
"requires static" optional dependency
+ // // -- build two versions of module.java: the existing one,
for --release=9, and a new version,
+ // // for --release=15 (or whatever the final release
version ends up being when the feature is
+ // // moved out of incubation).
+ // try {
+ // // JDK 14+ Invoke
MemorySegment.ofByteBuffer(myByteBuffer).close()
+ // // https://stackoverflow.com/a/26777380/3950982
+ // memorySegmentClass =
Class.forName("jdk.incubator.foreign.MemorySegment");
+ // memorySegmentCloseMethod =
AutoCloseable.class.getDeclaredMethod("close");
+ // memorySegmentOfByteBufferMethod =
memorySegmentClass.getMethod("ofByteBuffer",
+ // ByteBuffer.class);
+ // jdk14Success = true;
+ // } catch (ClassNotFoundException | NoSuchMethodException |
SecurityException e1) {
+ // // Fall through
+ // }
+ //if (!jdk14Success) { // In JDK9+, calling
sun.misc.Cleaner.clean() gives a reflection warning on stderr,
+ // so we need to call Unsafe.theUnsafe.invokeCleaner(byteBuffer)
instead, which makes
+ // the same call, but does not print the reflection warning.
+ try {
+ Class<?> unsafeClass;
+ try {
+ unsafeClass = Class.forName("sun.misc.Unsafe");
+ } catch (final ReflectiveOperationException | LinkageError e) {
+ throw new RuntimeException("Could not get class
sun.misc.Unsafe", e);
+ }
+ final Field theUnsafeField =
unsafeClass.getDeclaredField("theUnsafe");
+ theUnsafeField.setAccessible(true);
+ theUnsafe = theUnsafeField.get(null);
+ cleanerCleanMethod = unsafeClass.getMethod("invokeCleaner",
ByteBuffer.class);
+ cleanerCleanMethod.setAccessible(true);
+ } catch (final SecurityException e) {
+ throw new RuntimeException(
+ "You need to grant classgraph
RuntimePermission(\"accessClassInPackage.sun.misc\") "
+ + "and
ReflectPermission(\"suppressAccessChecks\")",
+ e);
+ } catch (final ReflectiveOperationException | LinkageError ex) {
+ // Ignore
+ }
+ //}
+ }
+ }
+
+ static {
+ AccessController.doPrivileged(new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ lookupCleanMethodPrivileged();
+ return null;
+ }
+ });
+ }
+
+ private static boolean closeDirectByteBufferPrivileged(final ByteBuffer
byteBuffer, final Consumer<String> log) {
+ if (!byteBuffer.isDirect()) {
+ // Nothing to do
+ return true;
+ }
+ try {
+ if (PRE_JAVA_9) {
+ if (attachmentMethod == null) {
+ if (log != null) {
+ log.accept("Could not unmap ByteBuffer,
attachmentMethod == null");
+ }
+ return false;
+ }
+ // Make sure duplicates and slices are not cleaned, since this
can result in duplicate
+ // attempts to clean the same buffer, which trigger a crash
with:
+ // "A fatal error has been detected by the Java Runtime
Environment: EXCEPTION_ACCESS_VIOLATION"
+ // See: https://stackoverflow.com/a/31592947/3950982
+ if (attachmentMethod.invoke(byteBuffer) != null) {
+ // Buffer is a duplicate or slice
+ return false;
+ }
+ // Invoke ((DirectBuffer) byteBuffer).cleaner().clean()
+ if (directByteBufferCleanerMethod == null) {
+ if (log != null) {
+ log.accept("Could not unmap ByteBuffer, cleanerMethod
== null");
+ }
+ return false;
+ }
+ try {
+ directByteBufferCleanerMethod.setAccessible(true);
+ } catch (final Exception e) {
+ if (log != null) {
+ log.accept("Could not unmap ByteBuffer,
cleanerMethod.setAccessible(true) failed");
+ }
+ return false;
+ }
+ final Object cleanerInstance =
directByteBufferCleanerMethod.invoke(byteBuffer);
+ if (cleanerInstance == null) {
+ if (log != null) {
+ log.accept("Could not unmap ByteBuffer, cleaner ==
null");
+ }
+ return false;
+ }
+ if (cleanerCleanMethod == null) {
+ if (log != null) {
+ log.accept("Could not unmap ByteBuffer, cleanMethod ==
null");
+ }
+ return false;
+ }
+ try {
+ cleanerCleanMethod.invoke(cleanerInstance);
+ return true;
+ } catch (final Exception e) {
+ if (log != null) {
+ log.accept("Could not unmap ByteBuffer,
cleanMethod.invoke(cleaner) failed: " + e);
+ }
+ return false;
+ }
+ // } else if (memorySegmentOfByteBufferMethod != null) {
+ // // JDK 14+
+ // final Object memorySegment =
memorySegmentOfByteBufferMethod.invoke(null, byteBuffer);
+ // if (memorySegment == null) {
+ // if (log != null) {
+ // log.log("Got null MemorySegment, could not
unmap ByteBuffer");
+ // }
+ // return false;
+ // }
+ // memorySegmentCloseMethod.invoke(memorySegment);
+ // return true;
+ } else {
+ if (theUnsafe == null) {
+ if (log != null) {
+ log.accept("Could not unmap ByteBuffer, theUnsafe ==
null");
+ }
+ return false;
+ }
+ if (cleanerCleanMethod == null) {
+ if (log != null) {
+ log.accept("Could not unmap ByteBuffer, cleanMethod ==
null");
+ }
+ return false;
+ }
+ try {
+ cleanerCleanMethod.invoke(theUnsafe, byteBuffer);
+ return true;
+ } catch (final IllegalArgumentException e) {
+ // Buffer is a duplicate or slice
+ return false;
+ }
+ }
+ } catch (final ReflectiveOperationException | SecurityException e) {
+ if (log != null) {
+ log.accept("Could not unmap ByteBuffer: " + e);
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Close a {@code DirectByteBuffer} -- in particular, will unmap a
+ * {@link java.nio.MappedByteBuffer}.
+ *
+ * @param byteBuffer The {@link ByteBuffer} to close/unmap.
+ * @return True if the byteBuffer was closed/unmapped (or if
the ByteBuffer was null or non-direct).
+ */
+ public static boolean closeDirectByteBuffer(final ByteBuffer byteBuffer) {
+ return closeDirectByteBuffer(byteBuffer, null);
+ }
+
+ /**
+ * Close a {@code DirectByteBuffer} -- in particular, will unmap a
+ * {@link java.nio.MappedByteBuffer}.
+ *
+ * @param byteBuffer The {@link ByteBuffer} to close/unmap.
+ * @param log The log.
+ * @return True if the byteBuffer was closed/unmapped (or if
the ByteBuffer was null or non-direct).
+ */
+ public static boolean closeDirectByteBuffer(final ByteBuffer byteBuffer,
final Consumer<String> log) {
+ if (byteBuffer != null && byteBuffer.isDirect()) {
+ return AccessController.doPrivileged(new
PrivilegedAction<Boolean>() {
+ @Override
+ public Boolean run() {
+ return closeDirectByteBufferPrivileged(byteBuffer, log);
+ }
+ });
+ } else {
+ // Nothing to unmap
+ return false;
+ }
+ }
+
+}
diff --git a/common/src/main/java/org/mvndaemon/mvnd/common/DaemonRegistry.java
b/common/src/main/java/org/mvndaemon/mvnd/common/DaemonRegistry.java
index 1e7abc8..b0423e0 100644
--- a/common/src/main/java/org/mvndaemon/mvnd/common/DaemonRegistry.java
+++ b/common/src/main/java/org/mvndaemon/mvnd/common/DaemonRegistry.java
@@ -18,6 +18,7 @@ package org.mvndaemon.mvnd.common;
import java.io.IOException;
import java.lang.management.ManagementFactory;
+import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
@@ -59,7 +60,8 @@ public class DaemonRegistry implements AutoCloseable {
private static final Map<Path, Object> locks = new ConcurrentHashMap<>();
private final Object lck;
private final FileChannel channel;
- private final MappedByteBuffer buffer;
+ private MappedByteBuffer buffer;
+ private long size;
private final Map<String, DaemonInfo> infosMap = new HashMap<>();
private final List<DaemonStopEvent> stopEvents = new ArrayList<>();
@@ -76,12 +78,21 @@ public class DaemonRegistry implements AutoCloseable {
}
channel = FileChannel.open(absPath,
StandardOpenOption.CREATE, StandardOpenOption.READ,
StandardOpenOption.WRITE);
- buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0,
MAX_LENGTH);
+ size = nextPowerOf2(channel.size(), MAX_LENGTH);
+ buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, size);
} catch (IOException e) {
throw new DaemonException(e);
}
}
+ private long nextPowerOf2(long a, long min) {
+ long b = min;
+ while (b < a) {
+ b = b << 1;
+ }
+ return b;
+ }
+
public void close() {
try {
channel.close();
@@ -177,7 +188,7 @@ public class DaemonRegistry implements AutoCloseable {
synchronized (lck) {
final long deadline = System.currentTimeMillis() + LOCK_TIMEOUT_MS;
while (System.currentTimeMillis() < deadline) {
- try (FileLock l = channel.tryLock(0, MAX_LENGTH, false)) {
+ try (FileLock l = tryLock()) {
BufferCaster.cast(buffer).position(0);
infosMap.clear();
int nb = buffer.getInt();
@@ -244,9 +255,34 @@ public class DaemonRegistry implements AutoCloseable {
writeString(dse.getReason());
}
}
+ if (buffer.remaining() >= buffer.position() * 2) {
+ long ns = nextPowerOf2(buffer.position(), MAX_LENGTH);
+ if (ns != size) {
+ size = ns;
+ LOGGER.info("Resizing registry to {} kb due to
buffer underflow", (size / 1024));
+ l.release();
+ BufferHelper.closeDirectByteBuffer(buffer,
LOGGER::debug);
+ channel.truncate(size);
+ try {
+ buffer =
channel.map(FileChannel.MapMode.READ_WRITE, 0, size);
+ } catch (IOException ex) {
+ throw new DaemonException("Could not resize
registry " + registryFile, ex);
+ }
+ }
+ }
return;
+ } catch (BufferOverflowException e) {
+ size <<= 1;
+ LOGGER.info("Resizing registry to {} kb due to buffer
overflow", (size / 1024));
+ try {
+ buffer = channel.map(FileChannel.MapMode.READ_WRITE,
0, size);
+ } catch (IOException ex) {
+ ex.addSuppressed(e);
+ throw new DaemonException("Could not resize registry "
+ registryFile, ex);
+ }
} catch (IOException e) {
- throw new RuntimeException("Could not lock offset 0 of " +
registryFile);
+ throw new DaemonException("Exception while "
+ + (updater != null ? "updating " : "reading ") +
registryFile, e);
} catch (IllegalStateException |
ArrayIndexOutOfBoundsException | BufferUnderflowException e) {
String absPath =
registryFile.toAbsolutePath().normalize().toString();
LOGGER.warn("Invalid daemon registry info, " +
@@ -261,6 +297,14 @@ public class DaemonRegistry implements AutoCloseable {
}
}
+ private FileLock tryLock() {
+ try {
+ return channel.tryLock(0, size, false);
+ } catch (IOException e) {
+ throw new DaemonException("Could not lock " + registryFile, e);
+ }
+ }
+
private void reset() {
infosMap.clear();
stopEvents.clear();
diff --git
a/common/src/test/java/org/mvndaemon/mvnd/common/DaemonRegistryTest.java
b/common/src/test/java/org/mvndaemon/mvnd/common/DaemonRegistryTest.java
index ec44f98..0af8162 100644
--- a/common/src/test/java/org/mvndaemon/mvnd/common/DaemonRegistryTest.java
+++ b/common/src/test/java/org/mvndaemon/mvnd/common/DaemonRegistryTest.java
@@ -19,14 +19,19 @@ import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import java.util.Locale;
import java.util.Random;
+import java.util.UUID;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class DaemonRegistryTest {
@@ -54,6 +59,44 @@ public class DaemonRegistryTest {
}
}
+ @Test
+ public void testBigRegistry() throws IOException {
+ int nbDaemons = 512;
+
+ Path temp = File.createTempFile("reg", ".data").toPath();
+ Random random = new Random();
+ try (DaemonRegistry reg = new DaemonRegistry(temp)) {
+ for (int i = 0; i < nbDaemons; i++) {
+ byte[] token = new byte[16];
+ random.nextBytes(token);
+ reg.store(new DaemonInfo(UUID.randomUUID().toString(),
"/java/home/",
+ "/data/reg/", random.nextInt(),
"inet:/127.0.0.1:7502", token,
+ Locale.getDefault().toLanguageTag(),
Collections.singletonList("-Xmx"),
+ DaemonState.Idle, System.currentTimeMillis(),
System.currentTimeMillis()));
+ }
+ }
+
+ long size = Files.size(temp);
+ assertTrue(size >= 128 * 1024);
+ try (DaemonRegistry reg = new DaemonRegistry(temp)) {
+ assertEquals(nbDaemons, reg.getAll().size());
+ }
+
+ try (DaemonRegistry reg = new DaemonRegistry(temp)) {
+ for (int i = 0; i < nbDaemons / 2; i++) {
+ List<DaemonInfo> list = reg.getAll();
+ reg.remove(list.get(random.nextInt(list.size())).getId());
+ }
+ }
+
+ long size2 = Files.size(temp);
+ assertTrue(size2 < 128 * 1024);
+ try (DaemonRegistry reg = new DaemonRegistry(temp)) {
+ assertEquals(nbDaemons / 2, reg.getAll().size());
+ }
+
+ }
+
@Test
public void testRecovery() throws IOException {
Path temp = File.createTempFile("reg", ".data").toPath();