Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ outcome that cannot occur.
- **Module reload = config refresh, not a jar swap.** `ModuleLifecycleManager.reloadAsync` calls the
module's `PluginModule#reload`; it does **not** unload or replace the jar. Changed module code is
only picked up by restarting the host plugin, and only manifests with `reloadable = true` refresh.
- **A failed `initialize` is never followed by `shutdown`.** The manager only logs and closes the
class loader, so a module must clean up its own partial registrations before throwing (documented
on `PluginModule#initialize`). `SimplePluginModule` tracks listeners but unregisters nothing by
itself. Changing this is a contract change for every module, not a bug fix.
- **`ModuleLifecycleManager` is not thread-safe.** Its state must be touched from one thread (the
server main thread on Paper).
- **`SchemaMigrator` is abstract and dialect-agnostic.** It provides no SQL of its own — subclasses
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ by a `ModuleManifest` and loaded at runtime by its own `URLClassLoader`.
- `ModuleLifecycleManager<T>` — owns the runtime lifecycle: `start()`, `stop()`,
`reloadAsync(moduleName)`, `getActiveModules()`.

> **Failed start:** if `initialize` throws, the manager closes the module's class loader but does
> **not** call `shutdown`. A module must do its fallible work before registering anything, or undo
> its registrations before rethrowing; otherwise its listeners and tasks stay live. `shutdown` is only
> called for modules that started.

> **Reload semantics:** a module reload is a **config refresh** (`PluginModule#reload`), not a jar
> unload/reswap. Changed module code is only picked up by restarting the host plugin. Only modules
> whose manifest sets `reloadable = true` are refreshed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,34 @@

public interface PluginModule<T extends Plugin> {

/**
* Starts this module: reads its configuration and registers its listeners, tasks and commands
* with the host.
*
* <p><strong>Failure is the module's to clean up.</strong> If this method throws, the
* {@link ModuleLifecycleManager} logs the failure and closes the module's class loader, but it
* does <em>not</em> call {@link #shutdown}. Anything registered before the failure stays live:
* a listener keeps firing with classes from a closed class loader, a task keeps running. An
* implementation must therefore either do all fallible work (reading config, resolving
* services) before registering anything, or undo what it registered before rethrowing.
*
* @param plugin the host plugin
* @param dataFolder this module's own data folder
* @throws ModuleInitializationException if the module cannot start; it must have released
* everything it registered before throwing
*/
void initialize(@NotNull T plugin, @NotNull Path dataFolder) throws ModuleInitializationException;

/**
* Stops this module and releases everything {@link #initialize} registered.
*
* <p>Called once, by {@link ModuleLifecycleManager#stop()}, and only for a module whose
* {@code initialize} returned normally. It is never called after a failed {@code initialize},
* so it may assume a fully started module.
*
* @param plugin the host plugin
* @throws ModuleShutdownException if releasing fails; the class loader is closed regardless
*/
void shutdown(@NotNull T plugin) throws ModuleShutdownException;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
import java.util.ArrayList;
import java.util.Collection;

/**
* A {@link PluginModule} base that keeps the host plugin and data folder, and tracks listeners
* registered through {@link #registerListener}.
*
* <p>Tracking is not cleanup: neither this class's {@link #initialize} nor its {@link #shutdown}
* unregisters anything. Following {@link PluginModule#initialize}'s contract, a subclass whose
* {@code initialize} fails after {@code registerListener} must call {@link #unregisterListeners()}
* before rethrowing, and its {@code shutdown} must call {@code unregisterListeners()} before
* {@code super.shutdown(plugin)}.
*
* @param <T> the host plugin type
*/
public abstract class SimplePluginModule<T extends Plugin> implements PluginModule<T> {

private final Collection<Listener> listeners = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,22 @@ void initializationFailureUnloads() throws Exception {
assertTrue(logger.hasRecord(Level.SEVERE, "Failed to initialize module bad"));
}

@Test
@DisplayName("a failed initialize is not followed by shutdown, neither at once nor on stop()")
void failedInitializeIsNeverShutDown() throws Exception {
// Pins the PluginModule contract: cleaning up after a failed initialize is the module's job.
List<String> shutdowns = new ArrayList<>();
FakeModule bad = new FakeModule("bad", shutdowns);
bad.initFailure = new RuntimeException("boom");
stubLoader(List.of(loaded("bad", false, bad)));

ModuleLifecycleManager<Plugin> manager = manager();
manager.start();
manager.stop();

assertTrue(shutdowns.isEmpty());
}

@Test
@DisplayName("stop() shuts modules down in reverse initialization order and clears state")
void stopShutsDownInReverseOrder() throws Exception {
Expand Down
Loading