PaperMC/paper-server/patches/features/0028-Improved-Watchdog-Support.patch
2024-12-21 12:15:25 +01:00

317 lines
17 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Sun, 12 Apr 2020 15:50:48 -0400
Subject: [PATCH] Improved Watchdog Support
Forced Watchdog Crash support and Improve Async Shutdown
If the request to shut down the server is received while we are in
a watchdog hang, immediately treat it as a crash and begin the shutdown
process. Shutdown process is now improved to also shutdown cleanly when
not using restart scripts either.
If a server is deadlocked, a server owner can send SIGUP (or any other signal
the JVM understands to shut down as it currently does) and the watchdog
will no longer need to wait until the full timeout, allowing you to trigger
a close process and try to shut the server down gracefully, saving player and
world data.
Previously there was no way to trigger this outside of waiting for a full watchdog
timeout, which may be set to a really long time...
Additionally, fix everything to do with shutting the server down asynchronously.
Previously, nearly everything about the process was fragile and unsafe. Main might
not have actually been frozen, and might still be manipulating state.
Or, some reuest might ask main to do something in the shutdown but main is dead.
Or worse, other things might start closing down items such as the Console or Thread Pool
before we are fully shutdown.
This change tries to resolve all of these issues by moving everything into the stop
method and guaranteeing only one thread is stopping the server.
We then issue Thread Death to the main thread of another thread initiates the stop process.
We have to ensure Thread Death propagates correctly though to stop main completely.
This is to ensure that if main isn't truely stuck, it's not manipulating state we are trying to save.
This also moves all plugins who register "delayed init" tasks to occur just before "Done" so they
are properly accounted for and wont trip watchdog on init.
diff --git a/io/papermc/paper/util/LogManagerShutdownThread.java b/io/papermc/paper/util/LogManagerShutdownThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..3d7df554b89cff23f64da7ad48b5e4d26ac2baf7
--- /dev/null
+++ b/io/papermc/paper/util/LogManagerShutdownThread.java
@@ -0,0 +1,29 @@
+package io.papermc.paper.util;
+
+import org.apache.logging.log4j.LogManager;
+
+public final class LogManagerShutdownThread extends Thread {
+
+ static LogManagerShutdownThread INSTANCE = new LogManagerShutdownThread();
+
+ public static void hook() {
+ if (INSTANCE == null) {
+ throw new IllegalStateException("Cannot re-hook after being unhooked");
+ }
+ Runtime.getRuntime().addShutdownHook(INSTANCE);
+ }
+
+ public static void unhook() {
+ Runtime.getRuntime().removeShutdownHook(INSTANCE);
+ INSTANCE = null;
+ }
+
+ private LogManagerShutdownThread() {
+ super("Log4j2 Shutdown Thread");
+ }
+
+ @Override
+ public void run() {
+ LogManager.shutdown();
+ }
+}
diff --git a/net/minecraft/CrashReport.java b/net/minecraft/CrashReport.java
index 3e0e88afcf010d9a3d46e48bca5cbdf98fe97544..8bd7999c17c8772451f873966f8c90969aee1482 100644
--- a/net/minecraft/CrashReport.java
+++ b/net/minecraft/CrashReport.java
@@ -205,6 +205,7 @@ public class CrashReport {
}
public static CrashReport forThrowable(Throwable cause, String description) {
+ if (cause instanceof ThreadDeath) com.destroystokyo.paper.util.SneakyThrow.sneaky(cause); // Paper
while (cause instanceof CompletionException && cause.getCause() != null) {
cause = cause.getCause();
}
diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java
index e738405e5112584e02e01df2d5ede2676fa1bffb..560d80cb1177297210646b44ce25fd2fa3766d40 100644
--- a/net/minecraft/server/Main.java
+++ b/net/minecraft/server/Main.java
@@ -68,6 +68,7 @@ public class Main {
)
@DontObfuscate
public static void main(final OptionSet optionSet) { // CraftBukkit - replaces main(String[] args)
+ io.papermc.paper.util.LogManagerShutdownThread.hook(); // Paper
SharedConstants.tryDetectVersion();
/* CraftBukkit start - Replace everything
OptionParser optionParser = new OptionParser();
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index ddd23354d68f5cbdc9f72c11246ab26a6c0bbe16..3f880bdfd95f7556d1d76e4602a6d24dda78438c 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -298,6 +298,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Spigot end
public volatile boolean hasFullyShutdown; // Paper - Improved watchdog support
public volatile boolean abnormalExit; // Paper - Improved watchdog support
+ public volatile Thread shutdownThread; // Paper - Improved watchdog support
public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations; // Paper - add paper configuration files
public boolean isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
private final Set<String> pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping
@@ -466,6 +467,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
*/
// Paper end
+ io.papermc.paper.util.LogManagerShutdownThread.unhook(); // Paper
Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this));
// CraftBukkit end
this.paperConfigurations = services.paperConfigurations(); // Paper - add paper configuration files
@@ -990,6 +992,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.hasStopped = true;
}
if (!hasLoggedStop && isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper - Debugging
+ // Paper start - kill main thread, and kill it hard
+ shutdownThread = Thread.currentThread();
+ org.spigotmc.WatchdogThread.doStop(); // Paper
+ // Paper end
// CraftBukkit end
if (this.metricsRecorder.isRecording()) {
this.cancelRecordingMetrics();
@@ -1061,6 +1067,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
ca.spottedleaf.moonrise.common.util.MoonriseCommon.haltExecutors();
}
// Paper end - rewrite chunk system
+ // Paper start - Improved watchdog support - move final shutdown items here
+ Util.shutdownExecutors();
+ try {
+ net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender
+ } catch (final Exception ignored) {
+ }
+ io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
+ this.onServerExit();
+ // Paper end - Improved watchdog support - move final shutdown items here
}
public String getLocalIp() {
@@ -1147,6 +1162,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
protected void runServer() {
try {
+ long serverStartTime = Util.getNanos(); // Paper
if (!this.initServer()) {
throw new IllegalStateException("Failed to initialize server");
}
@@ -1157,6 +1173,17 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.server.spark.enableBeforePlugins(); // Paper - spark
// Spigot start
+ // Paper start - Improved Watchdog Support
+ LOGGER.info("Running delayed init tasks");
+ this.server.getScheduler().mainThreadHeartbeat(); // run all 1 tick delay tasks during init,
+ // this is going to be the first thing the tick process does anyways, so move done and run it after
+ // everything is init before watchdog tick.
+ // anything at 3+ won't be caught here but also will trip watchdog....
+ // tasks are default scheduled at -1 + delay, and first tick will tick at 1
+ final long actualDoneTimeMs = System.currentTimeMillis() - org.bukkit.craftbukkit.Main.BOOT_TIME.toEpochMilli(); // Paper - Add total time
+ LOGGER.info("Done ({})! For help, type \"help\"", String.format(java.util.Locale.ROOT, "%.3fs", actualDoneTimeMs / 1000.00D)); // Paper - Add total time
+ org.spigotmc.WatchdogThread.tick();
+ // Paper end - Improved Watchdog Support
org.spigotmc.WatchdogThread.hasStarted = true; // Paper
Arrays.fill(this.recentTps, 20);
// Paper start - further improve server tick loop
@@ -1253,6 +1280,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
JvmProfiler.INSTANCE.onServerTick(this.smoothedTickTimeMillis);
}
} catch (Throwable var69) {
+ // Paper start
+ if (var69 instanceof ThreadDeath) {
+ MinecraftServer.LOGGER.error("Main thread terminated by WatchDog due to hard crash", var69);
+ return;
+ }
+ // Paper end
LOGGER.error("Encountered an unexpected exception", var69);
CrashReport crashReport = constructOrExtractCrashReport(var69);
this.fillSystemReport(crashReport.getSystemReport());
@@ -1275,15 +1308,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.services.profileCache().clearExecutor();
}
- org.spigotmc.WatchdogThread.doStop(); // Spigot
+ //org.spigotmc.WatchdogThread.doStop(); // Spigot // Paper - move into stop
// CraftBukkit start - Restore terminal to original settings
try {
- net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender
+ //net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Move into stop
} catch (Exception ignored) {
}
// CraftBukkit end
- io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
- this.onServerExit();
+ //io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
+ //this.onServerExit(); // Paper - moved into stop
}
}
}
@@ -1387,6 +1420,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@Override
public TickTask wrapRunnable(Runnable runnable) {
+ // Paper start - anything that does try to post to main during watchdog crash, run on watchdog
+ if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) {
+ runnable.run();
+ runnable = () -> {};
+ }
+ // Paper end
return new TickTask(this.tickCount, runnable);
}
@@ -2184,7 +2223,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.resources.managers.updateStaticRegistryTags();
this.resources.managers.getRecipeManager().finalizeRecipeLoading(this.worldData.enabledFeatures());
this.potionBrewing = this.potionBrewing.reload(this.worldData.enabledFeatures()); // Paper - Custom Potion Mixes
- this.getPlayerList().saveAll();
+ // Paper start
+ if (Thread.currentThread() != this.serverThread) {
+ return;
+ }
+ // this.getPlayerList().saveAll(); // Paper - we don't need to save everything, just advancements // TODO Move this to a different patch
+ for (ServerPlayer player : this.getPlayerList().getPlayers()) {
+ player.getAdvancements().save();
+ }
+ // Paper end
this.getPlayerList().reloadResources();
this.functionManager.replaceLibrary(this.resources.managers.getFunctionLibrary());
this.structureTemplateManager.onResourceManagerReload(this.resources.resourceManager);
diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
index 55d3f79af2e683b983d4d3f731bb9649dfe76f59..d900469dafd430ec3eba10d6f83bd8759f7a7edf 100644
--- a/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/net/minecraft/server/dedicated/DedicatedServer.java
@@ -322,7 +322,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
this.loadLevel(this.storageSource.getLevelId()); // CraftBukkit
long l = Util.getNanos() - nanos;
String string = String.format(Locale.ROOT, "%.3fs", l / 1.0E9);
- LOGGER.info("Done ({})! For help, type \"help\"", string);
+ LOGGER.info("Done preparing level \"{}\" ({})", this.getLevelIdName(), string); // Paper - clarify startup log messages & add total time
if (properties.announcePlayerAchievements != null) {
this.getGameRules().getRule(GameRules.RULE_ANNOUNCE_ADVANCEMENTS).set(properties.announcePlayerAchievements, this.overworld()); // CraftBukkit - per-world
}
@@ -419,7 +419,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
}
this.hasFullyShutdown = true; // Paper - Improved watchdog support
- System.exit(0); // CraftBukkit
+ System.exit(this.abnormalExit ? 70 : 0); // CraftBukkit // Paper
}
@Override
@@ -753,7 +753,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
@Override
public void stopServer() {
super.stopServer();
- Util.shutdownExecutors();
+ //Util.shutdownExecutors(); // Paper - moved into super
SkullBlockEntity.clear();
}
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
index 0a2f4af548561144607d24d3bd92270ce630fc95..42c1f5d0f384e0e4f348e161b4461858a6d84227 100644
--- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java
@@ -512,7 +512,7 @@ public abstract class PlayerList {
this.cserver.getPluginManager().callEvent(playerQuitEvent);
player.getBukkitEntity().disconnect(playerQuitEvent.getQuitMessage());
- player.doTick(); // SPIGOT-924
+ if (this.server.isSameThread()) player.doTick(); // SPIGOT-924 // Paper - don't tick during emergency shutdowns (Watchdog)
// CraftBukkit end
// Paper start - Configurable player collision; Remove from collideRule team if needed
diff --git a/net/minecraft/util/thread/BlockableEventLoop.java b/net/minecraft/util/thread/BlockableEventLoop.java
index 186c1b2e3599770385150eb7acdcd890aa5835eb..bfea9a2ae5e0bd5dae2873f715d192dfcbe97ee5 100644
--- a/net/minecraft/util/thread/BlockableEventLoop.java
+++ b/net/minecraft/util/thread/BlockableEventLoop.java
@@ -169,6 +169,6 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
public static boolean isNonRecoverable(Throwable error) {
return error instanceof ReportedException reportedException
? isNonRecoverable(reportedException.getCause())
- : error instanceof OutOfMemoryError || error instanceof StackOverflowError;
+ : error instanceof OutOfMemoryError || error instanceof StackOverflowError || error instanceof ThreadDeath; // Paper
}
}
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index 2c7b6034852216fc5aa5c3f42a70ebd8e8317a17..3bf79eedfc358f54bfe23b5a75b3ad121558f6c6 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -1498,6 +1498,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
try {
consumerEntity.accept(entity);
} catch (Throwable var6) {
+ if (var6 instanceof ThreadDeath) throw var6; // Paper
// Paper start - Prevent block entity and entity crashes
final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ());
MinecraftServer.LOGGER.error(msg, var6);
diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java
index c1ae7755e8d6fa8501d2210dab7605d993c55722..b10890c5a7e42163e419e74596b952525c3ed3eb 100644
--- a/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/net/minecraft/world/level/chunk/LevelChunk.java
@@ -929,6 +929,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
profilerFiller.pop();
} catch (Throwable var5) {
+ if (var5 instanceof ThreadDeath) throw var5; // Paper
// Paper start - Prevent block entity and entity crashes
final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ());
net.minecraft.server.MinecraftServer.LOGGER.error(msg, var5);