From 50eb4bec1a0e0c42c757d428fa3ce778af3e7cb2 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 7 Sep 2026 20:38:37 -0600 Subject: [PATCH] Extract executor lifecycle into shared runtime with Bukkit adapter --- .../bukkit/runtime/BukkitRuntimePlatform.java | 84 +++++++ .../core/platform/RuntimePlatform.java | 25 ++ .../core/runtime/AdvancedCoreRuntime.java | 91 ++++++++ .../lifecycle/AdvancedCoreLifecycle.java | 213 ++++-------------- .../tests/lifecycle/CoreRuntimeTest.java | 110 +++++++++ .../lifecycle/RuntimeHeadlessFixture.java | 46 ++++ .../tests/lifecycle/RuntimeHeadlessTest.java | 24 ++ docs/shared-runtime-foundation.md | 49 ++++ 8 files changed, 478 insertions(+), 164 deletions(-) create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/runtime/BukkitRuntimePlatform.java create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/core/platform/RuntimePlatform.java create mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/core/runtime/AdvancedCoreRuntime.java create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/CoreRuntimeTest.java create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/RuntimeHeadlessFixture.java create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/RuntimeHeadlessTest.java create mode 100644 docs/shared-runtime-foundation.md diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/runtime/BukkitRuntimePlatform.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/runtime/BukkitRuntimePlatform.java new file mode 100644 index 0000000000..5d54765d94 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/runtime/BukkitRuntimePlatform.java @@ -0,0 +1,84 @@ +package com.bencodez.advancedcore.bukkit.runtime; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; + +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.advancedcore.core.platform.RuntimePlatform; + +/** Bukkit services for the shared executor lifecycle; no duplicate owners are created. */ +public final class BukkitRuntimePlatform implements RuntimePlatform { + private final AdvancedCorePlugin plugin; + + public BukkitRuntimePlatform(AdvancedCorePlugin plugin) { + this.plugin = Objects.requireNonNull(plugin, "plugin"); + } + + @Override public ScheduledExecutorService getTimer() { return plugin.getTimer(); } + @Override public ScheduledExecutorService getLoginTimer() { return plugin.getLoginTimer(); } + @Override public ScheduledExecutorService getInventoryTimer() { return plugin.getInventoryTimer(); } + @Override public ScheduledExecutorService getTimeTimer() { + TimeChecker checker = plugin.getTimeChecker(); + return checker == null ? null : checker.getTimer(); + } + + @Override public List beforeExecutorShutdown() { + return List.of( + new Cleanup("Javascript engine", () -> { + if (plugin.getOptions() != null && plugin.getOptions().isJavascriptEngineEnabled()) { + plugin.getLogger().info("Shutting down Javascript engine"); + JavascriptEngineHandler.getInstance().clearCachedEngine(); + } + }), + new Cleanup("MySQL", () -> { + if (plugin.isLoadUserData() && plugin.getOptions() != null + && UserStorage.MYSQL.equals(plugin.getOptions().getStorageType()) && plugin.getMysql() != null) { + plugin.getMysql().close(); + } + }), + new Cleanup("server data timestamp", () -> { + if (plugin.getServerDataFile() != null) plugin.getServerDataFile().setLastUpdated(); + })); + } + + @Override public List afterExecutorGrace() { + return List.of(new Cleanup("reward handler", () -> { + if (plugin.getRewardHandler() != null) plugin.getRewardHandler().shutdown(); + })); + } + + @Override public List afterExecutorShutdown() { + return List.of( + new Cleanup("plugin unload hook", plugin::onUnLoad), + new Cleanup("skull cache", () -> { + if (plugin.getSkullCacheHandler() != null) plugin.getSkullCacheHandler().close(); + }), + new Cleanup("full inventory handler", () -> { + FullInventoryHandler handler = plugin.getFullInventoryHandler(); + if (handler != null) { + handler.shutdown(); + handler.save(); + } + }), + new Cleanup("hologram handler", () -> { + if (plugin.getHologramHandler() != null) plugin.getHologramHandler().onShutDown(); + }), + new Cleanup("permission handler", () -> { + if (plugin.getPermissionHandler() != null) plugin.getPermissionHandler().shutDown(); + }), + new Cleanup("dialog service", () -> { + if (plugin.getDialogService() != null) plugin.getDialogService().unregister(); + })); + } + + @Override public void info(String message) { plugin.getLogger().info(message); } + @Override public void cleanupFailed(String component, Throwable failure) { + plugin.getLogger().warning("Failed to shut down " + component + ": " + failure.getMessage()); + plugin.debug(failure); + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/platform/RuntimePlatform.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/platform/RuntimePlatform.java new file mode 100644 index 0000000000..42f37f98a3 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/platform/RuntimePlatform.java @@ -0,0 +1,25 @@ +package com.bencodez.advancedcore.core.platform; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; + +/** Supplies existing executor owners and platform cleanup without exposing game APIs. */ +public interface RuntimePlatform { + record Cleanup(String name, Runnable action) { + public Cleanup { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(action, "action"); + } + } + + ScheduledExecutorService getTimer(); + ScheduledExecutorService getLoginTimer(); + ScheduledExecutorService getInventoryTimer(); + ScheduledExecutorService getTimeTimer(); + List beforeExecutorShutdown(); + List afterExecutorGrace(); + List afterExecutorShutdown(); + void info(String message); + void cleanupFailed(String component, Throwable failure); +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/runtime/AdvancedCoreRuntime.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/runtime/AdvancedCoreRuntime.java new file mode 100644 index 0000000000..6eba089cec --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/runtime/AdvancedCoreRuntime.java @@ -0,0 +1,91 @@ +package com.bencodez.advancedcore.core.runtime; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import com.bencodez.advancedcore.core.platform.RuntimePlatform; +import com.bencodez.advancedcore.core.platform.RuntimePlatform.Cleanup; + +/** + * First shared runtime slice: executor creation and existing shutdown sequencing. + * It creates no second cache/service registry, and does not replace an entity or + * region scheduler with a global executor. The platform serializes lifecycle + * calls; cleanup hooks are synchronous and retain their existing failure policy. + */ +public final class AdvancedCoreRuntime { + private final RuntimePlatform platform; + + public AdvancedCoreRuntime(RuntimePlatform platform) { + this.platform = Objects.requireNonNull(platform, "platform"); + } + + /** The same three independent executor owners used by the Bukkit plugin. */ + public record ExecutorGroup(ScheduledExecutorService timer, ScheduledExecutorService loginTimer, + ScheduledExecutorService inventoryTimer) { } + + public static ExecutorGroup createExecutors() { + ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor(); + ScheduledExecutorService loginTimer = null; + try { + loginTimer = Executors.newSingleThreadScheduledExecutor(); + return new ExecutorGroup(timer, loginTimer, Executors.newSingleThreadScheduledExecutor()); + } catch (RuntimeException | Error failure) { + timer.shutdownNow(); + if (loginTimer != null) loginTimer.shutdownNow(); + throw failure; + } + } + + public void shutdown() { + clean(platform.beforeExecutorShutdown()); + + // Resolve the time-checker timer once, after the pre-shutdown actions, + // just as the old lifecycle did. Other getters retain their lookup order. + ScheduledExecutorService timeTimer = platform.getTimeTimer(); + shutdown(platform.getLoginTimer()); + shutdown(platform.getTimer()); + shutdown(timeTimer); + shutdown(platform.getInventoryTimer()); + + platform.info("Allowing background tasks to finish before shutdown"); + await(platform.getLoginTimer(), 2, TimeUnit.SECONDS); + await(platform.getTimer(), 2, TimeUnit.SECONDS); + await(timeTimer, 2, TimeUnit.SECONDS); + await(platform.getInventoryTimer(), 1, TimeUnit.SECONDS); + + clean(platform.afterExecutorGrace()); + shutdownNow(platform.getLoginTimer()); + shutdownNow(platform.getTimer()); + shutdownNow(timeTimer); + shutdownNow(platform.getInventoryTimer()); + clean(platform.afterExecutorShutdown()); + } + + private void clean(List actions) { + for (Cleanup action : actions) { + try { + action.action().run(); + } catch (Throwable failure) { + platform.cleanupFailed(action.name(), failure); + } + } + } + + public static void shutdown(ScheduledExecutorService executor) { + if (executor != null && !executor.isShutdown()) executor.shutdown(); + } + public static void shutdownNow(ScheduledExecutorService executor) { + if (executor != null && !executor.isTerminated()) executor.shutdownNow(); + } + public static void await(ScheduledExecutorService executor, long timeout, TimeUnit unit) { + if (executor == null) return; + try { + executor.awaitTermination(timeout, unit); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/lifecycle/AdvancedCoreLifecycle.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/lifecycle/AdvancedCoreLifecycle.java index 82fdebafb1..fd113b27cd 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/lifecycle/AdvancedCoreLifecycle.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/lifecycle/AdvancedCoreLifecycle.java @@ -1,177 +1,62 @@ 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.advancedcore.bukkit.runtime.BukkitRuntimePlatform; +import com.bencodez.advancedcore.core.runtime.AdvancedCoreRuntime; 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. + * Existing Bukkit lifecycle facade. Public signatures and executor identity are + * retained; the shared runtime owns the same sequencing through a Bukkit adapter. */ 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; - - public 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; - } - } + private final AdvancedCorePlugin plugin; + + public AdvancedCoreLifecycle(AdvancedCorePlugin plugin) { + this.plugin = plugin; + } + + public static RuntimeExecutors createRuntimeExecutors(AdvancedCorePlugin plugin) { + BukkitScheduler scheduler = new BukkitScheduler(plugin); + AdvancedCoreRuntime.ExecutorGroup executors = AdvancedCoreRuntime.createExecutors(); + return new RuntimeExecutors(scheduler, executors.timer(), executors.loginTimer(), executors.inventoryTimer()); + } + + public void shutdown() { + if (plugin != null) new AdvancedCoreRuntime(new BukkitRuntimePlatform(plugin)).shutdown(); + } + + static void shutdown(ScheduledExecutorService executor) { + AdvancedCoreRuntime.shutdown(executor); + } + static void shutdownNow(ScheduledExecutorService executor) { + AdvancedCoreRuntime.shutdownNow(executor); + } + static void await(ScheduledExecutorService executor, long timeout, TimeUnit unit) { + AdvancedCoreRuntime.await(executor, timeout, unit); + } + + public static final class RuntimeExecutors { + private final BukkitScheduler bukkitScheduler; + private final ScheduledExecutorService timer; + private final ScheduledExecutorService loginTimer; + private final ScheduledExecutorService inventoryTimer; + + public 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/CoreRuntimeTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/CoreRuntimeTest.java new file mode 100644 index 0000000000..cdd00efd6f --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/CoreRuntimeTest.java @@ -0,0 +1,110 @@ +package com.bencodez.advancedcore.tests.lifecycle; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.bukkit.runtime.BukkitRuntimePlatform; +import com.bencodez.advancedcore.core.platform.RuntimePlatform; +import com.bencodez.advancedcore.core.platform.RuntimePlatform.Cleanup; +import com.bencodez.advancedcore.core.runtime.AdvancedCoreRuntime; +import com.bencodez.advancedcore.lifecycle.AdvancedCoreLifecycle; + +class CoreRuntimeTest { + private RuntimePlatform platform() { + RuntimePlatform platform = mock(RuntimePlatform.class); + when(platform.beforeExecutorShutdown()).thenReturn(List.of()); + when(platform.afterExecutorGrace()).thenReturn(List.of()); + when(platform.afterExecutorShutdown()).thenReturn(List.of()); + return platform; + } + + @Test void constructionDoesNotStartOrReplaceExistingServices() { + RuntimePlatform platform = platform(); + clearInvocations(platform); + new AdvancedCoreRuntime(platform); + verifyNoInteractions(platform); + } + + @Test void preservesExecutorShutdownGraceAndRewardOrdering() throws Exception { + RuntimePlatform platform = platform(); + var events = new ArrayList(); + ScheduledExecutorService login = executor("login", events); + ScheduledExecutorService timer = executor("timer", events); + ScheduledExecutorService time = executor("time", events); + ScheduledExecutorService inventory = executor("inventory", events); + when(platform.getLoginTimer()).thenReturn(login); + when(platform.getTimer()).thenReturn(timer); + when(platform.getTimeTimer()).thenReturn(time); + when(platform.getInventoryTimer()).thenReturn(inventory); + when(platform.beforeExecutorShutdown()).thenReturn(List.of(new Cleanup("pre", () -> events.add("pre")))); + when(platform.afterExecutorGrace()).thenReturn(List.of(new Cleanup("rewards", () -> events.add("rewards")))); + when(platform.afterExecutorShutdown()).thenReturn(List.of(new Cleanup("post", () -> events.add("post")))); + doAnswer(call -> { events.add("wait-log"); return null; }).when(platform).info(anyString()); + new AdvancedCoreRuntime(platform).shutdown(); + assertEquals(List.of("pre", "login-stop", "timer-stop", "time-stop", "inventory-stop", "wait-log", + "login-wait", "timer-wait", "time-wait", "inventory-wait", "rewards", + "login-force", "timer-force", "time-force", "inventory-force", "post"), events); + verify(login).awaitTermination(2, TimeUnit.SECONDS); + verify(timer).awaitTermination(2, TimeUnit.SECONDS); + verify(time).awaitTermination(2, TimeUnit.SECONDS); + verify(inventory).awaitTermination(1, TimeUnit.SECONDS); + verify(platform, times(1)).getTimeTimer(); + } + + private ScheduledExecutorService executor(String name, List events) throws Exception { + ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + doAnswer(call -> { events.add(name + "-stop"); return null; }).when(executor).shutdown(); + when(executor.awaitTermination(anyLong(), any())).thenAnswer(call -> { events.add(name + "-wait"); return false; }); + when(executor.shutdownNow()).thenAnswer(call -> { events.add(name + "-force"); return List.of(); }); + return executor; + } + + @Test void cleanupFailureIsReportedWithoutSkippingLaterComponents() { + RuntimePlatform platform = platform(); + var events = new ArrayList(); + var failure = new IllegalStateException("fixture"); + when(platform.beforeExecutorShutdown()).thenReturn(List.of( + new Cleanup("failed", () -> { throw failure; }), + new Cleanup("next", () -> events.add("next")))); + when(platform.afterExecutorShutdown()).thenReturn(List.of(new Cleanup("last", () -> events.add("last")))); + new AdvancedCoreRuntime(platform).shutdown(); + assertEquals(List.of("next", "last"), events); + verify(platform).cleanupFailed("failed", failure); + } + + @Test void preservesInterruptAndSkipsAlreadyFinishedExecutors() throws Exception { + ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + when(executor.awaitTermination(2, TimeUnit.SECONDS)).thenThrow(new InterruptedException("fixture")); + try { + AdvancedCoreRuntime.await(executor, 2, TimeUnit.SECONDS); + assertTrue(Thread.currentThread().isInterrupted()); + } finally { Thread.interrupted(); } + when(executor.isShutdown()).thenReturn(true); + when(executor.isTerminated()).thenReturn(true); + AdvancedCoreRuntime.shutdown(executor); + AdvancedCoreRuntime.shutdownNow(executor); + verify(executor, never()).shutdown(); + verify(executor, never()).shutdownNow(); + assertDoesNotThrow(() -> AdvancedCoreRuntime.shutdown(null)); + assertDoesNotThrow(() -> AdvancedCoreRuntime.shutdownNow(null)); + assertDoesNotThrow(() -> AdvancedCoreRuntime.await(null, 1, TimeUnit.SECONDS)); + } + + @Test void bukkitAdapterRetainsExecutorIdentityAndLegacyNullShutdown() { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + ScheduledExecutorService timer = mock(ScheduledExecutorService.class); + when(plugin.getTimer()).thenReturn(timer); + var platform = new BukkitRuntimePlatform(plugin); + assertSame(timer, platform.getTimer()); + assertNull(platform.getTimeTimer()); + assertDoesNotThrow(() -> new AdvancedCoreLifecycle(null).shutdown()); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/RuntimeHeadlessFixture.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/RuntimeHeadlessFixture.java new file mode 100644 index 0000000000..6c435c6975 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/RuntimeHeadlessFixture.java @@ -0,0 +1,46 @@ +package com.bencodez.advancedcore.tests.lifecycle; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import com.bencodez.advancedcore.core.platform.RuntimePlatform; +import com.bencodez.advancedcore.core.runtime.AdvancedCoreRuntime; + +/** Runs with only project classes and the JDK, not Bukkit or the testing libraries. */ +public final class RuntimeHeadlessFixture { + private RuntimeHeadlessFixture() { } + + public static void run() throws Exception { + var group = AdvancedCoreRuntime.createExecutors(); + var steps = new ArrayList(); + try { + if (group.timer() == group.loginTimer() || group.loginTimer() == group.inventoryTimer()) { + throw new AssertionError("Executor ownership was combined"); + } + if (group.timer().submit(() -> 7).get(2, TimeUnit.SECONDS) != 7) throw new AssertionError("Task failed"); + var runtime = new AdvancedCoreRuntime(new RuntimePlatform() { + public ScheduledExecutorService getTimer() { return group.timer(); } + public ScheduledExecutorService getLoginTimer() { return group.loginTimer(); } + public ScheduledExecutorService getInventoryTimer() { return group.inventoryTimer(); } + public ScheduledExecutorService getTimeTimer() { return null; } + public List beforeExecutorShutdown() { return List.of(new Cleanup("before", () -> steps.add("before"))); } + public List afterExecutorGrace() { return List.of(new Cleanup("reward", () -> steps.add("reward"))); } + public List afterExecutorShutdown() { return List.of(new Cleanup("after", () -> steps.add("after"))); } + public void info(String message) { steps.add("wait"); } + public void cleanupFailed(String component, Throwable failure) { throw new AssertionError(component, failure); } + }); + runtime.shutdown(); + if (!steps.equals(List.of("before", "wait", "reward", "after"))) throw new AssertionError(steps); + try { group.timer().execute(() -> { }); throw new AssertionError("Accepted after shutdown"); } + catch (RejectedExecutionException expected) { } + if (!group.timer().isTerminated() || !group.loginTimer().isTerminated() || !group.inventoryTimer().isTerminated()) { + throw new AssertionError("Executor did not terminate"); + } + } finally { + group.timer().shutdownNow(); group.loginTimer().shutdownNow(); group.inventoryTimer().shutdownNow(); + } + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/RuntimeHeadlessTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/RuntimeHeadlessTest.java new file mode 100644 index 0000000000..35a1addd6f --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/lifecycle/RuntimeHeadlessTest.java @@ -0,0 +1,24 @@ +package com.bencodez.advancedcore.tests.lifecycle; + +import static org.junit.jupiter.api.Assertions.*; + +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.Test; + +import com.bencodez.advancedcore.core.runtime.AdvancedCoreRuntime; + +class RuntimeHeadlessTest { + @Test void runtimeExecutesAndStopsWithNoServerApiOnItsClasspath() throws Exception { + URL main = AdvancedCoreRuntime.class.getProtectionDomain().getCodeSource().getLocation(); + URL tests = getClass().getProtectionDomain().getCodeSource().getLocation(); + try (var isolated = new URLClassLoader(new URL[] { main, tests }, ClassLoader.getPlatformClassLoader())) { + assertThrows(ClassNotFoundException.class, () -> isolated.loadClass("org.bukkit.Bukkit")); + assertThrows(ClassNotFoundException.class, () -> isolated.loadClass("org.junit.jupiter.api.Test")); + Class fixture = isolated.loadClass(RuntimeHeadlessFixture.class.getName()); + assertSame(isolated, fixture.getClassLoader()); + fixture.getMethod("run").invoke(null); + } + } +} diff --git a/docs/shared-runtime-foundation.md b/docs/shared-runtime-foundation.md new file mode 100644 index 0000000000..9db6cbb6cc --- /dev/null +++ b/docs/shared-runtime-foundation.md @@ -0,0 +1,49 @@ +# Shared runtime foundation + +AdvancedCore remains one Maven project with one existing POM. New shared runtime +mechanics live under `core/runtime` and their platform contract under +`core/platform`; Bukkit cleanup hooks live under `bukkit/runtime`. No modules, +workflow files, dependency versions, publishing changes or game-loader stubs are +introduced. + +The existing `AdvancedCoreLifecycle` facade now delegates its executor creation +and shutdown sequence to `core.runtime.AdvancedCoreRuntime` through +`BukkitRuntimePlatform`. AdvancedCorePlugin's current enable/disable calls already +use that facade, so Bukkit uses the extracted code immediately. There is no +parallel runtime with different caches or a second set of background tasks. + +## Preserved contracts + +- The facade constructor, nested RuntimeExecutors constructor and getters retain + their signatures and return the same executor/scheduler objects. +- General, login and inventory work retain separate single-thread executors. +- BukkitScheduler/Folia behavior is unchanged; game/entity/region scheduling is + not redirected to these background executors. +- JavaScript cleanup, MySQL closing and server timestamp happen before executor + shutdown, as before. This extraction does not redesign database flushing. +- Executor grace periods remain 2/2/2/1 seconds, followed by reward shutdown, + forced executor shutdown and the existing unload/cache/inventory/integration + cleanup order. The time-checker timer is captured at the same point. +- Individual component failures retain the warning/debug callback behavior and + do not skip later components. Interrupted waits restore the interrupt flag. +- Lifecycle calls remain serialized by the owning platform. Hook execution is + synchronous; the executor grace periods are not a global bound on all cleanup + callbacks. No stronger transactional or idempotency guarantee is claimed. + +## What this does not port yet + +This is the executor/lifecycle slice of the future shared runtime, not a complete +Bukkit-free AdvancedCore service graph. Users, settings, storage, rewards, game +operations and integration providers are extracted in subsequent changes. Native +packaging must only include dependency-clean classes once that graph is ready; +this PR does not tell a Fabric server to load the full existing AdvancedCore JAR. + +## Validation + +Use the existing `mvn -B -f AdvancedCore/pom.xml package` command. Existing Bukkit +lifecycle tests remain in place. New tests compare the cleanup/executor ordering, +timeouts, failure handling, object identity and interrupt behavior. A headless +fixture creates real executor owners, runs a task, shuts down and checks rejection +of new work while Bukkit/JUnit are absent from its isolated classpath. + +No live-server or downstream VotingPlugin validation is implied by those tests.