9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-19 15:09:25 +00:00

async saving player stats and advancements (#334)

* async saving player stats and advancements

* remove thread check

* fix interrupt

* longer wait IO tasks time

* safe replace

* delay join while saving player

* mark as experimental

---------

Co-authored-by: Taiyou06 <kaandindar21@gmail.com>
This commit is contained in:
hayanesuru
2025-06-08 12:18:01 +09:00
committed by GitHub
parent acf2c14f80
commit 107ae7954f
11 changed files with 583 additions and 150 deletions

View File

@@ -32,10 +32,10 @@ index 4f01b53bf801f99253efd27df6216912705d18af..82a1715fea41e6a41c4ff441ea89f424
level.addDuringTeleport(this); level.addDuringTeleport(this);
this.triggerDimensionChangeTriggers(serverLevel); this.triggerDimensionChangeTriggers(serverLevel);
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
index 75fb49f1596f475278d12c8c7aea9ad4952b6056..b17c8a2f5294ac28cc05fb05c84a041b2c6c8721 100644 index 52a0fa425a30caa2e592c0fdda44800da169c2a0..3f5c5b6234eb400838973c37e5a48bb121d1ff16 100644
--- a/net/minecraft/server/players/PlayerList.java --- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java +++ b/net/minecraft/server/players/PlayerList.java
@@ -955,11 +955,11 @@ public abstract class PlayerList { @@ -997,11 +997,11 @@ public abstract class PlayerList {
byte b = (byte)(keepInventory ? 1 : 0); byte b = (byte)(keepInventory ? 1 : 0);
ServerLevel serverLevel = serverPlayer.serverLevel(); ServerLevel serverLevel = serverPlayer.serverLevel();
LevelData levelData = serverLevel.getLevelData(); LevelData levelData = serverLevel.getLevelData();

View File

@@ -6,15 +6,213 @@ Subject: [PATCH] Nitori: Async playerdata saving
Original license: GPL v3 Original license: GPL v3
Original project: https://github.com/Gensokyo-Reimagined/Nitori Original project: https://github.com/Gensokyo-Reimagined/Nitori
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
index 00a82873d226f113278632a53c0faca420dd67d4..2c4423eb2d465c2782a8dab851619ce539f69ae8 100644
--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
@@ -586,7 +586,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
// Paper end - Optimize network
private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world
- private static int joinAttemptsThisTick; // Paper - Buffer joins to world
+ public static int joinAttemptsThisTick; // Paper - Buffer joins to world // Leaf - Async player IO
private static int currTick; // Paper - Buffer joins to world
private static int tickSecond; // Purpur - Max joins per second
public void tick() {
diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java
index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..d5196b181a0a633cb04ce18b0471cda2dcaa8816 100644
--- a/net/minecraft/server/PlayerAdvancements.java
+++ b/net/minecraft/server/PlayerAdvancements.java
@@ -111,6 +111,7 @@ public class PlayerAdvancements {
}
private void load(ServerAdvancementManager manager) {
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.blockAdvancements(player.getUUID(), player.getScoreboardName()); // Leaf - Async player IO
if (Files.isRegularFile(this.playerSavePath)) {
try (JsonReader jsonReader = new JsonReader(Files.newBufferedReader(this.playerSavePath, StandardCharsets.UTF_8))) {
jsonReader.setLenient(false);
@@ -133,12 +134,18 @@ public class PlayerAdvancements {
JsonElement jsonElement = this.codec.encodeStart(JsonOps.INSTANCE, this.asData()).getOrThrow();
try {
- FileUtil.createDirectoriesSafe(this.playerSavePath.getParent());
-
- try (Writer bufferedWriter = Files.newBufferedWriter(this.playerSavePath, StandardCharsets.UTF_8)) {
- GSON.toJson(jsonElement, GSON.newJsonWriter(bufferedWriter));
- }
- } catch (JsonIOException | IOException var7) {
+ // Leaf start - Async player IO
+ String content = GSON.toJson(jsonElement);
+ final Path path = this.playerSavePath;
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.submitAdvancements(
+ this.player.getUUID(),
+ this.player.getScoreboardName(),
+ () -> {
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.safeReplace(path, content);
+ return null;
+ });
+ } catch (JsonIOException /*| IOException*/ var7) {
+ // Leaf end - Async player IO
LOGGER.error("Couldn't save player advancements to {}", this.playerSavePath, var7);
}
}
diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
index 114b25f933c6a1b011523581a5a02a5a2c1e827e..3da6dad3dd0f4c5750609b382f47a6cd14f18e7a 100644
--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
@@ -79,6 +79,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
.expireAfterWrite(org.dreeam.leaf.config.modules.misc.Cache.cachePlayerProfileResultTimeout, java.util.concurrent.TimeUnit.MINUTES)
.build();
// Leaf end - Cache player profileResult
+ @Nullable public java.util.UUID[] duplicateDisconnect = null; // Leaf - Async player IO
public ServerLoginPacketListenerImpl(MinecraftServer server, Connection connection, boolean transferred) {
this.server = server;
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
index 75fb49f1596f475278d12c8c7aea9ad4952b6056..52a0fa425a30caa2e592c0fdda44800da169c2a0 100644
--- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java
@@ -782,6 +782,31 @@ public abstract class PlayerList {
// UserBanListEntry userBanListEntry = this.bans.get(gameProfile);
// Moved from processLogin
UUID uuid = gameProfile.getId();
+
+ // Leaf start - Async player IO
+ if (org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.enabled) {
+ if (org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.isSaving(uuid)) {
+ if (Connection.joinAttemptsThisTick != 0) Connection.joinAttemptsThisTick -= 1;
+ return null;
+ }
+ if (loginlistener.duplicateDisconnect != null
+ && loginlistener.duplicateDisconnect.length != 0) {
+ // check last one
+ var last = loginlistener.duplicateDisconnect[loginlistener.duplicateDisconnect.length - 1];
+ if (org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.isSaving(last)) {
+ if (Connection.joinAttemptsThisTick != 0) Connection.joinAttemptsThisTick -= 1;
+ return null;
+ }
+ for (UUID uuid1 : loginlistener.duplicateDisconnect) {
+ if (org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.isSaving(uuid1)) {
+ if (Connection.joinAttemptsThisTick != 0) Connection.joinAttemptsThisTick -= 1;
+ return null;
+ }
+ }
+ loginlistener.duplicateDisconnect = null;
+ }
+ }
+ // Leaf end - Async player IO
List<ServerPlayer> list = Lists.newArrayList();
ServerPlayer entityplayer;
@@ -793,6 +818,23 @@ public abstract class PlayerList {
}
}
+ // Leaf start - Async player IO
+ if (org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.enabled && !list.isEmpty()) {
+ loginlistener.duplicateDisconnect = new UUID[list.size()];
+ java.util.Iterator<ServerPlayer> iterator = list.iterator();
+
+ int index = 0;
+ while (iterator.hasNext()) {
+ entityplayer = iterator.next();
+ // this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved
+ entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause
+ loginlistener.duplicateDisconnect[index] = entityplayer.getUUID();
+ index++;
+ }
+ if (Connection.joinAttemptsThisTick != 0) Connection.joinAttemptsThisTick -= 1;
+ return null;
+ }
+ // Leaf end - Async player IO
java.util.Iterator iterator = list.iterator();
while (iterator.hasNext()) {
@@ -1582,7 +1624,7 @@ public abstract class PlayerList {
*/
// Leaf end - Remove useless creating stats json bases on player name logic
- serverStatsCounter = new ServerStatsCounter(this.server, file1);
+ serverStatsCounter = new ServerStatsCounter(this.server, file1, displayName, uuid);
// this.stats.put(uuid, serverStatsCounter); // CraftBukkit
}
diff --git a/net/minecraft/stats/ServerStatsCounter.java b/net/minecraft/stats/ServerStatsCounter.java
index b26dbe807e5cb0a42f6c06b933397902310e5616..35ad7f249cfb6f5c779136d96f3698ea4de1eb7c 100644
--- a/net/minecraft/stats/ServerStatsCounter.java
+++ b/net/minecraft/stats/ServerStatsCounter.java
@@ -39,12 +39,23 @@ public class ServerStatsCounter extends StatsCounter {
private final File file;
private final Set<Stat<?>> dirty = Sets.newHashSet();
+ // Leaf start - Async player IO
+ private final String name;
+ private final java.util.UUID uuid;
+ @Deprecated(forRemoval = true)
public ServerStatsCounter(MinecraftServer server, File file) {
+ throw new UnsupportedOperationException();
+ }
+ public ServerStatsCounter(MinecraftServer server, File file, String name, java.util.UUID uuid) {
+ this.name = name;
+ this.uuid = uuid;
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.blockStats(uuid, name);
+ // Leaf end - Async player IO
this.server = server;
this.file = file;
if (file.isFile()) {
try {
- this.parseLocal(server.getFixerUpper(), FileUtils.readFileToString(file));
+ this.parseLocal(server.getFixerUpper(), FileUtils.readFileToString(file, java.nio.charset.StandardCharsets.UTF_8)); // Leaf - UTF-8
} catch (IOException var4) {
LOGGER.error("Couldn't read statistics file {}", file, var4);
} catch (JsonParseException var5) {
@@ -66,11 +77,37 @@ public class ServerStatsCounter extends StatsCounter {
public void save() {
if (org.spigotmc.SpigotConfig.disableStatSaving) return; // Spigot
+ // Leaf start - Async player IO
+ Map<StatType<?>, JsonObject> map = Maps.newHashMap();
+ for (it.unimi.dsi.fastutil.objects.Object2IntMap.Entry<Stat<?>> entry : this.stats.object2IntEntrySet()) {
+ Stat<?> stat = entry.getKey();
+ map.computeIfAbsent(stat.getType(), type -> new JsonObject()).addProperty(getKey(stat).toString(), entry.getIntValue());
+ }
+ final File file = this.file;
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.submitStats(
+ uuid,
+ name,
+ () -> {
+ JsonObject jsonObject = new JsonObject();
+
+ for (Entry<StatType<?>, JsonObject> entry1 : map.entrySet()) {
+ jsonObject.add(BuiltInRegistries.STAT_TYPE.getKey(entry1.getKey()).toString(), entry1.getValue());
+ }
+
+ JsonObject jsonObject1 = new JsonObject();
+ jsonObject1.add("stats", jsonObject);
+ jsonObject1.addProperty("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion());
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.safeReplace(file.toPath(), jsonObject1.toString());
+ return null;
+ });
+ /*
try {
FileUtils.writeStringToFile(this.file, this.toJson());
} catch (IOException var2) {
LOGGER.error("Couldn't save stats", (Throwable)var2);
}
+ */
+ // Leaf end - Async player IO
}
@Override
diff --git a/net/minecraft/world/level/storage/LevelStorageSource.java b/net/minecraft/world/level/storage/LevelStorageSource.java diff --git a/net/minecraft/world/level/storage/LevelStorageSource.java b/net/minecraft/world/level/storage/LevelStorageSource.java
index de43e54698125ce9f319d4889dd49f7029fe95e0..742bd4b60321adc9e63c3de910ea95f4990b618d 100644 index de43e54698125ce9f319d4889dd49f7029fe95e0..360e54b87db68fad60cdec63af466765baae0a07 100644
--- a/net/minecraft/world/level/storage/LevelStorageSource.java --- a/net/minecraft/world/level/storage/LevelStorageSource.java
+++ b/net/minecraft/world/level/storage/LevelStorageSource.java +++ b/net/minecraft/world/level/storage/LevelStorageSource.java
@@ -520,15 +520,26 @@ public class LevelStorageSource { @@ -520,15 +520,24 @@ public class LevelStorageSource {
private void saveLevelData(CompoundTag tag) { private void saveLevelData(CompoundTag tag) {
Path path = this.levelDirectory.path(); Path path = this.levelDirectory.path();
+ // Leaf start - Async playerdata saving + // Leaf start - Async player IO
+ // Save level.dat asynchronously + // Save level.dat asynchronously
+ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536); + var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536);
try { try {
@@ -28,38 +226,36 @@ index de43e54698125ce9f319d4889dd49f7029fe95e0..742bd4b60321adc9e63c3de910ea95f4
- LevelStorageSource.LOGGER.error("Failed to save level {}", path, var6); - LevelStorageSource.LOGGER.error("Failed to save level {}", path, var6);
+ LevelStorageSource.LOGGER.error("Failed to encode level {}", path, var6); + LevelStorageSource.LOGGER.error("Failed to encode level {}", path, var6);
} }
+ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> { + org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.saveLevelData(path, () -> {
+ try { + try {
+ Path path1 = Files.createTempFile(path, "level", ".dat"); + Path old = this.levelDirectory.oldDataFile();
+ org.apache.commons.io.FileUtils.writeByteArrayToFile(path1.toFile(), nbtBytes.array, 0, nbtBytes.length, false); + Path current = this.levelDirectory.dataFile();
+ Path path2 = this.levelDirectory.oldDataFile(); + org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.safeReplaceBackup(current, old, nbtBytes.array, 0, nbtBytes.length);
+ Path path3 = this.levelDirectory.dataFile();
+ Util.safeReplaceFile(path3, path1, path2);
+ } catch (Exception var6) { + } catch (Exception var6) {
+ LevelStorageSource.LOGGER.error("Failed to save level {}", path, var6); + LevelStorageSource.LOGGER.error("Failed to save level.dat {}", path, var6);
+ } + }
+ }); + });
+ // Leaf end - Async playerdata saving + // Leaf end - Async player IO
} }
public Optional<Path> getIconFile() { public Optional<Path> getIconFile() {
@@ -645,6 +654,7 @@ public class LevelStorageSource {
@Override
public void close() throws IOException {
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.saveLevelData(this.levelDirectory.path(), null); // Leaf - Async player IO
this.lock.close();
}
diff --git a/net/minecraft/world/level/storage/PlayerDataStorage.java b/net/minecraft/world/level/storage/PlayerDataStorage.java diff --git a/net/minecraft/world/level/storage/PlayerDataStorage.java b/net/minecraft/world/level/storage/PlayerDataStorage.java
index c44110b123ba5912af18faf0065e9ded780da9b7..fd8b4832c8b4a52bd8f9b3ea59111af85127b573 100644 index c44110b123ba5912af18faf0065e9ded780da9b7..2eae5ccb37b942b94964c28391b96989ae85b072 100644
--- a/net/minecraft/world/level/storage/PlayerDataStorage.java --- a/net/minecraft/world/level/storage/PlayerDataStorage.java
+++ b/net/minecraft/world/level/storage/PlayerDataStorage.java +++ b/net/minecraft/world/level/storage/PlayerDataStorage.java
@@ -25,6 +25,7 @@ public class PlayerDataStorage { @@ -34,19 +34,37 @@ public class PlayerDataStorage {
private final File playerDir;
protected final DataFixer fixerUpper;
private static final DateTimeFormatter FORMATTER = FileNameDateFormatter.create();
+ private final java.util.Map<java.util.UUID, java.util.concurrent.Future<?>> savingLocks = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(); // Leaf - Async playerdata saving
public PlayerDataStorage(LevelStorageSource.LevelStorageAccess levelStorageAccess, DataFixer fixerUpper) {
this.fixerUpper = fixerUpper;
@@ -34,19 +35,82 @@ public class PlayerDataStorage {
public void save(Player player) { public void save(Player player) {
if (org.spigotmc.SpigotConfig.disablePlayerDataSaving) return; // Spigot if (org.spigotmc.SpigotConfig.disablePlayerDataSaving) return; // Spigot
+ // Leaf start - Async playerdata saving + // Leaf start - Async player IO
+ CompoundTag compoundTag; + CompoundTag compoundTag;
try { try {
- CompoundTag compoundTag = player.saveWithoutId(new CompoundTag()); - CompoundTag compoundTag = player.saveWithoutId(new CompoundTag());
@@ -77,105 +273,60 @@ index c44110b123ba5912af18faf0065e9ded780da9b7..fd8b4832c8b4a52bd8f9b3ea59111af8
+ return; + return;
} }
+ save(player.getScoreboardName(), player.getUUID(), player.getStringUUID(), compoundTag); + save(player.getScoreboardName(), player.getUUID(), player.getStringUUID(), compoundTag);
+ // Leaf end - Async playerdata saving + // Leaf end - Async player IO
} }
+ // Leaf start - Async playerdata saving + // Leaf start - Async player IO
+ public void save(String playerName, java.util.UUID uniqueId, String stringId, CompoundTag compoundTag) { + public void save(String playerName, java.util.UUID uuid, String stringId, CompoundTag compoundTag) {
+ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536); + final File playerDir = this.playerDir;
+ try { + org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.submitEntity(
+ NbtIo.writeCompressed(compoundTag, nbtBytes); + uuid,
+ } catch (Exception exception) { + playerName,
+ LOGGER.warn("Failed to encode player data for {}", stringId, exception); + () -> {
+ } + var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536);
+ lockFor(uniqueId, playerName); + NbtIo.writeCompressed(compoundTag, nbtBytes);
+ synchronized (PlayerDataStorage.this) { + Path path = playerDir.toPath();
+ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> {
+ try {
+ Path path = this.playerDir.toPath();
+ Path path1 = Files.createTempFile(path, stringId + "-", ".dat");
+ org.apache.commons.io.FileUtils.writeByteArrayToFile(path1.toFile(), nbtBytes.array, 0, nbtBytes.length, false);
+ Path path2 = path.resolve(stringId + ".dat");
+ Path path3 = path.resolve(stringId + ".dat_old");
+ Util.safeReplaceFile(path2, path1, path3);
+ } catch (Exception var7) {
+ LOGGER.warn("Failed to save player data for {}", playerName, var7);
+ } finally {
+ synchronized (PlayerDataStorage.this) {
+ savingLocks.remove(uniqueId);
+ }
+ }
+ }).ifPresent(future -> savingLocks.put(uniqueId, future));
+ }
+ }
+ +
+ private void lockFor(java.util.UUID uniqueId, String playerName) { + Path current = path.resolve(stringId + ".dat");
+ java.util.concurrent.Future<?> fut; + Path old = path.resolve(stringId + ".dat_old");
+ synchronized (this) { + org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.safeReplaceBackup(current, old, nbtBytes.array, 0, nbtBytes.length);
+ fut = savingLocks.get(uniqueId); + return null;
+ } + });
+ if (fut == null) {
+ return;
+ }
+ while (true) {
+ try {
+ fut.get(10_000L, java.util.concurrent.TimeUnit.MILLISECONDS);
+ break;
+ } catch (InterruptedException ignored) {
+ } catch (java.util.concurrent.ExecutionException
+ | java.util.concurrent.TimeoutException exception) {
+ LOGGER.warn("Failed to save player data for {}", playerName, exception);
+
+ String threadDump = "";
+ var threadMXBean = java.lang.management.ManagementFactory.getThreadMXBean();
+ for (var threadInfo : threadMXBean.dumpAllThreads(true, true)) {
+ if (threadInfo.getThreadName().equals("Leaf IO Thread")) {
+ threadDump = threadInfo.toString();
+ break;
+ }
+ }
+ LOGGER.warn(threadDump);
+ fut.cancel(true);
+ break;
+ } finally {
+ savingLocks.remove(uniqueId);
+ }
+ }
+ } + }
+ // Leaf end - Async playerdata saving + // Leaf end - Async player IO
+ +
private void backup(String name, String stringUuid, String suffix) { // CraftBukkit private void backup(String name, String stringUuid, String suffix) { // CraftBukkit
Path path = this.playerDir.toPath(); Path path = this.playerDir.toPath();
Path path1 = path.resolve(stringUuid + suffix); // CraftBukkit Path path1 = path.resolve(stringUuid + suffix); // CraftBukkit
@@ -60,7 +124,13 @@ public class PlayerDataStorage { @@ -60,7 +78,13 @@ public class PlayerDataStorage {
} }
} }
- private Optional<CompoundTag> load(String name, String stringUuid, String suffix) { // CraftBukkit - private Optional<CompoundTag> load(String name, String stringUuid, String suffix) { // CraftBukkit
+ // Leaf start - Async playerdata saving + // Leaf start - Async player IO
+ private Optional<CompoundTag> load(String name, String stringUuid, String suffix) { + private Optional<CompoundTag> load(String name, String stringUuid, String suffix) {
+ return load(name, stringUuid, suffix, java.util.UUID.fromString(stringUuid)); + return load(name, stringUuid, suffix, java.util.UUID.fromString(stringUuid));
+ } + }
+ private Optional<CompoundTag> load(String name, String stringUuid, String suffix, java.util.UUID playerUuid) { // CraftBukkit + private Optional<CompoundTag> load(String name, String stringUuid, String suffix, java.util.UUID playerUuid) { // CraftBukkit
+ lockFor(playerUuid, name); + org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.blockEntity(playerUuid, name);
+ // Leaf end - Async playerdata saving + // Leaf end - Async player IO
File file = new File(this.playerDir, stringUuid + suffix); // CraftBukkit File file = new File(this.playerDir, stringUuid + suffix); // CraftBukkit
// Spigot start // Spigot start
boolean usingWrongFile = false; boolean usingWrongFile = false;
@@ -91,7 +161,7 @@ public class PlayerDataStorage { @@ -91,7 +115,7 @@ public class PlayerDataStorage {
public Optional<CompoundTag> load(Player player) { public Optional<CompoundTag> load(Player player) {
// CraftBukkit start // CraftBukkit start
- return this.load(player.getName().getString(), player.getStringUUID()).map((tag) -> { - return this.load(player.getName().getString(), player.getStringUUID()).map((tag) -> {
+ return this.load(player.getName().getString(), player.getStringUUID(), player.getUUID()).map((tag) -> { // Leaf - Async playerdata saving + return this.load(player.getName().getString(), player.getStringUUID(), player.getUUID()).map((tag) -> { // Leaf - Async player IO
if (player instanceof ServerPlayer serverPlayer) { if (player instanceof ServerPlayer serverPlayer) {
CraftPlayer craftPlayer = serverPlayer.getBukkitEntity(); CraftPlayer craftPlayer = serverPlayer.getBukkitEntity();
// Only update first played if it is older than the one we have // Only update first played if it is older than the one we have
@@ -106,20 +176,25 @@ public class PlayerDataStorage { @@ -106,20 +130,25 @@ public class PlayerDataStorage {
}); });
} }
+ // Leaf start - Async playerdata saving + // Leaf start - Async player IO
public Optional<CompoundTag> load(String name, String uuid) { public Optional<CompoundTag> load(String name, String uuid) {
+ return this.load(name, uuid, java.util.UUID.fromString(uuid)); + return this.load(name, uuid, java.util.UUID.fromString(uuid));
+ } + }
@@ -195,7 +346,7 @@ index c44110b123ba5912af18faf0065e9ded780da9b7..fd8b4832c8b4a52bd8f9b3ea59111af8
return compoundTag; return compoundTag;
}); });
} }
+ // Leaf end - Async playerdata saving + // Leaf end - Async player IO
// CraftBukkit start // CraftBukkit start
public File getPlayerDir() { public File getPlayerDir() {

View File

@@ -6,10 +6,10 @@ Subject: [PATCH] SparklyPaper: Skip dirty stats copy when requesting player
diff --git a/net/minecraft/stats/ServerStatsCounter.java b/net/minecraft/stats/ServerStatsCounter.java diff --git a/net/minecraft/stats/ServerStatsCounter.java b/net/minecraft/stats/ServerStatsCounter.java
index b26dbe807e5cb0a42f6c06b933397902310e5616..ce89060bd01b253af7577fd0e6c03fc95f046b91 100644 index 35ad7f249cfb6f5c779136d96f3698ea4de1eb7c..523dc12a8866a199eac1b2f418bf206f068ba80c 100644
--- a/net/minecraft/stats/ServerStatsCounter.java --- a/net/minecraft/stats/ServerStatsCounter.java
+++ b/net/minecraft/stats/ServerStatsCounter.java +++ b/net/minecraft/stats/ServerStatsCounter.java
@@ -81,11 +81,15 @@ public class ServerStatsCounter extends StatsCounter { @@ -118,11 +118,15 @@ public class ServerStatsCounter extends StatsCounter {
this.dirty.add(stat); this.dirty.add(stat);
} }
@@ -25,7 +25,7 @@ index b26dbe807e5cb0a42f6c06b933397902310e5616..ce89060bd01b253af7577fd0e6c03fc9
public void parseLocal(DataFixer fixerUpper, String json) { public void parseLocal(DataFixer fixerUpper, String json) {
try { try {
@@ -194,10 +198,12 @@ public class ServerStatsCounter extends StatsCounter { @@ -231,10 +235,12 @@ public class ServerStatsCounter extends StatsCounter {
public void sendStats(ServerPlayer player) { public void sendStats(ServerPlayer player) {
Object2IntMap<Stat<?>> map = new Object2IntOpenHashMap<>(); Object2IntMap<Stat<?>> map = new Object2IntOpenHashMap<>();

View File

@@ -458,7 +458,7 @@ index 63ff20f467c7508486a8f274442269b90faea108..15de8904a43c0ee1e6d55d511ebd84df
} }
// CraftBukkit end // CraftBukkit end
diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java
index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436b3c883d0 100644 index d5196b181a0a633cb04ce18b0471cda2dcaa8816..46433f0e6b37d31ec5468b3f4a5b2524d3cb29ed 100644
--- a/net/minecraft/server/PlayerAdvancements.java --- a/net/minecraft/server/PlayerAdvancements.java
+++ b/net/minecraft/server/PlayerAdvancements.java +++ b/net/minecraft/server/PlayerAdvancements.java
@@ -53,8 +53,11 @@ public class PlayerAdvancements { @@ -53,8 +53,11 @@ public class PlayerAdvancements {
@@ -483,7 +483,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
this.isFirstPacket = true; this.isFirstPacket = true;
this.lastSelectedTab = null; this.lastSelectedTab = null;
this.tree = manager.tree(); this.tree = manager.tree();
@@ -151,7 +155,7 @@ public class PlayerAdvancements { @@ -158,7 +162,7 @@ public class PlayerAdvancements {
if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.ignoredAdvancements) LOGGER.warn("Ignored advancement '{}' in progress file {} - it doesn't exist anymore?", path, this.playerSavePath); // Gale - Purpur - do not log ignored advancements if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.ignoredAdvancements) LOGGER.warn("Ignored advancement '{}' in progress file {} - it doesn't exist anymore?", path, this.playerSavePath); // Gale - Purpur - do not log ignored advancements
} else { } else {
this.startProgress(advancementHolder, progress); this.startProgress(advancementHolder, progress);
@@ -492,7 +492,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
this.markForVisibilityUpdate(advancementHolder); this.markForVisibilityUpdate(advancementHolder);
} }
}); });
@@ -183,10 +187,12 @@ public class PlayerAdvancements { @@ -190,10 +194,12 @@ public class PlayerAdvancements {
return false; return false;
} }
// Paper end - Add PlayerAdvancementCriterionGrantEvent // Paper end - Add PlayerAdvancementCriterionGrantEvent
@@ -509,7 +509,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
// Paper start - Add Adventure message to PlayerAdvancementDoneEvent // Paper start - Add Adventure message to PlayerAdvancementDoneEvent
final net.kyori.adventure.text.Component message = advancement.value().display().flatMap(info -> { final net.kyori.adventure.text.Component message = advancement.value().display().flatMap(info -> {
return java.util.Optional.ofNullable( return java.util.Optional.ofNullable(
@@ -220,12 +226,14 @@ public class PlayerAdvancements { @@ -227,12 +233,14 @@ public class PlayerAdvancements {
AdvancementProgress orStartProgress = this.getOrStartProgress(advancement); AdvancementProgress orStartProgress = this.getOrStartProgress(advancement);
boolean isDone = orStartProgress.isDone(); boolean isDone = orStartProgress.isDone();
if (orStartProgress.revokeProgress(criterionKey)) { if (orStartProgress.revokeProgress(criterionKey)) {
@@ -527,7 +527,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
this.markForVisibilityUpdate(advancement); this.markForVisibilityUpdate(advancement);
} }
@@ -271,7 +279,10 @@ public class PlayerAdvancements { @@ -278,7 +286,10 @@ public class PlayerAdvancements {
} }
public void flushDirty(ServerPlayer serverPlayer) { public void flushDirty(ServerPlayer serverPlayer) {
@@ -539,7 +539,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
Map<ResourceLocation, AdvancementProgress> map = new HashMap<>(); Map<ResourceLocation, AdvancementProgress> map = new HashMap<>();
Set<AdvancementHolder> set = new java.util.TreeSet<>(java.util.Comparator.comparing(adv -> adv.id().toString())); // Paper - Changed from HashSet to TreeSet ordered alphabetically. Set<AdvancementHolder> set = new java.util.TreeSet<>(java.util.Comparator.comparing(adv -> adv.id().toString())); // Paper - Changed from HashSet to TreeSet ordered alphabetically.
Set<ResourceLocation> set1 = new HashSet<>(); Set<ResourceLocation> set1 = new HashSet<>();
@@ -279,16 +290,24 @@ public class PlayerAdvancements { @@ -286,16 +297,24 @@ public class PlayerAdvancements {
for (AdvancementNode advancementNode : this.rootsToUpdate) { for (AdvancementNode advancementNode : this.rootsToUpdate) {
this.updateTreeVisibility(advancementNode, set, set1); this.updateTreeVisibility(advancementNode, set, set1);
} }
@@ -568,7 +568,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
if (!map.isEmpty() || !set.isEmpty() || !set1.isEmpty()) { if (!map.isEmpty() || !set.isEmpty() || !set1.isEmpty()) {
serverPlayer.connection.send(new ClientboundUpdateAdvancementsPacket(this.isFirstPacket, set, set1, map)); serverPlayer.connection.send(new ClientboundUpdateAdvancementsPacket(this.isFirstPacket, set, set1, map));
} }
@@ -331,10 +350,13 @@ public class PlayerAdvancements { @@ -338,10 +357,13 @@ public class PlayerAdvancements {
AdvancementHolder advancementHolder = node.holder(); AdvancementHolder advancementHolder = node.holder();
if (visible) { if (visible) {
if (this.visible.add(advancementHolder)) { if (this.visible.add(advancementHolder)) {
@@ -900,7 +900,7 @@ index 75fb49f1596f475278d12c8c7aea9ad4952b6056..de601491b7ecb83f1bb64a95989d6ed4
player.isRealPlayer = true; // Paper player.isRealPlayer = true; // Paper
player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed
GameProfile gameProfile = player.getGameProfile(); GameProfile gameProfile = player.getGameProfile();
@@ -891,6 +893,15 @@ public abstract class PlayerList { @@ -933,6 +935,15 @@ public abstract class PlayerList {
return this.respawn(player, keepInventory, reason, eventReason, null); return this.respawn(player, keepInventory, reason, eventReason, null);
} }
public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, org.bukkit.Location location) { public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, org.bukkit.Location location) {
@@ -916,7 +916,7 @@ index 75fb49f1596f475278d12c8c7aea9ad4952b6056..de601491b7ecb83f1bb64a95989d6ed4
player.stopRiding(); // CraftBukkit player.stopRiding(); // CraftBukkit
this.players.remove(player); this.players.remove(player);
this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
@@ -902,6 +913,7 @@ public abstract class PlayerList { @@ -944,6 +955,7 @@ public abstract class PlayerList {
ServerPlayer serverPlayer = player; ServerPlayer serverPlayer = player;
Level fromWorld = player.level(); Level fromWorld = player.level();
player.wonGame = false; player.wonGame = false;

View File

@@ -110,10 +110,10 @@ index 2e9eb04c7c4342393c05339906c267bca9ff29b1..53b9daa909c2b89046d5af515e17afe0
try { try {
PlayerList playerList = this.server.getPlayerList(); PlayerList playerList = this.server.getPlayerList();
diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
index 114b25f933c6a1b011523581a5a02a5a2c1e827e..5907f1c75002be5e2ef1f9875974e665f964db7a 100644 index 3da6dad3dd0f4c5750609b382f47a6cd14f18e7a..c1e4ea2f28aba688b5b61e5bea2c295e9f219aba 100644
--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java --- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
@@ -494,11 +494,31 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, @@ -495,11 +495,31 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY); this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
} }

View File

@@ -5,7 +5,7 @@ Subject: [PATCH] Async playerdata saving
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java diff --git a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java
index f2d87c12dd19210ce7e2147fada5c10191008632..14da4c731391f69fef104b6b3b7f2f977fe5ee95 100644 index f2d87c12dd19210ce7e2147fada5c10191008632..ad66046d31c24ba2a7d2b115f6c70adb95b9735b 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java --- a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java
@@ -207,7 +207,7 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa @@ -207,7 +207,7 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa
@@ -13,7 +13,7 @@ index f2d87c12dd19210ce7e2147fada5c10191008632..14da4c731391f69fef104b6b3b7f2f97
private CompoundTag getData() { private CompoundTag getData() {
- return this.storage.load(this.profile.getName(), this.profile.getId().toString()).orElse(null); - return this.storage.load(this.profile.getName(), this.profile.getId().toString()).orElse(null);
+ return this.storage.load(this.profile.getName(), this.profile.getId().toString(), this.profile.getId()).orElse(null); // Leaf - Async playerdata saving + return this.storage.load(this.profile.getName(), this.profile.getId().toString(), this.profile.getId()).orElse(null); // Leaf - Async player IO
} }
private CompoundTag getBukkitData() { private CompoundTag getBukkitData() {
@@ -31,7 +31,7 @@ index f2d87c12dd19210ce7e2147fada5c10191008632..14da4c731391f69fef104b6b3b7f2f97
- } catch (java.io.IOException e) { - } catch (java.io.IOException e) {
- e.printStackTrace(); - e.printStackTrace();
- } - }
+ server.console.playerDataStorage.save(this.getName(), this.getUniqueId(), this.getUniqueId().toString(), compoundTag); // Leaf - Async playerdata saving + server.console.playerDataStorage.save(this.getName(), this.getUniqueId(), this.getUniqueId().toString(), compoundTag); // Leaf - Async player IO
} }
// Purpur end - OfflinePlayer API // Purpur end - OfflinePlayer API
} }

View File

@@ -1,37 +0,0 @@
package org.dreeam.leaf.async;
import net.minecraft.Util;
import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class AsyncPlayerDataSaving {
public static final ExecutorService IO_POOL = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new com.google.common.util.concurrent.ThreadFactoryBuilder()
.setPriority(Thread.NORM_PRIORITY - 2)
.setNameFormat("Leaf IO Thread")
.setUncaughtExceptionHandler(Util::onThreadException)
.build(),
new ThreadPoolExecutor.DiscardPolicy()
);
private AsyncPlayerDataSaving() {
}
public static Optional<Future<?>> submit(Runnable runnable) {
if (!AsyncPlayerDataSave.enabled) {
runnable.run();
return Optional.empty();
} else {
return Optional.of(IO_POOL.submit(runnable));
}
}
}

View File

@@ -1,6 +1,7 @@
package org.dreeam.leaf.async; package org.dreeam.leaf.async;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import org.dreeam.leaf.async.storage.AsyncPlayerDataSaving;
import org.dreeam.leaf.async.tracker.MultithreadedTracker; import org.dreeam.leaf.async.tracker.MultithreadedTracker;
public class ShutdownExecutors { public class ShutdownExecutors {

View File

@@ -0,0 +1,307 @@
package org.dreeam.leaf.async.storage;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.Util;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.*;
public class AsyncPlayerDataSaving {
public static final AsyncPlayerDataSaving INSTANCE = new AsyncPlayerDataSaving();
private static final Logger LOGGER = LogManager.getLogger("Leaf Async Player IO");
public static ExecutorService IO_POOL = null;
private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
.appendValue(ChronoField.DAY_OF_MONTH, 2)
.appendValue(ChronoField.HOUR_OF_DAY, 2)
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
.appendValue(ChronoField.NANO_OF_SECOND, 9)
.toFormatter();
private record SaveTask(Ty ty, Callable<Void> callable, String name, UUID uuid) implements Runnable {
@Override
public void run() {
try {
callable.call();
} catch (Exception e) {
LOGGER.error("Failed to save player {} data for {}", ty, name, e);
} finally {
switch (ty) {
case ENTITY -> INSTANCE.entityFut.remove(uuid);
case STATS -> INSTANCE.statsFut.remove(uuid);
case ADVANCEMENTS -> INSTANCE.advancementsFut.remove(uuid);
}
}
}
}
private enum Ty {
ENTITY,
STATS,
ADVANCEMENTS,
}
// use same lock
private final Object2ObjectMap<UUID, Future<?>> entityFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
private final Object2ObjectMap<UUID, Future<?>> statsFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
private final Object2ObjectMap<UUID, Future<?>> advancementsFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
private final Object2ObjectMap<Path, Future<?>> levelDatFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
private AsyncPlayerDataSaving() {
}
public static void init() {
if (AsyncPlayerDataSaving.IO_POOL != null) {
throw new IllegalStateException("Already initialized");
}
AsyncPlayerDataSaving.IO_POOL = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setPriority(Thread.NORM_PRIORITY - 2)
.setNameFormat("Leaf Async Player IO Thread")
.setUncaughtExceptionHandler(Util::onThreadException)
.build(),
new ThreadPoolExecutor.DiscardPolicy()
);
}
public void saveLevelData(Path path, @Nullable Runnable runnable) {
if (!AsyncPlayerDataSave.enabled) {
if (runnable != null) {
runnable.run();
}
return;
}
var fut = levelDatFut.get(path);
if (fut != null) {
try {
while (true) {
try {
fut.get();
break;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (ExecutionException e) {
LOGGER.error("Failed to save level.dat for {}", path, e);
} finally {
levelDatFut.remove(path);
}
}
if (runnable != null) {
levelDatFut.put(path, IO_POOL.submit(() -> {
try {
runnable.run();
} catch (Exception e) {
LOGGER.error(e);
} finally {
levelDatFut.remove(path);
}
}));
}
}
public boolean isSaving(UUID uuid) {
var entity = entityFut.get(uuid);
var advancements = advancementsFut.get(uuid);
var stats = statsFut.get(uuid);
return entity != null || advancements != null || stats != null;
}
public void submitStats(UUID uuid, String playerName, Callable<Void> callable) {
submit(Ty.STATS, uuid, playerName, callable);
}
public void submitEntity(UUID uuid, String playerName, Callable<Void> callable) {
submit(Ty.ENTITY, uuid, playerName, callable);
}
public void submitAdvancements(UUID uuid, String playerName, Callable<Void> callable) {
submit(Ty.ADVANCEMENTS, uuid, playerName, callable);
}
private void submit(Ty type, UUID uuid, String playerName, Callable<Void> callable) {
if (!AsyncPlayerDataSave.enabled) {
try {
callable.call();
} catch (Exception e) {
LOGGER.error("Failed to save player {} data for {}", type, playerName, e);
}
return;
}
block(type, uuid, playerName);
var fut = IO_POOL.submit(new SaveTask(type, callable, playerName, uuid));
switch (type) {
case ENTITY -> entityFut.put(uuid, fut);
case ADVANCEMENTS -> advancementsFut.put(uuid, fut);
case STATS -> statsFut.put(uuid, fut);
}
}
public void blockStats(UUID uuid, String playerName) {
block(Ty.STATS, uuid, playerName);
}
public void blockEntity(UUID uuid, String playerName) {
block(Ty.ENTITY, uuid, playerName);
}
public void blockAdvancements(UUID uuid, String playerName) {
block(Ty.ADVANCEMENTS, uuid, playerName);
}
private void block(Ty type, UUID uuid, String playerName) {
if (!AsyncPlayerDataSave.enabled) {
return;
}
Future<?> fut = switch (type) {
case ENTITY -> entityFut.get(uuid);
case ADVANCEMENTS -> advancementsFut.get(uuid);
case STATS -> statsFut.get(uuid);
};
if (fut == null) {
return;
}
try {
while (true) {
try {
fut.get();
break;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (ExecutionException exception) {
LOGGER.warn("Failed to save player {} data for {}", type, playerName, exception);
fut.cancel(true);
} finally {
switch (type) {
case ENTITY -> entityFut.remove(uuid);
case ADVANCEMENTS -> advancementsFut.remove(uuid);
case STATS -> statsFut.remove(uuid);
}
}
}
private static final StandardCopyOption[] ATOMIC_MOVE = new StandardCopyOption[]{StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING};
private static final StandardCopyOption[] NO_ATOMIC_MOVE = new StandardCopyOption[]{StandardCopyOption.REPLACE_EXISTING};
public static void safeReplace(Path current, String content) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
safeReplace(current, bytes, 0, bytes.length);
}
@SuppressWarnings("unused")
public static void safeReplaceBackup(Path current, Path old, String content) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
safeReplaceBackup(current, old, bytes, 0, bytes.length);
}
public static void safeReplace(Path current, byte[] bytes, int offset, int length) {
File latest = writeTempFile(current, bytes, offset, length);
Objects.requireNonNull(latest);
for (int i = 1; i <= 10; i++) {
try {
try {
Files.move(latest.toPath(), current, ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(latest.toPath(), current, NO_ATOMIC_MOVE);
}
break;
} catch (IOException e) {
LOGGER.error("Failed move {} to {} retries ({} / 10)", latest, current, i, e);
}
}
}
public static void safeReplaceBackup(Path current, Path backup, byte[] bytes, int offset, int length) {
File latest = writeTempFile(current, bytes, offset, length);
Objects.requireNonNull(latest);
for (int i = 1; i <= 10; i++) {
try {
try {
Files.move(current, backup, ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(current, backup, NO_ATOMIC_MOVE);
}
break;
} catch (IOException e) {
LOGGER.error("Failed move {} to {} retries ({} / 10)", current, backup, i, e);
}
}
for (int i = 1; i <= 10; i++) {
try {
try {
Files.move(latest.toPath(), current, ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(latest.toPath(), current, NO_ATOMIC_MOVE);
}
break;
} catch (IOException e) {
LOGGER.error("Failed move {} to {} retries ({} / 10)", latest, current, i, e);
}
}
}
private static File writeTempFile(Path current, byte[] bytes, int offset, int length) {
Path dir = current.getParent();
for (int i = 1; i <= 10; i++) {
File temp = null;
try {
if (!dir.toFile().isDirectory()) {
Files.createDirectories(dir);
}
temp = tempFileDateTime(current).toFile();
if (temp.exists()) {
throw new FileAlreadyExistsException(temp.getPath());
}
// sync content and metadata to device
try (RandomAccessFile stream = new RandomAccessFile(temp, "rws")) {
stream.write(bytes, offset, length);
}
return temp;
} catch (IOException e) {
LOGGER.error("Failed write {} retries ({} / 10)", temp == null ? current : temp, i, e);
}
}
return null;
}
private static Path tempFileDateTime(Path path) {
String now = LocalDateTime.now().format(FORMATTER);
String last = path.getFileName().toString();
int dot = last.lastIndexOf('.');
String base = (dot == -1) ? last : last.substring(0, dot);
String ext = (dot == -1) ? "" : last.substring(dot);
String newExt = switch (ext) {
case ".json", ".dat" -> ext;
default -> ".temp";
};
return path.resolveSibling(base + "-" + now + newExt);
}
}

View File

@@ -1,7 +1,9 @@
package org.dreeam.leaf.config.modules.async; package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.async.storage.AsyncPlayerDataSaving;
import org.dreeam.leaf.config.ConfigModules; import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory; import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.Experimental;
public class AsyncPlayerDataSave extends ConfigModules { public class AsyncPlayerDataSave extends ConfigModules {
@@ -9,7 +11,9 @@ public class AsyncPlayerDataSave extends ConfigModules {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-playerdata-save"; return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-playerdata-save";
} }
@Experimental
public static boolean enabled = false; public static boolean enabled = false;
private static boolean asyncPlayerDataSaveInitialized;
@Override @Override
public void onLoaded() { public void onLoaded() {
@@ -18,6 +22,13 @@ public class AsyncPlayerDataSave extends ConfigModules {
""" """
异步保存玩家数据."""); 异步保存玩家数据.""");
if (asyncPlayerDataSaveInitialized) {
config.getConfigSection(getBasePath());
return;
}
asyncPlayerDataSaveInitialized = true;
enabled = config.getBoolean(getBasePath() + ".enabled", enabled); enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
if (enabled) AsyncPlayerDataSaving.init();
} }
} }

View File

@@ -14,7 +14,7 @@ public class BotStatsCounter extends ServerStatsCounter {
private static final File UNKOWN_FILE = new File("BOT_STATS_REMOVE_THIS"); private static final File UNKOWN_FILE = new File("BOT_STATS_REMOVE_THIS");
public BotStatsCounter(MinecraftServer server) { public BotStatsCounter(MinecraftServer server) {
super(server, UNKOWN_FILE); super(server, UNKOWN_FILE, "", net.minecraft.Util.NIL_UUID); // Leaf
} }
@Override @Override