mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-23 00:28:02 +00:00
Fixed pregen shutdown scheduling
- Scheduler admission avoids the plugin manager monitor - Saving callbacks no longer block the joined pregenerator during disable
This commit is contained in:
commit
04103bd15a
2 files changed
+224
-1
No files matched your search
@@ -772,7 +772,7 @@ public class J {
|
||||
}
|
||||
|
||||
private static boolean isPluginEnabled() {
|
||||
return usesBukkitScheduler() && BukkitPlatform.hasPlugin() && Bukkit.getPluginManager().isPluginEnabled(BukkitPlatform.plugin());
|
||||
return usesBukkitScheduler() && BukkitPlatform.hasPlugin() && BukkitPlatform.plugin().isEnabled();
|
||||
}
|
||||
|
||||
private static boolean canSchedule() {
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.core.pregenerator.IrisPregenerator;
|
||||
import art.arcane.iris.core.pregenerator.PregenApiPhase;
|
||||
import art.arcane.iris.core.pregenerator.PregenApiSink;
|
||||
import art.arcane.iris.core.pregenerator.PregenPhaseTracker;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.format.MemoryMonitor;
|
||||
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.plugin.IllegalPluginAccessException;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.bukkit.scheduler.BukkitScheduler;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class PregeneratorJobShutdownDispatchTest {
|
||||
@Test
|
||||
public void pluginDisableCanJoinAWorkerAlreadyDispatchingSaving() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
CountDownLatch stop = new CountDownLatch(1);
|
||||
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||
Thread worker = new Thread(() -> {
|
||||
try {
|
||||
fixture.withBukkit(() -> {
|
||||
fixture.job.onSaving();
|
||||
try {
|
||||
stop.await();
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
fixture.job.onClose();
|
||||
});
|
||||
} catch (Throwable error) {
|
||||
failure.set(error);
|
||||
}
|
||||
});
|
||||
fixture.set("worker", worker);
|
||||
doAnswer(invocation -> {
|
||||
stop.countDown();
|
||||
return null;
|
||||
}).when(fixture.pregenerator).close();
|
||||
AtomicReference<PregeneratorJob> instance = instance();
|
||||
PregeneratorJob previous = instance.getAndSet(fixture.job);
|
||||
try {
|
||||
synchronized (fixture.manager) {
|
||||
worker.start();
|
||||
assertTrue(fixture.dispatchEntered.await(5, TimeUnit.SECONDS));
|
||||
fixture.enabled.set(false);
|
||||
assertTrue(PregeneratorJob.shutdownAndWait(500));
|
||||
}
|
||||
assertFalse(worker.isAlive());
|
||||
assertNull(failure.get());
|
||||
verify(fixture.pregenerator).close();
|
||||
verify(fixture.monitor).close();
|
||||
verify(fixture.service).shutdownNow();
|
||||
verify(fixture.manager, never()).isPluginEnabled(fixture.plugin);
|
||||
} finally {
|
||||
stop.countDown();
|
||||
worker.interrupt();
|
||||
worker.join(5_000);
|
||||
assertFalse(worker.isAlive());
|
||||
instance.set(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enabledJobsKeepScheduledSavingAndTerminalEvents() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.withBukkit(() -> {
|
||||
fixture.job.onSaving();
|
||||
fixture.job.onClose();
|
||||
});
|
||||
assertTrue(fixture.delivered.isEmpty());
|
||||
assertEquals(2, fixture.queued.size());
|
||||
fixture.queued.forEach(Runnable::run);
|
||||
assertEquals(List.of(PregenApiPhase.SAVING, PregenApiPhase.CANCELLED), fixture.delivered);
|
||||
verify(fixture.monitor).close();
|
||||
verify(fixture.service).shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disableAfterEnabledCheckReportsRejectionAndStillCleansUp() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
IllegalPluginAccessException rejected = new IllegalPluginAccessException("Plugin disabled during scheduling");
|
||||
when(fixture.scheduler.scheduleSyncDelayedTask(eq(fixture.plugin), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
fixture.enabled.set(false);
|
||||
throw rejected;
|
||||
});
|
||||
fixture.withBukkit(() -> {
|
||||
fixture.job.onSaving();
|
||||
fixture.job.onClose();
|
||||
});
|
||||
verify(fixture.platform).reportError(contains("SAVING"), eq(rejected));
|
||||
verify(fixture.monitor).close();
|
||||
verify(fixture.service).shutdownNow();
|
||||
assertTrue(fixture.queued.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unexpectedSchedulingFailureRetainsItsFullCause() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
IllegalStateException failure = new IllegalStateException("Scheduler unavailable");
|
||||
when(fixture.scheduler.scheduleSyncDelayedTask(eq(fixture.plugin), any(Runnable.class))).thenThrow(failure);
|
||||
fixture.withBukkit(fixture.job::onSaving);
|
||||
verify(fixture.platform).reportError(contains("SAVING"), eq(failure));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static AtomicReference<PregeneratorJob> instance() throws Exception {
|
||||
Field field = PregeneratorJob.class.getDeclaredField("instance");
|
||||
field.setAccessible(true);
|
||||
return (AtomicReference<PregeneratorJob>) field.get(null);
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final PregeneratorJob job = mock(PregeneratorJob.class, CALLS_REAL_METHODS);
|
||||
private final IrisPregenerator pregenerator = mock(IrisPregenerator.class);
|
||||
private final MemoryMonitor monitor = mock(MemoryMonitor.class);
|
||||
private final ExecutorService service = mock(ExecutorService.class);
|
||||
private final IrisPlatform platform = mock(IrisPlatform.class);
|
||||
private final Plugin plugin = mock(Plugin.class);
|
||||
private final PluginManager manager = mock(PluginManager.class);
|
||||
private final BukkitScheduler scheduler = mock(BukkitScheduler.class);
|
||||
private final Server server = mock(Server.class);
|
||||
private final AtomicBoolean enabled = new AtomicBoolean(true);
|
||||
private final CountDownLatch dispatchEntered = new CountDownLatch(1);
|
||||
private final List<Runnable> queued = new ArrayList<>();
|
||||
private final List<PregenApiPhase> delivered = new ArrayList<>();
|
||||
private final PregenApiSink previousSink = IrisServices.getOrNull(PregenApiSink.class);
|
||||
|
||||
private Fixture() throws Exception {
|
||||
when(platform.platformName()).thenReturn("bukkit");
|
||||
when(plugin.isEnabled()).thenAnswer(invocation -> enabled.get());
|
||||
when(manager.isPluginEnabled(plugin)).thenAnswer(invocation -> {
|
||||
synchronized (manager) {
|
||||
return plugin.isEnabled();
|
||||
}
|
||||
});
|
||||
when(scheduler.scheduleSyncDelayedTask(eq(plugin), any(Runnable.class))).thenAnswer(invocation -> {
|
||||
queued.add(invocation.getArgument(1));
|
||||
return queued.size();
|
||||
});
|
||||
IrisPlatforms.bind(platform);
|
||||
BukkitPlatform.hostPlugin(plugin);
|
||||
PregenApiSink sink = (phase, progress) -> {
|
||||
dispatchEntered.countDown();
|
||||
J.s(() -> delivered.add(phase));
|
||||
};
|
||||
IrisServices.register(PregenApiSink.class, sink);
|
||||
set("bounds", new PregenRenderSnapshot.Bounds(0, 0, 0, 0));
|
||||
set("pregenerator", pregenerator);
|
||||
set("monitor", monitor);
|
||||
set("service", service);
|
||||
set("saving", new AtomicBoolean());
|
||||
set("stopRequested", new AtomicBoolean());
|
||||
set("apiPhases", new PregenPhaseTracker());
|
||||
set("whenDone", List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (previousSink == null) {
|
||||
IrisServices.remove(PregenApiSink.class);
|
||||
} else {
|
||||
IrisServices.register(PregenApiSink.class, previousSink);
|
||||
}
|
||||
BukkitPlatform.hostPlugin(null);
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
private void withBukkit(Runnable action) {
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||
bukkit.when(Bukkit::getPluginManager).thenReturn(manager);
|
||||
bukkit.when(Bukkit::getScheduler).thenReturn(scheduler);
|
||||
bukkit.when(Bukkit::getServer).thenReturn(server);
|
||||
action.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void set(String name, Object value) throws Exception {
|
||||
Field field = PregeneratorJob.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(job, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user