mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
Fix Iris replacement
This commit is contained in:
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.BlockDataMergeSupport;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.modded.command.IrisModdedCommands;
|
||||
import art.arcane.iris.modded.command.ModdedGuiHost;
|
||||
import art.arcane.iris.modded.command.ModdedObjectUndo;
|
||||
import art.arcane.iris.modded.command.ModdedPregenBossBar;
|
||||
@@ -109,6 +110,7 @@ public final class ModdedEngineBootstrap {
|
||||
if (scheduler != null) {
|
||||
scheduler.reset();
|
||||
}
|
||||
IrisModdedCommands.openDownloadAdmission();
|
||||
ModdedStartup.prepareForStartup();
|
||||
IrisModdedChunkGenerator.startGenPool();
|
||||
bindWorldGenerators(server);
|
||||
@@ -169,6 +171,7 @@ public final class ModdedEngineBootstrap {
|
||||
public static void stop() {
|
||||
MinecraftServer stoppingServer = currentServer;
|
||||
Throwable failure = null;
|
||||
failure = runStopStage(failure, "pack downloads", IrisModdedCommands::shutdownDownloads);
|
||||
failure = runStopStage(failure, "world check", () -> ModdedWorldCheck.serverStopped(stoppingServer));
|
||||
failure = runStopStage(failure, "protocol", ModdedProtocolHandler::stop);
|
||||
failure = runStopStage(failure, "pregenerator", ModdedPregenJob::shutdown);
|
||||
|
||||
+1
-1
@@ -349,7 +349,7 @@ final class ModdedNativeStructureStage {
|
||||
try {
|
||||
int runtimeMinY = world.getMinY();
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, heightmapStarts,
|
||||
world, chunkPos, heightmapStarts,
|
||||
worldgenSurfaceHeight(current, runtimeMinY),
|
||||
worldgenFloorHeight(current, runtimeMinY));
|
||||
} catch (Throwable error) {
|
||||
|
||||
@@ -24,14 +24,19 @@ import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@@ -76,6 +81,9 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
|
||||
private static RejectedExecutionHandler dropRejectedTask() {
|
||||
return (Runnable task, ThreadPoolExecutor executor) -> {
|
||||
if (task instanceof RejectionAwareTask rejectionAwareTask) {
|
||||
rejectionAwareTask.reject();
|
||||
}
|
||||
if (executor.isShutdown()) {
|
||||
LOGGER.debug("Iris async task dropped: scheduler is shut down");
|
||||
return;
|
||||
@@ -130,6 +138,24 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
executor.execute(() -> runGuarded(task));
|
||||
}
|
||||
|
||||
public boolean asyncIfRunning(Runnable task, Runnable rejection) {
|
||||
if (task == null) {
|
||||
return false;
|
||||
}
|
||||
ThreadPoolExecutor executor = asyncExecutor;
|
||||
RejectionAwareTask submittedTask = new RejectionAwareTask(
|
||||
() -> runGuarded(task),
|
||||
Objects.requireNonNull(rejection, "rejection")
|
||||
);
|
||||
warnOnBacklog(executor);
|
||||
try {
|
||||
executor.execute(submittedTask);
|
||||
} catch (RejectedExecutionException exception) {
|
||||
submittedTask.reject();
|
||||
}
|
||||
return !submittedTask.isRejected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void laterGlobal(Runnable task, int ticks) {
|
||||
if (task == null) {
|
||||
@@ -151,7 +177,9 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
if (asyncExecutor.isShutdown()) {
|
||||
asyncExecutor = createAsyncExecutor();
|
||||
} else {
|
||||
asyncExecutor.getQueue().clear();
|
||||
List<Runnable> abandonedTasks = new ArrayList<>();
|
||||
asyncExecutor.getQueue().drainTo(abandonedTasks);
|
||||
rejectAbandonedTasks(abandonedTasks);
|
||||
}
|
||||
mainQueue.clear();
|
||||
delayedQueue.clear();
|
||||
@@ -162,7 +190,8 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
asyncExecutor.shutdownNow();
|
||||
List<Runnable> abandonedTasks = asyncExecutor.shutdownNow();
|
||||
rejectAbandonedTasks(abandonedTasks);
|
||||
mainQueue.clear();
|
||||
delayedQueue.clear();
|
||||
mainThread = null;
|
||||
@@ -170,6 +199,14 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
ModdedServerLevels.forget();
|
||||
}
|
||||
|
||||
private static void rejectAbandonedTasks(List<Runnable> abandonedTasks) {
|
||||
for (Runnable abandonedTask : abandonedTasks) {
|
||||
if (abandonedTask instanceof RejectionAwareTask rejectionAwareTask) {
|
||||
rejectionAwareTask.reject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void warnOnBacklog(ThreadPoolExecutor executor) {
|
||||
int queued = executor.getQueue().size();
|
||||
if (queued < ASYNC_BACKLOG_WARN) {
|
||||
@@ -247,4 +284,33 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RejectionAwareTask implements Runnable {
|
||||
private final Runnable task;
|
||||
private final Runnable rejection;
|
||||
private final AtomicBoolean rejected;
|
||||
|
||||
private RejectionAwareTask(Runnable task, Runnable rejection) {
|
||||
this.task = task;
|
||||
this.rejection = rejection;
|
||||
rejected = new AtomicBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!rejected.get()) {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void reject() {
|
||||
if (rejected.compareAndSet(false, true)) {
|
||||
rejection.run();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRejected() {
|
||||
return rejected.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+181
-29
@@ -19,7 +19,13 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.IrisMessages;
|
||||
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||
import art.arcane.iris.core.localization.PackDownloadMessages;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.core.pack.PackDownloadExecution;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
@@ -56,15 +62,18 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
|
||||
public final class IrisModdedCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
|
||||
private static final Object DOWNLOAD_MONITOR = new Object();
|
||||
|
||||
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = ModdedCommandSuggestions.PACK_NAMES;
|
||||
private static PackDownloadExecution activeDownload;
|
||||
private static boolean downloadAdmissionOpen;
|
||||
|
||||
private IrisModdedCommands() {
|
||||
}
|
||||
@@ -76,6 +85,43 @@ public final class IrisModdedCommands {
|
||||
IrisLogging.info("Iris /iris command tree registered");
|
||||
}
|
||||
|
||||
public static void openDownloadAdmission() {
|
||||
synchronized (DOWNLOAD_MONITOR) {
|
||||
downloadAdmissionOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void shutdownDownloads() {
|
||||
PackDownloadExecution execution;
|
||||
synchronized (DOWNLOAD_MONITOR) {
|
||||
downloadAdmissionOpen = false;
|
||||
execution = activeDownload;
|
||||
}
|
||||
if (execution == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
execution.cancel();
|
||||
boolean interrupted = false;
|
||||
boolean warned = false;
|
||||
while (!execution.isComplete()) {
|
||||
try {
|
||||
if (!execution.await(DOWNLOAD_SHUTDOWN_POLL_SECONDS, TimeUnit.SECONDS) && !warned) {
|
||||
warned = true;
|
||||
LOGGER.warn(execution.isPublishing()
|
||||
? "Waiting for atomic pack publication to finish before Iris shutdown."
|
||||
: "Waiting for the active pack download to cancel before Iris shutdown.");
|
||||
}
|
||||
} catch (InterruptedException exception) {
|
||||
interrupted = true;
|
||||
execution.cancel();
|
||||
}
|
||||
}
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
static int tp(CommandSourceStack source, ServerLevel level, ServerPlayer target) {
|
||||
ServerPlayer player = target != null ? target : source.getPlayer();
|
||||
if (player == null) {
|
||||
@@ -264,9 +310,8 @@ public final class IrisModdedCommands {
|
||||
fail(source, "Use /iris download pack=overworld, /iris download pack=underworld, or /iris download link=<zip-url>.");
|
||||
return 0;
|
||||
}
|
||||
String target = request.pack() == null ? request.url() : request.pack();
|
||||
String target = downloadDisplayTarget(request);
|
||||
String downloadSource = request.pack() == null ? "direct ZIP URL" : "built-in beta release";
|
||||
ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", target), MessageArgument.untrusted("downloadSource", downloadSource)));
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
fail(source, IrisLanguage.plain(
|
||||
@@ -275,36 +320,143 @@ public final class IrisModdedCommands {
|
||||
MessageArgument.untrusted("downloadSource", downloadSource)));
|
||||
return 0;
|
||||
}
|
||||
scheduler.async(() -> {
|
||||
boolean installed = false;
|
||||
File packs = ModdedPackCommands.packsRoot();
|
||||
PackDownloadExecution execution;
|
||||
synchronized (DOWNLOAD_MONITOR) {
|
||||
if (!downloadAdmissionOpen) {
|
||||
fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
|
||||
MessageArgument.untrusted("pack", target),
|
||||
MessageArgument.untrusted("downloadSource", downloadSource)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
LifecycleOperationCoordinator.Lease lease;
|
||||
try {
|
||||
PackDownloader.PackInstallResult result = request.pack() == null
|
||||
? PackDownloader.downloadUrl(
|
||||
packs,
|
||||
request.url(),
|
||||
false,
|
||||
(String message) -> scheduler.global(() -> ok(source, message))
|
||||
)
|
||||
: PackDownloader.downloadBuiltIn(
|
||||
packs,
|
||||
request.pack(),
|
||||
false,
|
||||
(String message) -> scheduler.global(() -> ok(source, message))
|
||||
);
|
||||
installed = result != null;
|
||||
} catch (IOException | RuntimeException error) {
|
||||
LOGGER.error("Iris pack download failed for {}", target, error);
|
||||
lease = LifecycleOperationCoordinator.get().acquire(
|
||||
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
|
||||
LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD,
|
||||
target
|
||||
);
|
||||
} catch (LifecycleOperationCoordinator.BusyException error) {
|
||||
fail(source, downloadBusyMessage(error.currentOperation()));
|
||||
return 0;
|
||||
}
|
||||
if (installed) {
|
||||
scheduler.global(() -> ok(source, "Pack installed on disk. Restart the server before using it."));
|
||||
} else {
|
||||
scheduler.global(() -> fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, MessageArgument.untrusted("pack", target), MessageArgument.untrusted("downloadSource", downloadSource))));
|
||||
|
||||
execution = new PackDownloadExecution(
|
||||
lease,
|
||||
cancellation -> executeDownload(source, request, target, downloadSource, scheduler, cancellation)
|
||||
);
|
||||
PackDownloadExecution trackedExecution = execution;
|
||||
execution.onCompletion(() -> clearActiveDownload(trackedExecution));
|
||||
activeDownload = execution;
|
||||
boolean accepted;
|
||||
try {
|
||||
accepted = scheduler.asyncIfRunning(execution, execution::cancel);
|
||||
} catch (Throwable error) {
|
||||
execution.cancel();
|
||||
LOGGER.error("Iris pack download dispatch failed for {}", target, error);
|
||||
fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
|
||||
MessageArgument.untrusted("pack", target),
|
||||
MessageArgument.untrusted("downloadSource", downloadSource)));
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
if (!accepted) {
|
||||
execution.cancel();
|
||||
LOGGER.error("Iris pack download dispatch rejected for {} because the scheduler is shut down", target);
|
||||
fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
|
||||
MessageArgument.untrusted("pack", target),
|
||||
MessageArgument.untrusted("downloadSource", downloadSource)));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
ok(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.IRIS_MODDED_COMMANDS_DOWNLOADING_IRISDIMENSIONS,
|
||||
MessageArgument.untrusted("pack", target),
|
||||
MessageArgument.untrusted("downloadSource", downloadSource)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void executeDownload(
|
||||
CommandSourceStack source,
|
||||
DownloadRequest request,
|
||||
String target,
|
||||
String downloadSource,
|
||||
ModdedScheduler scheduler,
|
||||
PackDownloader.DownloadCancellation cancellation
|
||||
) throws PackDownloader.PackDownloadCancelledException {
|
||||
File packs = ModdedPackCommands.packsRoot();
|
||||
try {
|
||||
PackDownloader.PackInstallResult result = request.pack() == null
|
||||
? PackDownloader.downloadUrl(
|
||||
packs,
|
||||
request.url(),
|
||||
false,
|
||||
(String message) -> scheduler.global(() -> ok(source, message)),
|
||||
cancellation
|
||||
)
|
||||
: PackDownloader.downloadBuiltIn(
|
||||
packs,
|
||||
request.pack(),
|
||||
false,
|
||||
(String message) -> scheduler.global(() -> ok(source, message)),
|
||||
cancellation
|
||||
);
|
||||
String completionMessage = downloadCompletionMessage(result);
|
||||
if (result != null) {
|
||||
if (completionMessage != null) {
|
||||
scheduler.global(() -> ok(source, completionMessage));
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (PackDownloader.PackDownloadCancelledException error) {
|
||||
throw error;
|
||||
} catch (PackDownloader.PackDownloadBusyException error) {
|
||||
scheduler.global(() -> fail(source, error.getMessage()));
|
||||
return;
|
||||
} catch (IOException | RuntimeException error) {
|
||||
LOGGER.error("Iris pack download failed for {}", target, error);
|
||||
}
|
||||
scheduler.global(() -> fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
|
||||
MessageArgument.untrusted("pack", target),
|
||||
MessageArgument.untrusted("downloadSource", downloadSource))));
|
||||
}
|
||||
|
||||
static String downloadBusyMessage(LifecycleOperationCoordinator.ActiveOperation operation) {
|
||||
if (operation.domain() == LifecycleOperationCoordinator.Domain.PACK_MUTATION
|
||||
&& operation.kind() == LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD) {
|
||||
return IrisLanguage.plain(PackDownloadMessages.IN_PROGRESS);
|
||||
}
|
||||
return "Iris pack changes are busy with " + operation.kind().name().toLowerCase(Locale.ROOT)
|
||||
+ " for '" + operation.target() + "'. Try again when it completes.";
|
||||
}
|
||||
|
||||
static String downloadCompletionMessage(PackDownloader.PackInstallResult result) {
|
||||
if (result == null || !result.changed()) {
|
||||
return null;
|
||||
}
|
||||
return result.restartRequired()
|
||||
? "Pack installed on disk. Restart the server before using it."
|
||||
: "Pack installed on disk.";
|
||||
}
|
||||
|
||||
static String downloadDisplayTarget(DownloadRequest request) {
|
||||
return request.pack() == null
|
||||
? IrisLanguage.plain(PackDownloadMessages.PROGRESS_SOURCE_REMOTE)
|
||||
: request.pack();
|
||||
}
|
||||
|
||||
private static void clearActiveDownload(PackDownloadExecution execution) {
|
||||
synchronized (DOWNLOAD_MONITOR) {
|
||||
if (activeDownload == execution) {
|
||||
activeDownload = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static DownloadRequest parseDownloadRequest(String rawRequest) {
|
||||
if (rawRequest == null || rawRequest.isBlank()) {
|
||||
return null;
|
||||
|
||||
+54
@@ -86,6 +86,60 @@ public class ModdedGenerationLeaseContractTest {
|
||||
assertTrue(source.contains("active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packDownloadAdmissionPrecedesAsyncDispatch() throws IOException {
|
||||
String source = source("art/arcane/iris/modded/command/IrisModdedCommands.java");
|
||||
String download = method(source, "static int download(CommandSourceStack source, String rawRequest)");
|
||||
int admission = download.indexOf("LifecycleOperationCoordinator.get().acquire(");
|
||||
int executionTracking = download.indexOf("new PackDownloadExecution(");
|
||||
int dispatch = download.indexOf("scheduler.asyncIfRunning(execution, execution::cancel)");
|
||||
|
||||
assertTrue(admission >= 0);
|
||||
assertTrue(executionTracking > admission);
|
||||
assertTrue(dispatch > admission);
|
||||
assertTrue(download.contains("execution.cancel();"));
|
||||
|
||||
String execution = method(source, "private static void executeDownload(");
|
||||
assertTrue(execution.contains("PackDownloader.DownloadCancellation cancellation"));
|
||||
assertTrue(execution.contains("catch (PackDownloader.PackDownloadCancelledException error)"));
|
||||
assertFalse(execution.contains("lease.close();"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packDownloadsDrainBeforeModdedSchedulerShutdown() throws IOException {
|
||||
String commands = source("art/arcane/iris/modded/command/IrisModdedCommands.java");
|
||||
String shutdownDownloads = method(commands, "public static void shutdownDownloads()");
|
||||
assertTrue(shutdownDownloads.contains("downloadAdmissionOpen = false;"));
|
||||
assertTrue(shutdownDownloads.contains("execution.cancel();"));
|
||||
assertTrue(shutdownDownloads.contains("execution.await("));
|
||||
|
||||
String bootstrap = source("art/arcane/iris/modded/ModdedEngineBootstrap.java");
|
||||
String stop = method(bootstrap, "public static void stop()");
|
||||
int downloads = stop.indexOf("IrisModdedCommands::shutdownDownloads");
|
||||
int scheduler = stop.indexOf("scheduler::shutdown");
|
||||
assertTrue(downloads >= 0);
|
||||
assertTrue(scheduler > downloads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moddedSchedulerRejectsAndCancelsAbandonedDownloadSubmissions() throws IOException {
|
||||
String scheduler = source("art/arcane/iris/modded/ModdedScheduler.java");
|
||||
String dispatch = method(scheduler, "public boolean asyncIfRunning(Runnable task, Runnable rejection)");
|
||||
assertTrue(dispatch.contains("RejectionAwareTask"));
|
||||
assertTrue(dispatch.contains("Objects.requireNonNull(rejection"));
|
||||
|
||||
String shutdown = method(scheduler, "public void shutdown()");
|
||||
assertTrue(shutdown.contains("asyncExecutor.shutdownNow()"));
|
||||
assertTrue(shutdown.contains("rejectAbandonedTasks(abandonedTasks);"));
|
||||
|
||||
String reset = method(scheduler, "public void reset()");
|
||||
assertTrue(reset.contains("asyncExecutor.getQueue().drainTo(abandonedTasks);"));
|
||||
assertTrue(reset.contains("rejectAbandonedTasks(abandonedTasks);"));
|
||||
|
||||
String rejection = method(scheduler, "private static void rejectAbandonedTasks(");
|
||||
assertTrue(rejection.contains("rejectionAwareTask.reject();"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockingPregenShutdownDefersItsFinalSaveToTheServerThread() throws IOException {
|
||||
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.core.pack.PackDownloadExecution;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedSchedulerDownloadCancellationTest {
|
||||
@Test
|
||||
public void rejectedDownloadSubmissionReleasesItsLease() throws Exception {
|
||||
ModdedScheduler scheduler = new ModdedScheduler();
|
||||
scheduler.shutdown();
|
||||
TestLease lease = new TestLease();
|
||||
AtomicBoolean ran = new AtomicBoolean();
|
||||
PackDownloadExecution execution = new PackDownloadExecution(
|
||||
lease,
|
||||
cancellation -> ran.set(true)
|
||||
);
|
||||
|
||||
boolean accepted = scheduler.asyncIfRunning(execution, execution::cancel);
|
||||
|
||||
assertFalse(accepted);
|
||||
assertTrue(execution.await(1L, TimeUnit.SECONDS));
|
||||
assertFalse(ran.get());
|
||||
assertEquals(1, lease.closeCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schedulerShutdownCancelsQueuedDownloadAndReleasesItsLease() throws Exception {
|
||||
ModdedScheduler scheduler = new ModdedScheduler();
|
||||
int workerCount = Math.max(4, Runtime.getRuntime().availableProcessors());
|
||||
CountDownLatch workersStarted = new CountDownLatch(workerCount);
|
||||
CountDownLatch releaseWorkers = new CountDownLatch(1);
|
||||
for (int index = 0; index < workerCount; index++) {
|
||||
scheduler.async(() -> {
|
||||
workersStarted.countDown();
|
||||
try {
|
||||
releaseWorkers.await();
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
}
|
||||
assertTrue(workersStarted.await(10L, TimeUnit.SECONDS));
|
||||
|
||||
TestLease lease = new TestLease();
|
||||
AtomicBoolean ran = new AtomicBoolean();
|
||||
PackDownloadExecution execution = new PackDownloadExecution(
|
||||
lease,
|
||||
cancellation -> ran.set(true)
|
||||
);
|
||||
assertTrue(scheduler.asyncIfRunning(execution, execution::cancel));
|
||||
|
||||
try {
|
||||
scheduler.shutdown();
|
||||
|
||||
assertTrue(execution.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(ran.get());
|
||||
assertEquals(1, lease.closeCount());
|
||||
} finally {
|
||||
releaseWorkers.countDown();
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestLease implements LifecycleOperationCoordinator.Lease {
|
||||
private final LifecycleOperationCoordinator.ActiveOperation operation;
|
||||
private final AtomicBoolean closed;
|
||||
private final AtomicInteger closeCount;
|
||||
|
||||
private TestLease() {
|
||||
operation = new LifecycleOperationCoordinator.ActiveOperation(
|
||||
1L,
|
||||
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
|
||||
LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD,
|
||||
"test"
|
||||
);
|
||||
closed = new AtomicBoolean();
|
||||
closeCount = new AtomicInteger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifecycleOperationCoordinator.ActiveOperation operation() {
|
||||
return operation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return closed.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closeCount.incrementAndGet();
|
||||
closed.set(true);
|
||||
}
|
||||
|
||||
private int closeCount() {
|
||||
return closeCount.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
-1
@@ -1,5 +1,9 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.PackDownloadMessages;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
import net.minecraft.SharedConstants;
|
||||
@@ -9,10 +13,11 @@ import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisModdedCommandParityTest {
|
||||
@@ -73,6 +78,9 @@ public class IrisModdedCommandParityTest {
|
||||
assertEquals("underworld", underworld.pack());
|
||||
assertNotNull(link);
|
||||
assertEquals("https://packs.example.test/custom.zip?token=a=b", link.url());
|
||||
String displayTarget = IrisModdedCommands.downloadDisplayTarget(link);
|
||||
assertEquals(IrisLanguage.plain(PackDownloadMessages.PROGRESS_SOURCE_REMOTE), displayTarget);
|
||||
assertFalse(displayTarget.contains("token"));
|
||||
assertNull(IrisModdedCommands.parseDownloadRequest("overworld"));
|
||||
assertNull(IrisModdedCommands.parseDownloadRequest("pack=custom"));
|
||||
assertNull(IrisModdedCommands.parseDownloadRequest("link=https://packs.example.test/custom.tar.gz"));
|
||||
@@ -80,6 +88,51 @@ public class IrisModdedCommandParityTest {
|
||||
assertNull(IrisModdedCommands.parseDownloadRequest("pack=underworld overwrite=true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadCompletionMessageOnlyReportsActualPackChanges() {
|
||||
assertEquals(
|
||||
"Pack installed on disk. Restart the server before using it.",
|
||||
IrisModdedCommands.downloadCompletionMessage(
|
||||
new PackDownloader.PackInstallResult("overworld", true, true)
|
||||
)
|
||||
);
|
||||
assertEquals(
|
||||
"Pack installed on disk.",
|
||||
IrisModdedCommands.downloadCompletionMessage(
|
||||
new PackDownloader.PackInstallResult("overworld", true, false)
|
||||
)
|
||||
);
|
||||
assertNull(IrisModdedCommands.downloadCompletionMessage(
|
||||
new PackDownloader.PackInstallResult("overworld", false, false)
|
||||
));
|
||||
assertNull(IrisModdedCommands.downloadCompletionMessage(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadBusyMessageDistinguishesPackDownloadsFromOtherLifecycleWork() {
|
||||
LifecycleOperationCoordinator.ActiveOperation packDownload = new LifecycleOperationCoordinator.ActiveOperation(
|
||||
1L,
|
||||
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
|
||||
LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD,
|
||||
"overworld"
|
||||
);
|
||||
LifecycleOperationCoordinator.ActiveOperation worldCreation = new LifecycleOperationCoordinator.ActiveOperation(
|
||||
2L,
|
||||
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
||||
LifecycleOperationCoordinator.OperationKind.WORLD_CREATE,
|
||||
"iris_world"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
IrisLanguage.plain(PackDownloadMessages.IN_PROGRESS),
|
||||
IrisModdedCommands.downloadBusyMessage(packDownload)
|
||||
);
|
||||
assertEquals(
|
||||
"Iris pack changes are busy with world_create for 'iris_world'. Try again when it completes.",
|
||||
IrisModdedCommands.downloadBusyMessage(worldCreation)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void helpDocumentsParityCommandsAndPlatformStubs() {
|
||||
assertTrue(ModdedCommandHelp.documents("what", "here"));
|
||||
|
||||
Reference in New Issue
Block a user