mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Defer runtime injection
- Add RuntimeInjection so the Java agent attach and the server-code injection run once, on demand, instead of during every enable - Install the injection when a world Iris created is about to be built and when Bukkit asks for the generator of a world configured outside Iris - Lock the runtime with the same reason a boot-time injection failure used to raise when the deferred install fails - Add general.eagerRuntimeInjection to keep boot-time injection and its startup verification - Re-read the startup lock after a deferred install so a failed install refuses the world instead of generating it - Move the stale cache/temp sweep off the boot thread and make every pack import wait it out - Move the default-pack presence warning off the boot thread; it walks the whole pack tree to pick a warning - Build the wand and dust templates on the first tick instead of during service enable - Report enable duration, per-phase timings and the three slowest services on every boot - Drop the wand and dust templates on service disable
This commit is contained in:
1 parent
6c6dba0232
commit
486c6eeae9
11 files changed
+426
-35
No files matched your search
@@ -308,6 +308,13 @@ public class IrisSettings {
|
||||
public boolean debug = false;
|
||||
public boolean dumpMantleOnError = false;
|
||||
public boolean disableNMS = false;
|
||||
/**
|
||||
* Attach the Java agent and install the server-code injection while Iris enables, instead of the
|
||||
* first time a world Iris generates is about to load. Boot-time injection costs roughly a second
|
||||
* on a server that may never load an Iris world; turn this on to pay it up front and have the
|
||||
* startup safeguard verify it.
|
||||
*/
|
||||
public boolean eagerRuntimeInjection = false;
|
||||
public boolean splashLogoStartup = true;
|
||||
public boolean useConsoleCustomColors = true;
|
||||
public boolean useCustomColorsIngame = true;
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.runtime.RuntimeInjection;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
|
||||
@@ -22,6 +23,7 @@ public final class IrisRuntimeStatics {
|
||||
IrisServices.clear();
|
||||
WorldLifecycleService.reset();
|
||||
WorldRuntimeControlService.reset();
|
||||
RuntimeInjection.reset();
|
||||
ServerConfigurator.resetLoadedDatapackRuntime();
|
||||
IrisLanguage.shutdown();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.runtime.RuntimeInjection;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
@@ -85,6 +86,7 @@ public final class WorldLifecycleService {
|
||||
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||
WorldLifecycleBackend backend;
|
||||
try {
|
||||
RuntimeInjection.installIfDeferred();
|
||||
if (request.callerKind() == WorldLifecycleCaller.FORCED_STUDIO) {
|
||||
ServerConfigurator.requireWorldCreationReady(true);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.core.IrisStartupValidation;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.project.agent.Agent;
|
||||
|
||||
/**
|
||||
* Attaches the Java agent and installs the server-code injection Iris generation needs, once per boot.
|
||||
* <p>
|
||||
* Both steps are expensive - a self-attach to the running JVM, then three class retransformations - and
|
||||
* neither is needed until a world that Iris generates is about to exist. A server that never loads an Iris
|
||||
* world never pays for them. Whoever is about to produce such a world calls {@link #installIfDeferred()}
|
||||
* first; it is idempotent, and a failure marks the runtime invalid so the same lock that guards a failed
|
||||
* boot-time injection guards a failed deferred one.
|
||||
*/
|
||||
public final class RuntimeInjection {
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private static volatile Outcome outcome;
|
||||
|
||||
private RuntimeInjection() {
|
||||
}
|
||||
|
||||
public enum Failure {
|
||||
NONE,
|
||||
NO_BINDING,
|
||||
AGENT,
|
||||
INJECTION
|
||||
}
|
||||
|
||||
public record Outcome(boolean installed, Failure failure, String lockReason) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the install if it has not run yet and reports what happened. Never throws.
|
||||
*/
|
||||
public static Outcome install() {
|
||||
Outcome current = outcome;
|
||||
if (current != null) {
|
||||
return current;
|
||||
}
|
||||
|
||||
synchronized (LOCK) {
|
||||
if (outcome != null) {
|
||||
return outcome;
|
||||
}
|
||||
Outcome resolved = attemptInstall();
|
||||
outcome = resolved;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs on the deferred path and locks the runtime when that fails, so the caller's own readiness
|
||||
* check refuses the world instead of loading it onto uninstrumented server code.
|
||||
*/
|
||||
public static void installIfDeferred() {
|
||||
Outcome resolved = install();
|
||||
if (resolved.installed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisLogging.error("Iris runtime injection is unavailable: " + resolved.lockReason());
|
||||
IrisStartupValidation.markRuntimeInvalid(resolved.lockReason());
|
||||
}
|
||||
|
||||
public static boolean isInstalled() {
|
||||
Outcome current = outcome;
|
||||
return current != null && current.installed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgets the result so the next boot in this JVM installs against the binding it is running with.
|
||||
*/
|
||||
public static void reset() {
|
||||
synchronized (LOCK) {
|
||||
outcome = null;
|
||||
}
|
||||
}
|
||||
|
||||
static String agentLockReason() {
|
||||
return "Iris Java agent is unavailable. Add -javaagent:" + Agent.AGENT_JAR.getPath()
|
||||
+ " to the JVM arguments before -jar and restart the server.";
|
||||
}
|
||||
|
||||
private static Outcome attemptInstall() {
|
||||
if (!INMS.isBound() || INMS.get() instanceof NMSBinding1X) {
|
||||
return new Outcome(false, Failure.NO_BINDING,
|
||||
"Iris cannot use this server's NMS runtime. Resolve the Server Version or NMS Disabled error above and restart the server.");
|
||||
}
|
||||
|
||||
if (!Agent.install()) {
|
||||
return new Outcome(false, Failure.AGENT, agentLockReason());
|
||||
}
|
||||
|
||||
if (!INMS.get().injectBukkit()) {
|
||||
return new Outcome(false, Failure.INJECTION,
|
||||
"Iris runtime injection failed. Resolve the startup errors and restart the server.");
|
||||
}
|
||||
|
||||
return new Outcome(true, Failure.NONE, null);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package art.arcane.iris.core.safeguard.task;
|
||||
|
||||
import art.arcane.iris.BuildConstants;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.runtime.RuntimeInjection;
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
||||
@@ -128,28 +130,29 @@ public final class Tasks {
|
||||
});
|
||||
|
||||
private static final Task INJECTION = Task.critical("injection", INJECTION_LOCK_REASON, () -> {
|
||||
if (!INMS.isBound() || INMS.get() instanceof NMSBinding1X) {
|
||||
return CheckResult.danger(NMS_LOCK_REASON,
|
||||
Diagnostic.Logger.ERROR.create("Code Injection"),
|
||||
Diagnostic.Logger.ERROR.create("- Runtime injection was skipped because Iris has no usable NMS binding."));
|
||||
if (!IrisSettings.get().getGeneral().isEagerRuntimeInjection()) {
|
||||
return CheckResult.stable(
|
||||
Diagnostic.Logger.INFO.create("Runtime Injection"),
|
||||
Diagnostic.Logger.INFO.create("- Deferred to the first world load. Set general.eagerRuntimeInjection to verify it during startup."));
|
||||
}
|
||||
|
||||
if (!Agent.install()) {
|
||||
String agentLockReason = "Iris Java agent is unavailable. Add -javaagent:"
|
||||
+ Agent.AGENT_JAR.getPath() + " to the JVM arguments before -jar and restart the server.";
|
||||
return CheckResult.danger(agentLockReason,
|
||||
RuntimeInjection.Outcome outcome = RuntimeInjection.install();
|
||||
if (outcome.installed()) {
|
||||
return CheckResult.stable();
|
||||
}
|
||||
|
||||
return switch (outcome.failure()) {
|
||||
case NO_BINDING -> CheckResult.danger(outcome.lockReason(),
|
||||
Diagnostic.Logger.ERROR.create("Code Injection"),
|
||||
Diagnostic.Logger.ERROR.create("- Runtime injection was skipped because Iris has no usable NMS binding."));
|
||||
case AGENT -> CheckResult.danger(outcome.lockReason(),
|
||||
Diagnostic.Logger.ERROR.create("Java Agent"),
|
||||
Diagnostic.Logger.ERROR.create("- Add -javaagent:" + Agent.AGENT_JAR.getPath() + " before -jar in your startup command and restart the server."),
|
||||
Diagnostic.Logger.ERROR.create("- Dynamic attachment requires -XX:+EnableDynamicAgentLoading and a host that permits JVM attachment."));
|
||||
}
|
||||
|
||||
if (!INMS.get().injectBukkit()) {
|
||||
return CheckResult.danger(INJECTION_LOCK_REASON,
|
||||
case INJECTION, NONE -> CheckResult.danger(INJECTION_LOCK_REASON,
|
||||
Diagnostic.Logger.ERROR.create("Code Injection"),
|
||||
Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support"));
|
||||
}
|
||||
|
||||
return CheckResult.stable();
|
||||
};
|
||||
});
|
||||
|
||||
private static final Task DIMENSION_TYPES = Task.critical("dimensionTypes", DIMENSION_TYPE_LOCK_REASON, () -> {
|
||||
|
||||
@@ -90,8 +90,10 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -106,23 +108,54 @@ public class StudioSVC implements IrisService {
|
||||
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
|
||||
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
||||
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
|
||||
private static final long STALE_TEMP_CLEANUP_TIMEOUT_SECONDS = 120L;
|
||||
private static volatile CompletableFuture<Void> staleTempCleanup = CompletableFuture.completedFuture(null);
|
||||
|
||||
private final Object downloadAdmissionMonitor = new Object();
|
||||
private volatile IrisProject activeProject;
|
||||
private volatile CompletableFuture<StudioOpenCoordinator.StudioOpenResult> activeOpen;
|
||||
private PackDownloadExecution activeDownload;
|
||||
private boolean downloadAdmissionOpen;
|
||||
|
||||
/**
|
||||
* Binds the stale cache/temp sweep a boot starts in the background. Pack imports stage through that
|
||||
* folder, so a delete running underneath one truncates it; the import waits the sweep out instead of
|
||||
* the boot waiting for the sweep.
|
||||
*/
|
||||
public static void gateDownloadsOnStaleTempCleanup(CompletableFuture<Void> cleanup) {
|
||||
staleTempCleanup = cleanup == null ? CompletableFuture.completedFuture(null) : cleanup;
|
||||
}
|
||||
|
||||
private static void awaitStaleTempCleanup() {
|
||||
CompletableFuture<Void> cleanup = staleTempCleanup;
|
||||
if (cleanup.isDone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cleanup.get(STALE_TEMP_CLEANUP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
IrisLogging.reportError("Interrupted while waiting for the stale Iris temp sweep to finish.", interrupted);
|
||||
} catch (ExecutionException | TimeoutException failure) {
|
||||
IrisLogging.reportError("The stale Iris temp sweep did not finish; this pack import may race it.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
synchronized (downloadAdmissionMonitor) {
|
||||
activeDownload = null;
|
||||
downloadAdmissionOpen = true;
|
||||
}
|
||||
String configuredPack = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||
if (!PackDownloader.isPackPresent(getWorkspaceFolder(), configuredPack)) {
|
||||
IrisLogging.warn("Default pack '" + configuredPack
|
||||
+ "' is not installed. Install a built-in pack with /iris download pack=overworld or provide /iris download link=<zip-url>.");
|
||||
}
|
||||
MultiBurst.ioBurst.completeValueAsync(() -> {
|
||||
String configuredPack = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||
if (!PackDownloader.isPackPresent(getWorkspaceFolder(), configuredPack)) {
|
||||
IrisLogging.warn("Default pack '" + configuredPack
|
||||
+ "' is not installed. Install a built-in pack with /iris download pack=overworld or provide /iris download link=<zip-url>.");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1274,7 +1307,10 @@ public class StudioSVC implements IrisService {
|
||||
|
||||
execution = new PackDownloadExecution(
|
||||
lease,
|
||||
cancellation -> executePackMutation(mutation, reporter, failureMessage, cancellation)
|
||||
cancellation -> {
|
||||
awaitStaleTempCleanup();
|
||||
executePackMutation(mutation, reporter, failureMessage, cancellation);
|
||||
}
|
||||
);
|
||||
PackDownloadExecution trackedExecution = execution;
|
||||
execution.onCompletion(() -> {
|
||||
|
||||
Reference in new issue
Block a user