mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 23:10:32 +00:00
Stabilized shutdown and Studio
- Shutdown protection initializes before enable and drains inline when retention fails - Cleanup preserves class loader retention and closes native providers in order - Error reports retain context and handle empty stack traces - Studio maps retain partial coverage and suspend hidden views - Hydrology previews refresh when plans and rasters complete - Terrain scratch hashing spreads lattice samples across cache slots - Plugin and probe artifacts include LWJGL libraries and native resources - Regression tests cover shutdown, error reporting, map progress, and terrain scratch
This commit is contained in:
1 parent
f64a7c6753
commit
3dc958edcf
31 files changed
+1973
-217
No files matched your search
@@ -57,6 +57,7 @@ import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
||||
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
||||
import art.arcane.iris.integration.MultiverseCoreLink;
|
||||
import art.arcane.iris.platform.agent.Agent;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||
import art.arcane.iris.studio.view.BukkitGuiHost;
|
||||
@@ -201,10 +202,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private final AtomicBoolean postStopFinisherStarted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false);
|
||||
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
|
||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
||||
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
||||
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||
private volatile boolean generatorDrainCompleted;
|
||||
private volatile boolean enableFailed;
|
||||
private volatile PlaceholderRegistration papiRegistration;
|
||||
@@ -225,6 +226,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private BukkitDebugDump debugDump;
|
||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||
private boolean shutdownProtectionInitialized;
|
||||
|
||||
public static VolmitSender getSender() {
|
||||
VolmitSender current = sender;
|
||||
@@ -474,10 +476,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
|
||||
String n = e.getClass().getName();
|
||||
StackTraceElement[] trace = e.getStackTrace();
|
||||
if (trace.length > 0) {
|
||||
n += "-" + trace[0].getClassName() + "-" + trace[0].getLineNumber();
|
||||
}
|
||||
|
||||
if (e.getCause() != null) {
|
||||
n += "-" + e.getCause().getStackTrace()[0].getClassName() + "-" + e.getCause().getStackTrace()[0].getLineNumber();
|
||||
StackTraceElement[] causeTrace = e.getCause().getStackTrace();
|
||||
n += "-" + (causeTrace.length == 0 ? e.getCause().getClass().getName()
|
||||
: causeTrace[0].getClassName() + "-" + causeTrace[0].getLineNumber());
|
||||
}
|
||||
|
||||
File f = instance.getDataFile("debug", "caught-exceptions", n + ".txt");
|
||||
@@ -627,10 +635,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
postStopFinisherStarted.set(false);
|
||||
serverStopTeardownDeferred.set(false);
|
||||
servicesDisabled.set(false);
|
||||
runtimeTeardownFailed.set(false);
|
||||
sharedRuntimeClosed.set(false);
|
||||
startupBoundaryRestart.set(false);
|
||||
terminalCleanupCompleted.set(false);
|
||||
runtimeTeardownFailed.set(false);
|
||||
generatorDrainCompleted = false;
|
||||
deferredShutdownGenerators.clear();
|
||||
MultiBurst.burst.reopen();
|
||||
@@ -911,6 +919,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
public void onEnable() {
|
||||
enableFailed = false;
|
||||
shutdownProtectionInitialized = false;
|
||||
IrisStartupValidation.begin();
|
||||
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
||||
try {
|
||||
@@ -922,13 +931,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
refuseUnsupportedServer();
|
||||
return;
|
||||
}
|
||||
initializeShutdownProtection();
|
||||
enable();
|
||||
} catch (Throwable failure) {
|
||||
enableFailed = true;
|
||||
if (!FailedEnableLock.engage(failure, FailedEnableLock.irisWorldsPresent(),
|
||||
generatorResolver.refusals().startupWorldsCreated())) {
|
||||
throw failure;
|
||||
}
|
||||
enableFailed = true;
|
||||
return;
|
||||
}
|
||||
BukkitGuiHost.install();
|
||||
@@ -946,6 +956,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
reportLockedRuntime();
|
||||
}
|
||||
|
||||
private void initializeShutdownProtection() {
|
||||
if (!INMS.get().supportsIrisWorldGeneration()) {
|
||||
return;
|
||||
}
|
||||
if (!Agent.install()) {
|
||||
throw new IllegalStateException("Iris Java agent is unavailable; shutdown protection cannot be installed.");
|
||||
}
|
||||
INMS.get().initializeLifecycle(getClass().getClassLoader());
|
||||
shutdownProtectionInitialized = true;
|
||||
}
|
||||
|
||||
private void refuseUnsupportedServer() {
|
||||
Throwable bindFailure = INMS.bindFailure();
|
||||
IllegalStateException failure = new IllegalStateException("Iris cannot start: " + (bindFailure == null
|
||||
@@ -1008,7 +1029,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
IrisLanguage.shutdown();
|
||||
teardownPapi();
|
||||
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping())
|
||||
&& INMS.get().deferPluginClassLoaderClose();
|
||||
&& tryDeferPluginClassLoaderClose();
|
||||
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
||||
if (restartingAtStartupBoundary) {
|
||||
teardownRuntime("startup-boundary-restart", 30L);
|
||||
@@ -1040,7 +1061,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
preservePendingBlockDrops();
|
||||
papiLifecycleActive = false;
|
||||
teardownPapi();
|
||||
if (IrisToolbelt.isServerStopping() && INMS.get().deferPluginClassLoaderClose()) {
|
||||
if (IrisToolbelt.isServerStopping() && tryDeferPluginClassLoaderClose()) {
|
||||
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
||||
startPostStopFinisher();
|
||||
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
||||
@@ -1055,6 +1076,19 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
private boolean tryDeferPluginClassLoaderClose() {
|
||||
if (!shutdownProtectionInitialized) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
INMS.get().deferPluginClassLoaderClose();
|
||||
return true;
|
||||
} catch (RuntimeException | LinkageError failure) {
|
||||
Iris.reportError("Failed to retain the Iris plugin class loader; draining shutdown inline.", failure);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drains the world generators exactly once. Serialized against the JVM shutdown hook so a
|
||||
* second caller cannot rip the pools or services out from under an in-flight drain.
|
||||
@@ -1101,19 +1135,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
try {
|
||||
MultiBurst.burst.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris generation workers.", failure);
|
||||
}
|
||||
try {
|
||||
MultiBurst.hydrology.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris hydrology workers.", failure);
|
||||
}
|
||||
try {
|
||||
MultiBurst.ioBurst.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
||||
}
|
||||
clearQueues();
|
||||
@@ -1229,30 +1260,37 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
if (!terminalCleanupCompleted.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
J.attempt(() -> INMS.get().uninjectBukkit());
|
||||
try {
|
||||
if (INMS.isBound()) {
|
||||
INMS.get().uninjectBukkit();
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to remove Iris world-generation hooks.", failure);
|
||||
}
|
||||
try {
|
||||
runPostShutdown();
|
||||
} catch (Throwable e) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
||||
} finally {
|
||||
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
|
||||
}
|
||||
boolean activeResources = serverStopTeardownDeferred.get()
|
||||
&& (runtimeTeardownFailed.get() || hasActiveShutdownResources());
|
||||
IrisPlatforms.unbind();
|
||||
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
|
||||
BukkitPlatform.releaseHost();
|
||||
if (activeResources) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Owned background work has not completed cleanup; retaining the plugin class loader until JVM exit.");
|
||||
} else {
|
||||
try {
|
||||
INMS.get().releasePluginClassLoaderClose();
|
||||
SlimJar.closeNativeProviders();
|
||||
if (shutdownProtectionInitialized) {
|
||||
INMS.get().releasePluginClassLoaderClose();
|
||||
} else {
|
||||
SlimJar.closeNativeProviders();
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
|
||||
failure.printStackTrace(SHUTDOWN_ERRORS);
|
||||
}
|
||||
}
|
||||
BukkitPlatform.releaseHost();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,6 +148,9 @@ public class FailedEnableLockTest {
|
||||
try (EnableFixture fixture = new EnableFixture(false)) {
|
||||
IllegalStateException failure = assertThrows(IllegalStateException.class, plugin::onEnable);
|
||||
assertTrue(failure.getMessage(), failure.getMessage().contains("already bound"));
|
||||
Field failed = Iris.class.getDeclaredField("enableFailed");
|
||||
failed.setAccessible(true);
|
||||
assertTrue(failed.getBoolean(plugin));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisErrorReportingTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void debugReportingPreservesStacklessFailures() throws Exception {
|
||||
ArrayIndexOutOfBoundsException failure = new ArrayIndexOutOfBoundsException("raster index");
|
||||
failure.setStackTrace(new StackTraceElement[0]);
|
||||
|
||||
String output = reportWithDebug(failure);
|
||||
|
||||
assertEquals(1L, output.lines().filter(line -> line.contains("raster index")).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void debugReportingPreservesFailureWithStacklessCause() throws Exception {
|
||||
ArrayIndexOutOfBoundsException cause = new ArrayIndexOutOfBoundsException("raster index");
|
||||
cause.setStackTrace(new StackTraceElement[0]);
|
||||
IllegalStateException failure = new IllegalStateException("Hydrology failed", cause);
|
||||
|
||||
String output = reportWithDebug(failure);
|
||||
|
||||
assertEquals(1L, output.lines().filter(line -> line.contains("Hydrology failed")).count());
|
||||
assertTrue(output.contains("Caused by: java.lang.ArrayIndexOutOfBoundsException: raster index"));
|
||||
assertTrue(output.contains("IrisErrorReportingTest.debugReportingPreservesFailureWithStacklessCause"));
|
||||
}
|
||||
|
||||
private String reportWithDebug(Throwable failure) throws Exception {
|
||||
Iris previousPlugin = Iris.instance;
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
PrintStream previousError = System.err;
|
||||
Iris plugin = mock(Iris.class);
|
||||
Field deferred = Iris.class.getDeclaredField("serverStopTeardownDeferred");
|
||||
deferred.setAccessible(true);
|
||||
deferred.set(plugin, new AtomicBoolean());
|
||||
File report = new File(temporaryFolder.getRoot(), "failure.txt");
|
||||
when(plugin.getDataFile(any(String[].class))).thenReturn(report);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
try (PrintStream errors = new PrintStream(output, true, StandardCharsets.UTF_8);
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
Iris.instance = plugin;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
IrisSettings.settings.getGeneral().setDebug(true);
|
||||
System.setErr(errors);
|
||||
iris.when(() -> Iris.reportError(failure)).thenCallRealMethod();
|
||||
|
||||
Iris.reportError(failure);
|
||||
|
||||
assertTrue(Files.readString(report.toPath()).contains(failure.toString()));
|
||||
return output.toString(StandardCharsets.UTF_8);
|
||||
} finally {
|
||||
System.setErr(previousError);
|
||||
IrisSettings.settings = previousSettings;
|
||||
Iris.instance = previousPlugin;
|
||||
}
|
||||
}
|
||||
}
|
||||
+227
-5
@@ -1,7 +1,17 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.agent.Agent;
|
||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitPlugin;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.world.lifecycle.IrisRuntimeStatics;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionManager;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
@@ -16,8 +26,8 @@ import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.WorldMaintenance;
|
||||
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.integration.ReloadAware;
|
||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||
import art.arcane.volmlib.integration.ReloadAware;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
@@ -27,10 +37,13 @@ import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -38,12 +51,16 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -152,13 +169,181 @@ public class IrisShutdownMaintenanceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupEnsuresAgentReadinessBeforeLifecycleProtectionWithoutWorldInjection() throws Exception {
|
||||
try (Fixture fixture = new Fixture(); MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||
when(fixture.binding.supportsIrisWorldGeneration()).thenReturn(true);
|
||||
agent.when(Agent::install).thenReturn(true);
|
||||
doAnswer(invocation -> {
|
||||
agent.verify(Agent::install);
|
||||
assertSame(fixture.plugin.getClass().getClassLoader(), invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(fixture.binding).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||
invoke(fixture.plugin, "initializeShutdownProtection");
|
||||
verify(fixture.binding).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||
verify(fixture.binding, never()).injectBukkit();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableAgentStopsLifecycleInitialization() throws Exception {
|
||||
try (Fixture fixture = new Fixture(); MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||
when(fixture.binding.supportsIrisWorldGeneration()).thenReturn(true);
|
||||
InvocationTargetException failure = assertThrows(InvocationTargetException.class,
|
||||
() -> invoke(fixture.plugin, "initializeShutdownProtection"));
|
||||
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||
verify(fixture.binding, never()).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worldHookTeardownFailureReleasesProtectionAfterServicesAndWorkersStop() throws Exception {
|
||||
try (Fixture fixture = new Fixture();
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class);
|
||||
MockedStatic<SlimJar> providers = mockStatic(SlimJar.class)) {
|
||||
IrisService service = mock(IrisService.class);
|
||||
IllegalStateException failure = new IllegalStateException("world hook teardown failed");
|
||||
fixture.nms.when(INMS::isBound).thenReturn(true);
|
||||
doThrow(failure).when(fixture.binding).uninjectBukkit();
|
||||
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||
doAnswer(invocation -> {
|
||||
platforms.verify(IrisPlatforms::unbind);
|
||||
bukkitPlatform.verify(BukkitPlatform::releaseHost);
|
||||
assertTrue(MultiBurst.burst.isTerminated());
|
||||
assertTrue(MultiBurst.hydrology.isTerminated());
|
||||
assertTrue(MultiBurst.ioBurst.isTerminated());
|
||||
return null;
|
||||
}).when(fixture.binding).releasePluginClassLoaderClose();
|
||||
fixture.quiesce();
|
||||
try {
|
||||
fixture.finish(5L);
|
||||
fixture.finish(5L);
|
||||
verify(service).onDisable();
|
||||
iris.verify(() -> Iris.reportError("Failed to remove Iris world-generation hooks.", failure));
|
||||
assertTrue(MultiBurst.burst.isTerminated());
|
||||
assertTrue(MultiBurst.hydrology.isTerminated());
|
||||
assertTrue(MultiBurst.ioBurst.isTerminated());
|
||||
verify(fixture.binding).releasePluginClassLoaderClose();
|
||||
platforms.verify(IrisPlatforms::unbind);
|
||||
bukkitPlatform.verify(BukkitPlatform::releaseHost);
|
||||
} finally {
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.hydrology.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedServiceWithAnOwnedExecutorRetainsTheClassLoader() throws Exception {
|
||||
ExecutorService ownedExecutor = Executors.newSingleThreadExecutor();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch finish = new CountDownLatch(1);
|
||||
ownedExecutor.submit(() -> {
|
||||
started.countDown();
|
||||
finish.await();
|
||||
return null;
|
||||
});
|
||||
try (Fixture fixture = new Fixture();
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class)) {
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
IrisService service = mock(IrisService.class);
|
||||
IllegalStateException failure = new IllegalStateException("service teardown failed");
|
||||
doThrow(failure).when(service).onDisable();
|
||||
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||
fixture.quiesce();
|
||||
fixture.finish(5L);
|
||||
verify(service).onDisable();
|
||||
iris.verify(() -> Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", failure));
|
||||
assertFalse(ownedExecutor.isTerminated());
|
||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||
} finally {
|
||||
finish.countDown();
|
||||
ownedExecutor.shutdownNow();
|
||||
assertTrue(ownedExecutor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.hydrology.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedPostShutdownCallbackRetainsTheClassLoaderForSkippedCleanup() throws Exception {
|
||||
ExecutorService ownedExecutor = Executors.newSingleThreadExecutor();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch finish = new CountDownLatch(1);
|
||||
ownedExecutor.submit(() -> {
|
||||
started.countDown();
|
||||
finish.await();
|
||||
return null;
|
||||
});
|
||||
try (Fixture fixture = new Fixture();
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class)) {
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
IllegalStateException failure = new IllegalStateException("post-shutdown callback failed");
|
||||
AtomicBoolean cleanupAttempted = new AtomicBoolean();
|
||||
KList<Runnable> callbacks = new KList<>();
|
||||
callbacks.add(() -> { throw failure; });
|
||||
callbacks.add(() -> {
|
||||
cleanupAttempted.set(true);
|
||||
ownedExecutor.shutdown();
|
||||
});
|
||||
Field callbacksField = VolmitPlugin.class.getDeclaredField("postShutdown");
|
||||
callbacksField.setAccessible(true);
|
||||
callbacksField.set(fixture.plugin, callbacks);
|
||||
Method runCallbacks = VolmitPlugin.class.getDeclaredMethod("runPostShutdown");
|
||||
runCallbacks.setAccessible(true);
|
||||
runCallbacks.invoke(doCallRealMethod().when(fixture.plugin));
|
||||
fixture.quiesce();
|
||||
fixture.finish(5L);
|
||||
iris.verify(() -> Iris.reportError("Failed to run Iris post-shutdown cleanup.", failure));
|
||||
assertFalse(cleanupAttempted.get());
|
||||
assertFalse(ownedExecutor.isShutdown());
|
||||
assertFalse(ownedExecutor.isTerminated());
|
||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||
} finally {
|
||||
finish.countDown();
|
||||
ownedExecutor.shutdownNow();
|
||||
assertTrue(ownedExecutor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.hydrology.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompleteDrainRetainsServicesWorkersAndClassLoader() throws Exception {
|
||||
try (Fixture fixture = new Fixture(); MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
IrisService service = mock(IrisService.class);
|
||||
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||
when(fixture.generator.closeAsync()).thenReturn(new CompletableFuture<>());
|
||||
fixture.quiesce();
|
||||
fixture.finish(0L);
|
||||
verify(service, never()).onDisable();
|
||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||
verify(fixture.binding, never()).uninjectBukkit();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverStopPreUnloadDefersTeardownWhileTheClassLoaderIsHeld() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
when(fixture.binding.deferPluginClassLoaderClose()).thenReturn(true);
|
||||
setField(fixture.plugin, "postStopFinisherStarted", new AtomicBoolean(true));
|
||||
try {
|
||||
fixture.commitReloadDuringServerStop();
|
||||
verify(fixture.binding).deferPluginClassLoaderClose();
|
||||
verify(fixture.generator).quiesceForServerShutdown();
|
||||
verify(fixture.generator, never()).closeAsync();
|
||||
assertTrue(fixture.serverStopTeardownDeferred());
|
||||
@@ -170,19 +355,43 @@ public class IrisShutdownMaintenanceTest {
|
||||
|
||||
@Test
|
||||
public void serverStopPreUnloadDrainsInlineWhenTheClassLoaderCannotBeHeld() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
when(fixture.binding.deferPluginClassLoaderClose()).thenReturn(false);
|
||||
try (Fixture fixture = new Fixture(); MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
IllegalStateException failure = new IllegalStateException("class loader hold failed");
|
||||
doThrow(failure).when(fixture.binding).deferPluginClassLoaderClose();
|
||||
try {
|
||||
fixture.commitReloadDuringServerStop();
|
||||
verify(fixture.generator).closeAsync();
|
||||
verify(fixture.generator, never()).quiesceForServerShutdown();
|
||||
assertFalse(fixture.serverStopTeardownDeferred());
|
||||
iris.verify(() -> Iris.reportError("Failed to retain the Iris plugin class loader; draining shutdown inline.", failure));
|
||||
} finally {
|
||||
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverStopPreUnloadDrainsInlineWithoutInitializedProtection() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
setField(fixture.plugin, "shutdownProtectionInitialized", false);
|
||||
try {
|
||||
fixture.commitReloadDuringServerStop();
|
||||
verify(fixture.binding, never()).deferPluginClassLoaderClose();
|
||||
verify(fixture.generator).closeAsync();
|
||||
verify(fixture.generator, never()).quiesceForServerShutdown();
|
||||
assertFalse(fixture.serverStopTeardownDeferred());
|
||||
} finally {
|
||||
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void invoke(Iris plugin, String name) throws ReflectiveOperationException {
|
||||
Method method = Iris.class.getDeclaredMethod(name);
|
||||
method.setAccessible(true);
|
||||
method.invoke(plugin);
|
||||
}
|
||||
|
||||
private static void setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
|
||||
Field field = Iris.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
@@ -210,6 +419,12 @@ public class IrisShutdownMaintenanceTest {
|
||||
|
||||
private Fixture() throws ReflectiveOperationException {
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
setField(plugin, "shutdownProtectionInitialized", true);
|
||||
setField(plugin, "alreadyDrained", new AtomicBoolean());
|
||||
setField(plugin, "servicesDisabled", new AtomicBoolean());
|
||||
setField(plugin, "runtimeTeardownFailed", new AtomicBoolean());
|
||||
setField(plugin, "sharedRuntimeClosed", new AtomicBoolean());
|
||||
setField(plugin, "terminalCleanupCompleted", new AtomicBoolean());
|
||||
setField(plugin, "serverStopTeardownDeferred", new AtomicBoolean());
|
||||
setField(plugin, "postStopFinisherStarted", new AtomicBoolean());
|
||||
setField(plugin, "deferredShutdownGenerators", new CopyOnWriteArrayList<PlatformChunkGenerator>());
|
||||
@@ -251,6 +466,13 @@ public class IrisShutdownMaintenanceTest {
|
||||
return ((AtomicBoolean) field.get(plugin)).get();
|
||||
}
|
||||
|
||||
|
||||
private void finish(long timeoutSeconds) throws ReflectiveOperationException {
|
||||
Method method = Iris.class.getDeclaredMethod("finishDeferredRuntimeTeardown", String.class, long.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(plugin, "test_shutdown", timeoutSeconds);
|
||||
}
|
||||
|
||||
private void startFinisher() throws ReflectiveOperationException {
|
||||
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
|
||||
method.setAccessible(true);
|
||||
|
||||
Reference in new issue
Block a user