diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/AdvancedCorePlugin.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/AdvancedCorePlugin.java index cc75a6d9f6..b1d30897a9 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/AdvancedCorePlugin.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/AdvancedCorePlugin.java @@ -1111,58 +1111,20 @@ private void loadVersionFile() { @Override public void onDisable() { - if (getOptions().isJavascriptEngineEnabled()) { - getLogger().info("Shutting down Javascript engine"); - JavascriptEngineHandler.getInstance().clearCachedEngine(); - } - - if (loadUserData && UserStorage.MYSQL.equals(getOptions().getStorageType()) && getMysql() != null) { - getMysql().close(); - } - getServerDataFile().setLastUpdated(); - timer.shutdown(); - loginTimer.shutdown(); - timeChecker.getTimer().shutdown(); - inventoryTimer.shutdown(); - try { - getLogger().info("Allowing background tasks to finish, this could take up to 5 seconds"); - loginTimer.awaitTermination(2, TimeUnit.SECONDS); - timer.awaitTermination(2, TimeUnit.SECONDS); - timeChecker.getTimer().awaitTermination(2, TimeUnit.SECONDS); - inventoryTimer.awaitTermination(1, TimeUnit.SECONDS); - } catch (InterruptedException e) { - debug(e); - } - rewardHandler.shutdown(); - loginTimer.shutdownNow(); - timer.shutdownNow(); - inventoryTimer.shutdownNow(); - timeChecker.getTimer().shutdownNow(); - onUnLoad(); - getSkullCacheHandler().close(); - fullInventoryHandler.save(); - - hologramHandler.onShutDown(); - - if (getPermissionHandler() != null) { - getPermissionHandler().shutDown(); - } - - if (dialogService != null) { - dialogService.unregister(); - } - + new com.bencodez.advancedcore.lifecycle.AdvancedCoreLifecycle(this).shutdown(); javaPlugin = null; } @Override public void onEnable() { javaPlugin = this; - bukkitScheduler = new BukkitScheduler(this); - timer = Executors.newSingleThreadScheduledExecutor(); - loginTimer = Executors.newSingleThreadScheduledExecutor(); + com.bencodez.advancedcore.lifecycle.AdvancedCoreLifecycle.RuntimeExecutors runtimeExecutors = com.bencodez.advancedcore.lifecycle.AdvancedCoreLifecycle + .createRuntimeExecutors(this); + bukkitScheduler = runtimeExecutors.getBukkitScheduler(); + timer = runtimeExecutors.getTimer(); + loginTimer = runtimeExecutors.getLoginTimer(); advancedCoreCommandLoader = CommandLoader.getInstance(); - inventoryTimer = Executors.newSingleThreadScheduledExecutor(); + inventoryTimer = runtimeExecutors.getInventoryTimer(); onPreLoad(); diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/lifecycle/AdvancedCoreLifecycle.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/lifecycle/AdvancedCoreLifecycle.java new file mode 100644 index 0000000000..bff6d6b4b4 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/lifecycle/AdvancedCoreLifecycle.java @@ -0,0 +1,177 @@ +package com.bencodez.advancedcore.lifecycle; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.item.FullInventoryHandler; +import com.bencodez.advancedcore.api.javascript.JavascriptEngineHandler; +import com.bencodez.advancedcore.api.time.TimeChecker; +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.simpleapi.scheduler.BukkitScheduler; + +/** + * Owns AdvancedCore runtime executor creation and best-effort shutdown sequencing. + * Public plugin getters remain the compatibility surface; this class only + * centralizes lifecycle mechanics that were previously embedded in the plugin. + */ +public final class AdvancedCoreLifecycle { + + private final AdvancedCorePlugin plugin; + + public AdvancedCoreLifecycle(AdvancedCorePlugin plugin) { + this.plugin = plugin; + } + + public static RuntimeExecutors createRuntimeExecutors(AdvancedCorePlugin plugin) { + return new RuntimeExecutors(new BukkitScheduler(plugin), Executors.newSingleThreadScheduledExecutor(), + Executors.newSingleThreadScheduledExecutor(), Executors.newSingleThreadScheduledExecutor()); + } + + public void shutdown() { + if (plugin == null) { + return; + } + + runCleanup("Javascript engine", () -> { + if (plugin.getOptions() != null && plugin.getOptions().isJavascriptEngineEnabled()) { + plugin.getLogger().info("Shutting down Javascript engine"); + JavascriptEngineHandler.getInstance().clearCachedEngine(); + } + }); + + runCleanup("MySQL", () -> { + if (plugin.isLoadUserData() && plugin.getOptions() != null + && UserStorage.MYSQL.equals(plugin.getOptions().getStorageType()) && plugin.getMysql() != null) { + plugin.getMysql().close(); + } + }); + + runCleanup("server data timestamp", () -> { + if (plugin.getServerDataFile() != null) { + plugin.getServerDataFile().setLastUpdated(); + } + }); + + ScheduledExecutorService timeTimer = null; + TimeChecker timeChecker = plugin.getTimeChecker(); + if (timeChecker != null) { + timeTimer = timeChecker.getTimer(); + } + + shutdown(plugin.getLoginTimer()); + shutdown(plugin.getTimer()); + shutdown(timeTimer); + shutdown(plugin.getInventoryTimer()); + + plugin.getLogger().info("Allowing background tasks to finish before shutdown"); + await(plugin.getLoginTimer(), 2, TimeUnit.SECONDS); + await(plugin.getTimer(), 2, TimeUnit.SECONDS); + await(timeTimer, 2, TimeUnit.SECONDS); + await(plugin.getInventoryTimer(), 1, TimeUnit.SECONDS); + + runCleanup("reward handler", () -> { + if (plugin.getRewardHandler() != null) { + plugin.getRewardHandler().shutdown(); + } + }); + + shutdownNow(plugin.getLoginTimer()); + shutdownNow(plugin.getTimer()); + shutdownNow(timeTimer); + shutdownNow(plugin.getInventoryTimer()); + + runCleanup("plugin unload hook", plugin::onUnLoad); + runCleanup("skull cache", () -> { + if (plugin.getSkullCacheHandler() != null) { + plugin.getSkullCacheHandler().close(); + } + }); + runCleanup("full inventory handler", () -> { + FullInventoryHandler handler = plugin.getFullInventoryHandler(); + if (handler != null) { + handler.shutdown(); + handler.save(); + } + }); + runCleanup("hologram handler", () -> { + if (plugin.getHologramHandler() != null) { + plugin.getHologramHandler().onShutDown(); + } + }); + runCleanup("permission handler", () -> { + if (plugin.getPermissionHandler() != null) { + plugin.getPermissionHandler().shutDown(); + } + }); + runCleanup("dialog service", () -> { + if (plugin.getDialogService() != null) { + plugin.getDialogService().unregister(); + } + }); + } + + private void runCleanup(String component, Runnable cleanup) { + try { + cleanup.run(); + } catch (Throwable e) { + plugin.getLogger().warning("Failed to shut down " + component + ": " + e.getMessage()); + plugin.debug(e); + } + } + + static void shutdown(ScheduledExecutorService executor) { + if (executor != null && !executor.isShutdown()) { + executor.shutdown(); + } + } + + static void shutdownNow(ScheduledExecutorService executor) { + if (executor != null && !executor.isTerminated()) { + executor.shutdownNow(); + } + } + + static void await(ScheduledExecutorService executor, long timeout, TimeUnit unit) { + if (executor == null) { + return; + } + try { + executor.awaitTermination(timeout, unit); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static final class RuntimeExecutors { + private final BukkitScheduler bukkitScheduler; + private final ScheduledExecutorService timer; + private final ScheduledExecutorService loginTimer; + private final ScheduledExecutorService inventoryTimer; + + RuntimeExecutors(BukkitScheduler bukkitScheduler, ScheduledExecutorService timer, + ScheduledExecutorService loginTimer, ScheduledExecutorService inventoryTimer) { + this.bukkitScheduler = bukkitScheduler; + this.timer = timer; + this.loginTimer = loginTimer; + this.inventoryTimer = inventoryTimer; + } + + public BukkitScheduler getBukkitScheduler() { + return bukkitScheduler; + } + + public ScheduledExecutorService getTimer() { + return timer; + } + + public ScheduledExecutorService getLoginTimer() { + return loginTimer; + } + + public ScheduledExecutorService getInventoryTimer() { + return inventoryTimer; + } + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/AdvancedCoreLifecycleTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/AdvancedCoreLifecycleTest.java new file mode 100644 index 0000000000..2e0fae365e --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/AdvancedCoreLifecycleTest.java @@ -0,0 +1,74 @@ +package com.bencodez.advancedcore.lifecycle; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import org.junit.jupiter.api.Test; + +import com.bencodez.advancedcore.AdvancedCoreConfigOptions; +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.item.FullInventoryHandler; +import com.bencodez.advancedcore.api.rewards.RewardHandler; +import com.bencodez.advancedcore.api.time.TimeChecker; +import com.bencodez.advancedcore.lifecycle.AdvancedCoreLifecycle.RuntimeExecutors; +import com.bencodez.simpleapi.scheduler.BukkitScheduler; + +public class AdvancedCoreLifecycleTest { + + @Test + public void runtimeExecutorsKeepExistingOwnershipBoundaries() { + BukkitScheduler bukkitScheduler = mock(BukkitScheduler.class); + ScheduledExecutorService timer = mock(ScheduledExecutorService.class); + ScheduledExecutorService loginTimer = mock(ScheduledExecutorService.class); + ScheduledExecutorService inventoryTimer = mock(ScheduledExecutorService.class); + RuntimeExecutors runtime = new RuntimeExecutors(bukkitScheduler, timer, loginTimer, inventoryTimer); + + assertNotSame(runtime.getTimer(), runtime.getLoginTimer()); + assertNotSame(runtime.getTimer(), runtime.getInventoryTimer()); + assertNotSame(runtime.getLoginTimer(), runtime.getInventoryTimer()); + } + + @Test + public void shutdownCoordinatesExecutorsAndInventoryPersistence() throws Exception { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); + ScheduledExecutorService timer = mock(ScheduledExecutorService.class); + ScheduledExecutorService loginTimer = mock(ScheduledExecutorService.class); + ScheduledExecutorService inventoryTimer = mock(ScheduledExecutorService.class); + ScheduledExecutorService timeTimer = mock(ScheduledExecutorService.class); + TimeChecker timeChecker = mock(TimeChecker.class); + RewardHandler rewardHandler = mock(RewardHandler.class); + FullInventoryHandler fullInventoryHandler = mock(FullInventoryHandler.class); + + when(plugin.getLogger()).thenReturn(Logger.getLogger("AdvancedCoreLifecycleTest")); + when(plugin.getOptions()).thenReturn(options); + when(plugin.getTimer()).thenReturn(timer); + when(plugin.getLoginTimer()).thenReturn(loginTimer); + when(plugin.getInventoryTimer()).thenReturn(inventoryTimer); + when(plugin.getTimeChecker()).thenReturn(timeChecker); + when(timeChecker.getTimer()).thenReturn(timeTimer); + when(plugin.getRewardHandler()).thenReturn(rewardHandler); + when(plugin.getFullInventoryHandler()).thenReturn(fullInventoryHandler); + + new AdvancedCoreLifecycle(plugin).shutdown(); + + verify(timer).shutdown(); + verify(loginTimer).shutdown(); + verify(inventoryTimer).shutdown(); + verify(timeTimer).shutdown(); + verify(timer).awaitTermination(2, TimeUnit.SECONDS); + verify(loginTimer).awaitTermination(2, TimeUnit.SECONDS); + verify(inventoryTimer).awaitTermination(1, TimeUnit.SECONDS); + verify(timeTimer).awaitTermination(2, TimeUnit.SECONDS); + verify(rewardHandler).shutdown(); + verify(fullInventoryHandler).shutdown(); + verify(fullInventoryHandler).save(); + verify(plugin).onUnLoad(); + } +}