mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
Modded adapters
This commit is contained in:
@@ -67,6 +67,8 @@ tasks.named('test').configure {
|
||||
systemProperty('iris.pregeneratorJobSource', rootProject.file('core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java').absolutePath)
|
||||
systemProperty('iris.bukkitEnginePlatformHooksSource', file('src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java').absolutePath)
|
||||
systemProperty('iris.engineSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java').absolutePath)
|
||||
systemProperty('iris.jigsawStudioSvcSource', rootProject.file('core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java').absolutePath)
|
||||
systemProperty('iris.studioSvcSource', rootProject.file('core/src/main/java/art/arcane/iris/core/service/StudioSVC.java').absolutePath)
|
||||
systemProperty('iris.terrainSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java').absolutePath)
|
||||
systemProperty('iris.apiEventSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java').absolutePath)
|
||||
systemProperty('iris.worldInfoFactorySource', file('src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java').absolutePath)
|
||||
|
||||
@@ -75,7 +75,6 @@ import art.arcane.iris.core.service.WandSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.EnginePanic;
|
||||
import art.arcane.iris.engine.framework.BlockEditAccess;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.object.IrisCompat;
|
||||
@@ -148,6 +147,8 @@ import java.util.regex.Pattern;
|
||||
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L;
|
||||
private static final long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L;
|
||||
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
|
||||
|
||||
static {
|
||||
@@ -175,8 +176,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
private static final Object TEARDOWN_LOCK = new Object();
|
||||
private final AtomicBoolean alreadyDrained = new AtomicBoolean(false);
|
||||
private final AtomicBoolean postStopFinisherStarted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false);
|
||||
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
|
||||
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
||||
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
||||
private volatile PlaceholderRegistration papiRegistration;
|
||||
private volatile IrisPapiListener papiListener;
|
||||
private volatile IrisPapiState papiState;
|
||||
@@ -184,11 +188,13 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
|
||||
// shutdown-hook thread during teardown; a plain list would CME and abort the teardown.
|
||||
private final List<IrisService> enabledServices = new CopyOnWriteArrayList<>();
|
||||
private final List<PlatformChunkGenerator> deferredShutdownGenerators = new CopyOnWriteArrayList<>();
|
||||
private final IrisWorldGeneratorResolver generatorResolver = new IrisWorldGeneratorResolver(this);
|
||||
private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this);
|
||||
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
||||
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
|
||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||
private volatile Thread serverLifecycleThread;
|
||||
|
||||
public static VolmitSender getSender() {
|
||||
if (sender == null) {
|
||||
@@ -530,8 +536,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return false;
|
||||
}
|
||||
alreadyDrained.set(false);
|
||||
postStopFinisherStarted.set(false);
|
||||
serverStopTeardownDeferred.set(false);
|
||||
servicesDisabled.set(false);
|
||||
sharedRuntimeClosed.set(false);
|
||||
terminalCleanupCompleted.set(false);
|
||||
deferredShutdownGenerators.clear();
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
IrisLanguage.initialize();
|
||||
@@ -669,7 +679,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
public void addShutdownHook() {
|
||||
removeShutdownHook();
|
||||
shutdownHook = new Thread(() -> teardownRuntime("shutdown-hook", 30L), "Iris-ShutdownHook");
|
||||
serverLifecycleThread = Thread.currentThread();
|
||||
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
||||
try {
|
||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||
} catch (IllegalStateException ex) {
|
||||
@@ -756,9 +767,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
public void onDisable() {
|
||||
teardownPapi();
|
||||
teardownRuntime("onDisable", 30L);
|
||||
removeShutdownHook();
|
||||
J.attempt(() -> INMS.get().uninjectBukkit());
|
||||
boolean serverStopping = IrisToolbelt.isServerStopping();
|
||||
if (serverStopping) {
|
||||
quiesceRuntimeForServerShutdown("onDisable");
|
||||
startPostStopFinisher();
|
||||
} else {
|
||||
teardownRuntime("onDisable", 30L);
|
||||
removeShutdownHook();
|
||||
}
|
||||
if (BukkitPlatform.hasHud()) {
|
||||
BukkitPlatform.hudSlots().shutdown();
|
||||
BukkitPlatform.hudLanes().shutdown();
|
||||
@@ -767,15 +783,22 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
configHotloadEngine.clear();
|
||||
configHotloadEngine = null;
|
||||
}
|
||||
runPostShutdown();
|
||||
// super.onDisable() cancels plugin tasks and unregisters every listener.
|
||||
super.onDisable();
|
||||
IrisPlatforms.unbind();
|
||||
if (!serverStopping) {
|
||||
finishTerminalCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
|
||||
teardownPapi();
|
||||
if (IrisToolbelt.isServerStopping()) {
|
||||
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
||||
startPostStopFinisher();
|
||||
Iris.info("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
||||
return;
|
||||
}
|
||||
if (alreadyDrained.get()) {
|
||||
Iris.info("Pre-unload hook skipped; Iris already drained.");
|
||||
return;
|
||||
@@ -829,14 +852,131 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
|
||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||
List<World> irisWorlds = new ArrayList<>();
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
if (IrisToolbelt.access(world) != null) {
|
||||
irisWorlds.add(world);
|
||||
private void quiesceRuntimeForServerShutdown(String reason) {
|
||||
serverStopTeardownDeferred.set(true);
|
||||
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
||||
if (jigsawStudioService != null) {
|
||||
try {
|
||||
jigsawStudioService.quiesceForServerShutdown();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to quiesce Jigsaw Studio before server shutdown.", e);
|
||||
}
|
||||
}
|
||||
if (irisWorlds.isEmpty()) {
|
||||
StudioSVC studioService = IrisServices.getOrNull(StudioSVC.class);
|
||||
if (studioService != null) {
|
||||
studioService.quiesceDownloadsForShutdown();
|
||||
}
|
||||
|
||||
try {
|
||||
PregeneratorJob.shutdownAndWait(SERVER_STOP_PREGEN_TIMEOUT_MILLIS);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to quiesce the Iris pregenerator before server shutdown.", e);
|
||||
}
|
||||
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||
if (generator == null) {
|
||||
continue;
|
||||
}
|
||||
IrisToolbelt.beginWorldMaintenance(world, reason, true);
|
||||
if (!deferredShutdownGenerators.contains(generator)) {
|
||||
deferredShutdownGenerators.add(generator);
|
||||
}
|
||||
generator.quiesceForServerShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private void startPostStopFinisher() {
|
||||
if (!postStopFinisherStarted.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
Thread activeServerThread = serverLifecycleThread;
|
||||
if (activeServerThread == null) {
|
||||
Iris.warn("Iris could not start its post-stop runtime finisher because the server lifecycle thread is unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
Thread finisher = new Thread(() -> {
|
||||
if (!awaitServerThreadTermination(activeServerThread)) {
|
||||
return;
|
||||
}
|
||||
finishDeferredRuntimeTeardown("post-server-stop", 30L);
|
||||
}, "Iris-PostStop-Finisher");
|
||||
finisher.setDaemon(false);
|
||||
finisher.start();
|
||||
}
|
||||
|
||||
static boolean awaitServerThreadTermination(Thread serverThread) {
|
||||
if (serverThread == null || serverThread == Thread.currentThread()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
serverThread.join();
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
Iris.reportError("Iris post-stop runtime finisher was interrupted.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void runShutdownHook() {
|
||||
if (!awaitServerShutdownBoundary()) {
|
||||
Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary.");
|
||||
return;
|
||||
}
|
||||
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
|
||||
}
|
||||
|
||||
private boolean awaitServerShutdownBoundary() {
|
||||
if (!INMS.isBound()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return INMS.get().awaitServerShutdownBoundary(
|
||||
SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to await Paper's post-world-close shutdown boundary.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) {
|
||||
teardownRuntime(reason, timeoutSeconds);
|
||||
finishTerminalCleanup();
|
||||
}
|
||||
|
||||
private void finishTerminalCleanup() {
|
||||
if (!terminalCleanupCompleted.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
J.attempt(() -> INMS.get().uninjectBukkit());
|
||||
try {
|
||||
runPostShutdown();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
||||
} finally {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||
List<World> irisWorlds = new ArrayList<>();
|
||||
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
||||
if (serverStopTeardownDeferred.get()) {
|
||||
generators.addAll(deferredShutdownGenerators);
|
||||
} else {
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||
if (generator != null) {
|
||||
irisWorlds.add(world);
|
||||
generators.add(generator);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (generators.isEmpty()) {
|
||||
Iris.info("No Iris worlds to freeze.");
|
||||
return;
|
||||
}
|
||||
@@ -848,17 +988,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
J.attempt(PregeneratorJob::shutdownInstance);
|
||||
|
||||
List<CompletableFuture<Void>> closes = new ArrayList<>();
|
||||
for (World world : irisWorlds) {
|
||||
PlatformChunkGenerator gen = IrisToolbelt.access(world);
|
||||
if (gen == null) continue;
|
||||
|
||||
Engine engine = gen.getEngine();
|
||||
if (engine != null) {
|
||||
J.attempt(() -> engine.getMantle().saveAllNow());
|
||||
}
|
||||
|
||||
for (PlatformChunkGenerator generator : generators) {
|
||||
try {
|
||||
closes.add(gen.closeAsync());
|
||||
closes.add(generator.closeAsync());
|
||||
} catch (Throwable t) {
|
||||
Iris.reportError(t);
|
||||
}
|
||||
|
||||
+111
-37
@@ -45,6 +45,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -53,6 +54,7 @@ import java.util.function.Supplier;
|
||||
* Bukkit plugin entry points delegate to.
|
||||
*/
|
||||
public final class IrisWorldGeneratorResolver {
|
||||
private static final int VALIDATION_STABILITY_ATTEMPTS = 2;
|
||||
private static final Object SNAPSHOT_VALIDATION_LOCK = new Object();
|
||||
|
||||
private final VolmitPlugin plugin;
|
||||
@@ -67,15 +69,16 @@ public final class IrisWorldGeneratorResolver {
|
||||
PackValidationRegistry.clear();
|
||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
|
||||
String contentFingerprint = "";
|
||||
ServerConfigurator.PackContentSnapshot contentSnapshot =
|
||||
new ServerConfigurator.PackContentSnapshot("", Map.of());
|
||||
String contextFingerprint = "";
|
||||
Optional<List<PackValidationResult>> cached = Optional.empty();
|
||||
try {
|
||||
contentFingerprint = PackValidationCache.contentFingerprint(packsRoot);
|
||||
contentSnapshot = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
||||
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
cached = PackValidationCache.load(
|
||||
cacheFile,
|
||||
contentFingerprint,
|
||||
contentSnapshot.content(),
|
||||
contextFingerprint,
|
||||
packNames);
|
||||
} catch (RuntimeException exception) {
|
||||
@@ -88,33 +91,31 @@ public final class IrisWorldGeneratorResolver {
|
||||
Iris.info("Reused persisted validation for " + results.size()
|
||||
+ " unchanged Iris pack(s); full pack parsing was skipped.");
|
||||
} else {
|
||||
results = new ArrayList<>(packDirs.size());
|
||||
for (File packDir : packDirs) {
|
||||
FreshValidation validation = validateStablePacks(packsRoot, packDirs, contentSnapshot);
|
||||
packDirs = validation.packDirs();
|
||||
contentSnapshot = validation.contentSnapshot();
|
||||
results = validation.results();
|
||||
if (validation.stable()) {
|
||||
try {
|
||||
results.add(PackValidator.validate(packDir));
|
||||
} catch (Throwable exception) {
|
||||
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", exception);
|
||||
String detail = exception.getMessage();
|
||||
if (detail == null || detail.isBlank()) {
|
||||
detail = exception.getClass().getSimpleName();
|
||||
}
|
||||
results.add(new PackValidationResult(
|
||||
packDir.getName(),
|
||||
List.of("Pack validation failed with " + exception.getClass().getSimpleName()
|
||||
+ ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis()));
|
||||
PackValidationCache.save(
|
||||
cacheFile,
|
||||
contentSnapshot.content(),
|
||||
contextFingerprint,
|
||||
results);
|
||||
} catch (IOException exception) {
|
||||
Iris.reportError("Could not persist Iris pack-validation results", exception);
|
||||
}
|
||||
}
|
||||
try {
|
||||
PackValidationCache.save(cacheFile, contentFingerprint, contextFingerprint, results);
|
||||
} catch (IOException exception) {
|
||||
Iris.reportError("Could not persist Iris pack-validation results", exception);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> packFingerprints = contentSnapshot.packContents();
|
||||
for (PackValidationResult result : results) {
|
||||
PackValidationRegistry.publish(result);
|
||||
String packFingerprint = packFingerprints.get(result.getPackName());
|
||||
File packDirectory = PackDirectoryResolver.resolveExisting(packsRoot, result.getPackName());
|
||||
if (packDirectory != null && packFingerprint != null && !packFingerprint.isBlank()) {
|
||||
PackValidationRegistry.publish(packDirectory.toPath(), result, packFingerprint);
|
||||
}
|
||||
if (!result.isLoadable()) {
|
||||
Iris.error("Pack '" + result.getPackName()
|
||||
+ "' FAILED validation - world and Studio creation with this pack will be refused. Reasons:");
|
||||
@@ -134,6 +135,67 @@ public final class IrisWorldGeneratorResolver {
|
||||
IrisStartupValidation.markPacksReady();
|
||||
}
|
||||
|
||||
private static FreshValidation validateStablePacks(
|
||||
File packsRoot,
|
||||
List<File> initialPackDirs,
|
||||
ServerConfigurator.PackContentSnapshot initialSnapshot
|
||||
) {
|
||||
List<File> packDirs = initialPackDirs;
|
||||
ServerConfigurator.PackContentSnapshot before = initialSnapshot;
|
||||
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
|
||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||
List<PackValidationResult> results = validatePacks(packDirs);
|
||||
ServerConfigurator.PackContentSnapshot after;
|
||||
try {
|
||||
after = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
||||
} catch (RuntimeException exception) {
|
||||
Iris.reportError("Could not verify Iris pack bytes after validation", exception);
|
||||
after = new ServerConfigurator.PackContentSnapshot("", Map.of());
|
||||
}
|
||||
List<File> afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
|
||||
if (!before.content().isBlank()
|
||||
&& before.content().equals(after.content())
|
||||
&& packNames.equals(afterPackNames)) {
|
||||
return new FreshValidation(afterPackDirs, after, results, true);
|
||||
}
|
||||
packDirs = afterPackDirs;
|
||||
before = after;
|
||||
}
|
||||
Iris.error("Iris pack files kept changing during validation; validation was refused until writes stop.");
|
||||
List<PackValidationResult> failures = new ArrayList<>(packDirs.size());
|
||||
for (File packDir : packDirs) {
|
||||
failures.add(new PackValidationResult(
|
||||
packDir.getName(),
|
||||
List.of("Pack files changed while validation was in progress; retry after writes stop."),
|
||||
List.of(),
|
||||
System.currentTimeMillis()));
|
||||
}
|
||||
return new FreshValidation(packDirs, before, failures, false);
|
||||
}
|
||||
|
||||
private static List<PackValidationResult> validatePacks(List<File> packDirs) {
|
||||
List<PackValidationResult> results = new ArrayList<>(packDirs.size());
|
||||
for (File packDir : packDirs) {
|
||||
try {
|
||||
results.add(PackValidator.validate(packDir));
|
||||
} catch (Throwable exception) {
|
||||
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", exception);
|
||||
String detail = exception.getMessage();
|
||||
if (detail == null || detail.isBlank()) {
|
||||
detail = exception.getClass().getSimpleName();
|
||||
}
|
||||
results.add(new PackValidationResult(
|
||||
packDir.getName(),
|
||||
List.of("Pack validation failed with " + exception.getClass().getSimpleName()
|
||||
+ ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis()));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static PackValidationResult requireSnapshotLoadable(File packRoot) {
|
||||
Path normalizedRoot = packRoot.toPath().toAbsolutePath().normalize();
|
||||
PackValidationResult result = PackValidationRegistry.get(normalizedRoot);
|
||||
@@ -141,22 +203,26 @@ public final class IrisWorldGeneratorResolver {
|
||||
synchronized (SNAPSHOT_VALIDATION_LOCK) {
|
||||
result = PackValidationRegistry.get(normalizedRoot);
|
||||
if (result == null) {
|
||||
try {
|
||||
result = PackValidator.validate(normalizedRoot.toFile());
|
||||
} catch (Throwable exception) {
|
||||
Iris.reportError("Snapshot pack validation failed for '" + normalizedRoot + "'", exception);
|
||||
String detail = exception.getMessage();
|
||||
if (detail == null || detail.isBlank()) {
|
||||
detail = exception.getClass().getSimpleName();
|
||||
PackValidationRegistry.ValidationTicket ticket =
|
||||
PackValidationRegistry.tryBeginValidation(normalizedRoot);
|
||||
if (ticket != null) {
|
||||
try {
|
||||
result = PackValidator.validate(normalizedRoot.toFile());
|
||||
} catch (Throwable exception) {
|
||||
Iris.reportError("Snapshot pack validation failed for '" + normalizedRoot + "'", exception);
|
||||
String detail = exception.getMessage();
|
||||
if (detail == null || detail.isBlank()) {
|
||||
detail = exception.getClass().getSimpleName();
|
||||
}
|
||||
result = new PackValidationResult(
|
||||
normalizedRoot.getFileName().toString(),
|
||||
List.of("Pack validation failed with " + exception.getClass().getSimpleName()
|
||||
+ ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
result = new PackValidationResult(
|
||||
normalizedRoot.getFileName().toString(),
|
||||
List.of("Pack validation failed with " + exception.getClass().getSimpleName()
|
||||
+ ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis());
|
||||
PackValidationRegistry.publishIfCurrent(ticket, result);
|
||||
}
|
||||
PackValidationRegistry.publish(normalizedRoot, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,4 +331,12 @@ public final class IrisWorldGeneratorResolver {
|
||||
|
||||
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
|
||||
}
|
||||
|
||||
private record FreshValidation(
|
||||
List<File> packDirs,
|
||||
ServerConfigurator.PackContentSnapshot contentSnapshot,
|
||||
List<PackValidationResult> results,
|
||||
boolean stable
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisShutdownOrderingTest {
|
||||
@Test
|
||||
public void drainWorldGenerators_closesGeneratorsWithoutEagerMantleSave() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
String drain = section(source, "private void drainWorldGenerators", "private void setupPapi");
|
||||
|
||||
assertTrue("Shutdown must close each Iris generator", drain.contains("generator.closeAsync()"));
|
||||
assertFalse("Shutdown must not close Mantle plates before generation drains", drain.contains("saveAllNow()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverStop_defersRuntimeTeardownUntilPaperShutdownBoundary() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
String onDisable = section(source, "public void onDisable()", "public void onPreUnload");
|
||||
String quiesce = section(source, "private void quiesceRuntimeForServerShutdown", "private void startPostStopFinisher");
|
||||
String shutdownHook = section(source, "private void runShutdownHook()", "private boolean awaitServerShutdownBoundary");
|
||||
String finisher = section(source, "private void startPostStopFinisher()", "static boolean awaitServerThreadTermination");
|
||||
String serverJoin = section(source, "static boolean awaitServerThreadTermination", "private void runShutdownHook");
|
||||
String generatorSource = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource")));
|
||||
String generatorQuiesce = section(generatorSource,
|
||||
"public void quiesceForServerShutdown()", "public boolean isStudio()");
|
||||
|
||||
assertOrdered(onDisable,
|
||||
"if (serverStopping)",
|
||||
"quiesceRuntimeForServerShutdown(\"onDisable\")",
|
||||
"startPostStopFinisher()",
|
||||
"else",
|
||||
"teardownRuntime(\"onDisable\", 30L)");
|
||||
assertOrdered(shutdownHook,
|
||||
"awaitServerShutdownBoundary()",
|
||||
"finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)");
|
||||
assertOrdered(finisher,
|
||||
"finisher.setDaemon(false)",
|
||||
"finisher.start()");
|
||||
assertOrdered(finisher,
|
||||
"awaitServerThreadTermination(activeServerThread)",
|
||||
"finishDeferredRuntimeTeardown(\"post-server-stop\", 30L)");
|
||||
assertOrdered(serverJoin,
|
||||
"serverThread == Thread.currentThread()",
|
||||
"serverThread.join()");
|
||||
assertTrue("Server-stop quiescence must leave queued Paper generation admitted",
|
||||
quiesce.contains("generator.quiesceForServerShutdown()"));
|
||||
assertOrdered(quiesce,
|
||||
"jigsawStudioService.quiesceForServerShutdown()",
|
||||
"PregeneratorJob.shutdownAndWait",
|
||||
"generator.quiesceForServerShutdown()");
|
||||
assertOrdered(onDisable,
|
||||
"quiesceRuntimeForServerShutdown(\"onDisable\")",
|
||||
"super.onDisable()");
|
||||
assertFalse("Server-stop quiescence must not begin generator close before Paper's boundary",
|
||||
generatorQuiesce.contains("closing = true"));
|
||||
assertFalse("Server-stop quiescence must not dispatch generator close before Paper's boundary",
|
||||
generatorQuiesce.contains("closeAsync()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deferredTeardown_keepsGeneratorClosingServicesBehindGeneratorDrain() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
String engineService = Files.readString(Path.of(System.getProperty("iris.engineSvcSource")));
|
||||
String studioService = Files.readString(Path.of(System.getProperty("iris.studioSvcSource")));
|
||||
String generatorSource = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource")));
|
||||
String teardown = section(source, "private void teardownRuntime", "private void quiesceRuntimeForServerShutdown");
|
||||
String engineDisable = section(engineService, "public void onDisable()", "public void engineStatus");
|
||||
String studioDisable = section(studioService, "public void onDisable()", "public IrisDimension installIntoWorld");
|
||||
String generatorClose = section(generatorSource,
|
||||
"public void close()", "public CompletableFuture<Void> closeAsync()");
|
||||
String generatorCloseAsync = section(generatorSource,
|
||||
"public CompletableFuture<Void> closeAsync()", "public void quiesceForServerShutdown()");
|
||||
|
||||
assertOrdered(teardown,
|
||||
"drainWorldGenerators(reason, timeoutSeconds)",
|
||||
"service.onDisable()",
|
||||
"MultiBurst.burst::close",
|
||||
"MultiBurst.ioBurst::close");
|
||||
assertTrue("Engine service must retain its generator close behind deferred service teardown",
|
||||
engineDisable.contains("startClose("));
|
||||
assertTrue("Studio service must retain its generator close behind deferred service teardown",
|
||||
studioDisable.contains("generator.close()"));
|
||||
assertTrue("Service-level generator close must delegate to the shared idempotent close future",
|
||||
generatorClose.contains("closeAsync()"));
|
||||
assertTrue("Repeated post-boundary closes must return the already-published close future",
|
||||
generatorCloseAsync.contains("return existing;"));
|
||||
assertFalse("A completed generator close must never be re-dispatched",
|
||||
generatorCloseAsync.contains("!existing.isDone()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preUnload_doesNotDrainGeneratorsDuringServerStop() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
String preUnload = section(source, "public void onPreUnload", "private void drainOnce");
|
||||
|
||||
assertOrdered(preUnload,
|
||||
"IrisToolbelt.isServerStopping()",
|
||||
"quiesceRuntimeForServerShutdown",
|
||||
"startPostStopFinisher()",
|
||||
"return;",
|
||||
"drainOnce(");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jigsawStudio_quiescesOnceBeforeDeferredServiceTeardown() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.jigsawStudioSvcSource")));
|
||||
String enable = section(source, "public void onEnable()", "public void onDisable()");
|
||||
String disable = section(source, "public void onDisable()", "public void quiesceForServerShutdown()");
|
||||
String quiesce = section(source, "public void quiesceForServerShutdown()", "public void register(");
|
||||
String register = section(source, "public void register(", "public void activationCommitted(");
|
||||
String activation = section(source, "public void activationCommitted(", "public void markChunkGenerated(");
|
||||
String chunkGenerated = section(source, "public void markChunkGenerated(", "private void markChunkAvailable(");
|
||||
|
||||
assertOrdered(enable, "disableStarted.set(false)", "enabled = true");
|
||||
assertTrue("Deferred service teardown must reuse the idempotent early disable",
|
||||
disable.contains("quiesceForServerShutdown();"));
|
||||
assertOrdered(quiesce,
|
||||
"disableStarted.compareAndSet(false, true)",
|
||||
"finalizeAllJigsawTileWatches()",
|
||||
"drainAutosavesBeforeDisable()",
|
||||
"enabled = false",
|
||||
"activeMenuController.closeAll()",
|
||||
"previewRenderer.removeAll()",
|
||||
"studios.clear()");
|
||||
assertTrue("Registration must reject work after shutdown begins",
|
||||
occurrences(register, "!enabled || disableStarted.get()") >= 2);
|
||||
assertTrue("Activation must reject work after shutdown begins",
|
||||
activation.contains("!enabled || disableStarted.get()"));
|
||||
assertTrue("Chunk-generation callbacks must reject work after shutdown begins",
|
||||
occurrences(chunkGenerated, "!enabled || disableStarted.get()") >= 2);
|
||||
}
|
||||
|
||||
private static String section(String source, String startMarker, String endMarker) {
|
||||
int start = source.indexOf(startMarker);
|
||||
int end = source.indexOf(endMarker, start);
|
||||
assertTrue("Missing source section starting with " + startMarker, start >= 0);
|
||||
assertTrue("Missing source section ending with " + endMarker, end > start);
|
||||
return source.substring(start, end);
|
||||
}
|
||||
|
||||
private static void assertOrdered(String source, String... markers) {
|
||||
int previous = -1;
|
||||
for (String marker : markers) {
|
||||
int current = source.indexOf(marker);
|
||||
assertTrue("Missing source marker " + marker, current >= 0);
|
||||
assertTrue("Source marker is out of order: " + marker, current > previous);
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
private static int occurrences(String source, String marker) {
|
||||
int count = 0;
|
||||
int offset = 0;
|
||||
while ((offset = source.indexOf(marker, offset)) >= 0) {
|
||||
count++;
|
||||
offset += marker.length();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
+17
@@ -64,6 +64,23 @@ public class IrisWorldGeneratorResolverTest {
|
||||
assertFalse(invalid.getBlockingErrors().toString(), invalid.isLoadable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupValidationPublishesFingerprintBoundExactRootResults() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java"));
|
||||
int validateAll = source.indexOf("public void validateAllPacks()");
|
||||
int snapshot = source.indexOf("ServerConfigurator.computePackContentSnapshot(packsRoot)", validateAll);
|
||||
int perPackFingerprint = source.indexOf("contentSnapshot.packContents()", snapshot);
|
||||
int exactRootPublish = source.indexOf(
|
||||
"PackValidationRegistry.publish(packDirectory.toPath(), result, packFingerprint)",
|
||||
perPackFingerprint);
|
||||
|
||||
assertTrue(validateAll >= 0);
|
||||
assertTrue(snapshot > validateAll);
|
||||
assertTrue(perPackFingerprint > snapshot);
|
||||
assertTrue(exactRootPublish > perPackFingerprint);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void paperStartupAliasResolvesToCanonicalRuntimeKey() {
|
||||
assertEquals(
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ public class BukkitEngineLifecycleContractTest {
|
||||
|
||||
assertBefore(closeAsync, "closeFuture.compareAndSet(null, future)", "withExclusiveControlFuture(");
|
||||
assertTrue(closeAsync.contains("while (!closeFuture.compareAndSet(null, future))"));
|
||||
assertTrue(closeAsync.contains("return existing;"));
|
||||
assertTrue(closeAsync.contains("operation.whenComplete("));
|
||||
assertFalse(closeAsync.contains("!existing.isDone()"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user