mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 08:07:42 +00:00
Added generation history
- World-local pack epochs preserve generated chunks across restart-only updates - Kernel catalogs and source seals retain executable generation behavior across jar changes - Crash-safe ownership, boundary, registry, semantic, and mantle records survive activation changes - New terrain blends lower surfaces and inverted ceilings against frozen historical boundaries - Cave, object, structure, biome, and hydrology work respects activation ownership - Find commands merge recorded facts with eligible active predictions for biomes, regions, structures, objects, and rivers - Bukkit, Fabric, Forge, and NeoForge route generation through retained runtime scopes - Shutdown ordering preserves class-loader resources until world and service cleanup finishes - Tests cover activation chains, failure recovery, retained registries, bounded lookup, and transition geometry
This commit is contained in:
1 parent
c1bcdd0a9d
commit
30113c4133
258 files changed
+38965
-1659
No files matched your search
@@ -1,9 +1,41 @@
|
||||
package art.arcane.iris.util.project.agent;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class Installer {
|
||||
private static volatile Instrumentation instrumentation;
|
||||
private static final Map<ClassLoader, Boolean> retainedClassLoaders = new IdentityHashMap<>();
|
||||
|
||||
public static synchronized void retainClassLoader(ClassLoader loader) {
|
||||
ClassLoader requiredLoader = Objects.requireNonNull(loader, "Plugin class loader");
|
||||
if (!(requiredLoader instanceof Closeable)) {
|
||||
throw new IllegalArgumentException("Plugin class loader is not closeable");
|
||||
}
|
||||
retainedClassLoaders.putIfAbsent(requiredLoader, false);
|
||||
}
|
||||
|
||||
public static synchronized boolean deferClassLoaderClose(ClassLoader loader) {
|
||||
if (!retainedClassLoaders.containsKey(loader)) {
|
||||
return false;
|
||||
}
|
||||
retainedClassLoaders.put(loader, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void releaseClassLoader(ClassLoader loader) throws IOException {
|
||||
boolean closeRequested;
|
||||
synchronized (Installer.class) {
|
||||
closeRequested = Boolean.TRUE.equals(retainedClassLoaders.remove(loader));
|
||||
}
|
||||
if (closeRequested) {
|
||||
((Closeable) loader).close();
|
||||
}
|
||||
}
|
||||
|
||||
public static Instrumentation getInstrumentation() {
|
||||
Instrumentation instrumentation = Installer.instrumentation;
|
||||
@@ -26,4 +58,4 @@ public class Installer {
|
||||
return;
|
||||
Installer.instrumentation = instrumentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package art.arcane.iris.util.project.agent;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ClassLoaderCloseDeferralTest {
|
||||
@Test
|
||||
public void unarmedPluginDisableClosesNormally() throws Exception {
|
||||
TestLoader loader = new TestLoader(new URL[0]);
|
||||
|
||||
loader.close();
|
||||
Installer.releaseClassLoader(loader);
|
||||
|
||||
assertEquals(1, loader.closes.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void holdingOnePluginDoesNotHoldAnotherEqualLoader() throws Exception {
|
||||
TestLoader iris = new TestLoader(new URL[0]);
|
||||
TestLoader other = new TestLoader(new URL[0]);
|
||||
Installer.retainClassLoader(iris);
|
||||
try {
|
||||
other.close();
|
||||
iris.close();
|
||||
|
||||
assertEquals(1, other.closes.get());
|
||||
assertEquals(0, iris.closes.get());
|
||||
} finally {
|
||||
Installer.releaseClassLoader(iris);
|
||||
}
|
||||
assertEquals(1, iris.closes.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deferredJarRemainsReadableUntilCleanupReleasesIt() throws Exception {
|
||||
Path jar = Files.createTempFile("iris-loader-close", ".jar");
|
||||
try {
|
||||
try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(jar))) {
|
||||
output.putNextEntry(new JarEntry("late-cleanup.txt"));
|
||||
output.write("available".getBytes(StandardCharsets.UTF_8));
|
||||
output.closeEntry();
|
||||
}
|
||||
TestLoader loader = new TestLoader(new URL[]{jar.toUri().toURL()});
|
||||
Installer.retainClassLoader(loader);
|
||||
try {
|
||||
loader.close();
|
||||
loader.close();
|
||||
try (InputStream input = loader.getResourceAsStream("late-cleanup.txt")) {
|
||||
assertEquals("available", new String(input.readAllBytes(), StandardCharsets.UTF_8));
|
||||
}
|
||||
} finally {
|
||||
Installer.releaseClassLoader(loader);
|
||||
}
|
||||
Installer.releaseClassLoader(loader);
|
||||
assertEquals(1, loader.closes.get());
|
||||
assertNull(loader.getResourceAsStream("late-cleanup.txt"));
|
||||
} finally {
|
||||
Files.delete(jar);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancellingBeforePaperRequestsCloseDoesNotCloseTheLoader() throws Exception {
|
||||
TestLoader loader = new TestLoader(new URL[0]);
|
||||
Installer.retainClassLoader(loader);
|
||||
Installer.releaseClassLoader(loader);
|
||||
|
||||
assertEquals(0, loader.closes.get());
|
||||
loader.close();
|
||||
assertEquals(1, loader.closes.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentReleaseClosesOnceOutsideLifecycleLock() throws Exception {
|
||||
TestLoader loader = new TestLoader(new URL[0]);
|
||||
loader.closeEntered = new CountDownLatch(1);
|
||||
loader.finishClose = new CountDownLatch(1);
|
||||
Installer.retainClassLoader(loader);
|
||||
loader.close();
|
||||
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||
Thread first = new Thread(() -> release(loader, failure), "first-loader-release");
|
||||
first.start();
|
||||
try {
|
||||
assertTrue(loader.closeEntered.await(5L, TimeUnit.SECONDS));
|
||||
Thread second = new Thread(() -> release(loader, failure), "second-loader-release");
|
||||
second.start();
|
||||
second.join(5000L);
|
||||
assertFalse(second.isAlive());
|
||||
assertEquals(1, loader.closes.get());
|
||||
} finally {
|
||||
loader.finishClose.countDown();
|
||||
first.join(5000L);
|
||||
}
|
||||
assertFalse(first.isAlive());
|
||||
assertNull(failure.get());
|
||||
}
|
||||
|
||||
private static void release(TestLoader loader, AtomicReference<Throwable> failure) {
|
||||
try {
|
||||
Installer.releaseClassLoader(loader);
|
||||
} catch (Throwable exception) {
|
||||
failure.compareAndSet(null, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestLoader extends URLClassLoader {
|
||||
private final AtomicInteger closes = new AtomicInteger();
|
||||
private CountDownLatch closeEntered;
|
||||
private CountDownLatch finishClose;
|
||||
|
||||
private TestLoader(URL[] urls) {
|
||||
super(urls, ClassLoader.getPlatformClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (Installer.deferClassLoaderClose(this)) {
|
||||
return;
|
||||
}
|
||||
assertFalse(Thread.holdsLock(Installer.class));
|
||||
closes.incrementAndGet();
|
||||
if (closeEntered != null) {
|
||||
closeEntered.countDown();
|
||||
try {
|
||||
if (!finishClose.await(5L, TimeUnit.SECONDS)) {
|
||||
throw new IOException("Loader close was not released");
|
||||
}
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException(exception);
|
||||
}
|
||||
}
|
||||
super.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof TestLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user