mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 23:10:32 +00:00
Fixed shutdown teardown and shutdown staging wait
- Server stop defers teardown only while the plugin class loader close is held - Shutdown with no Iris world loaded tears down during plugin disable - Pre-unload hook drains generators inline when the class loader close cannot be held - INMSBinding reports whether the class loader close is held - Shutdown tests cover the held and unheld pre-unload paths - Shutdown waits for an in-progress world replacement to finish staging - Replacements started after shutdown begins are refused - Admission tests cover the shutdown wait and the refusal
This commit is contained in:
1 parent
494cdb1a5c
commit
2690e0878b
6 files changed
+169
-8
No files matched your search
+47
-2
@@ -16,6 +16,7 @@ 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 org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
@@ -37,9 +38,11 @@ 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.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.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -109,8 +112,6 @@ public class IrisShutdownMaintenanceTest {
|
||||
}
|
||||
verify(fixture.generator).quiesceForServerShutdown();
|
||||
verify(fixture.generator, never()).closeAsync();
|
||||
fixture.nms.verify(INMS::get);
|
||||
verify(fixture.binding).deferPluginClassLoaderClose();
|
||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||
}
|
||||
assertEquals(0, sessions.activeLeases());
|
||||
@@ -151,6 +152,37 @@ public class IrisShutdownMaintenanceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@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.generator).quiesceForServerShutdown();
|
||||
verify(fixture.generator, never()).closeAsync();
|
||||
assertTrue(fixture.serverStopTeardownDeferred());
|
||||
} finally {
|
||||
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverStopPreUnloadDrainsInlineWhenTheClassLoaderCannotBeHeld() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
when(fixture.binding.deferPluginClassLoaderClose()).thenReturn(false);
|
||||
try {
|
||||
fixture.commitReloadDuringServerStop();
|
||||
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 setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
|
||||
Field field = Iris.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
@@ -206,6 +238,19 @@ public class IrisShutdownMaintenanceTest {
|
||||
method.invoke(plugin, "manual_disable", 5L);
|
||||
}
|
||||
|
||||
private void commitReloadDuringServerStop() throws ReflectiveOperationException {
|
||||
setField(plugin, "alreadyDrained", new AtomicBoolean());
|
||||
toolbelt.when(IrisToolbelt::isServerStopping).thenReturn(true);
|
||||
doCallRealMethod().when(plugin).commitReload(any());
|
||||
plugin.commitReload(ReloadAware.PreUnloadReason.HOT_UNLOAD);
|
||||
}
|
||||
|
||||
private boolean serverStopTeardownDeferred() throws ReflectiveOperationException {
|
||||
Field field = Iris.class.getDeclaredField("serverStopTeardownDeferred");
|
||||
field.setAccessible(true);
|
||||
return ((AtomicBoolean) field.get(plugin)).get();
|
||||
}
|
||||
|
||||
private void startFinisher() throws ReflectiveOperationException {
|
||||
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
|
||||
method.setAccessible(true);
|
||||
|
||||
+55
@@ -11,12 +11,16 @@ import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@@ -42,4 +46,55 @@ public class PendingWorldReplacementAdmissionTest {
|
||||
executor.awaitTermination(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownRejectsReplacementsThatStartAfterIt() {
|
||||
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||
manager.awaitStagingForShutdown();
|
||||
|
||||
IOException rejected = assertThrows(IOException.class, () -> manager.stageReplacement(
|
||||
mock(VolmitSender.class), NamespacedKey.minecraft("overworld"), mock(IrisDimension.class), null));
|
||||
|
||||
assertTrue(rejected.getMessage().contains("shutting down"));
|
||||
assertTrue(LifecycleOperationCoordinator.get().isIdle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownWaitsForTheReplacementBeingStaged() throws Exception {
|
||||
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||
AtomicReference<Throwable> stagingFailure = new AtomicReference<>();
|
||||
Thread staging = new Thread(() -> {
|
||||
try {
|
||||
manager.stageReplacement(mock(VolmitSender.class), NamespacedKey.minecraft("overworld"),
|
||||
mock(IrisDimension.class), null);
|
||||
} catch (Throwable failure) {
|
||||
stagingFailure.set(failure);
|
||||
}
|
||||
});
|
||||
Thread shutdown = new Thread(manager::awaitStagingForShutdown);
|
||||
synchronized (manager) {
|
||||
staging.start();
|
||||
awaitState(staging, Thread.State.BLOCKED);
|
||||
shutdown.start();
|
||||
awaitState(shutdown, Thread.State.WAITING);
|
||||
assertTrue(staging.isAlive());
|
||||
}
|
||||
shutdown.join(TimeUnit.SECONDS.toMillis(10L));
|
||||
staging.join(TimeUnit.SECONDS.toMillis(10L));
|
||||
|
||||
assertFalse(shutdown.isAlive());
|
||||
assertFalse(staging.isAlive());
|
||||
assertTrue(stagingFailure.get() instanceof Exception);
|
||||
assertTrue(LifecycleOperationCoordinator.get().isIdle());
|
||||
}
|
||||
|
||||
private static void awaitState(Thread thread, Thread.State state) throws InterruptedException {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||
while (thread.getState() != state) {
|
||||
if (System.nanoTime() > deadline) {
|
||||
throw new AssertionError(thread.getName() + " never reached " + state + "; it is " + thread.getState());
|
||||
}
|
||||
Thread.sleep(5L);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user