From d7801dd64f358e62e1b683bea8ab988ab7a64363 Mon Sep 17 00:00:00 2001 From: Christian Koop Date: Mon, 12 Jul 2021 16:09:21 +0200 Subject: [PATCH] Hotfix async vouchers.yml write task as large files cause lag [SD-8155] I've some a TODO/FIXME explaining what to do. Wanna focus on bringing the tickets down first --- .../songoda/epicvouchers/EpicVouchers.java | 227 +++++++++++------- .../songoda/epicvouchers/utils/Callback.java | 6 + .../epicvouchers/utils/ThreadSync.java | 31 +++ 3 files changed, 172 insertions(+), 92 deletions(-) create mode 100644 src/main/java/com/songoda/epicvouchers/utils/Callback.java create mode 100644 src/main/java/com/songoda/epicvouchers/utils/ThreadSync.java diff --git a/src/main/java/com/songoda/epicvouchers/EpicVouchers.java b/src/main/java/com/songoda/epicvouchers/EpicVouchers.java index 686f3f7..6c7642d 100644 --- a/src/main/java/com/songoda/epicvouchers/EpicVouchers.java +++ b/src/main/java/com/songoda/epicvouchers/EpicVouchers.java @@ -20,6 +20,8 @@ import com.songoda.epicvouchers.libraries.inventory.IconInv; import com.songoda.epicvouchers.listeners.PlayerCommandListener; import com.songoda.epicvouchers.listeners.PlayerInteractListener; import com.songoda.epicvouchers.settings.Settings; +import com.songoda.epicvouchers.utils.Callback; +import com.songoda.epicvouchers.utils.ThreadSync; import com.songoda.epicvouchers.voucher.CoolDownManager; import com.songoda.epicvouchers.voucher.Voucher; import com.songoda.epicvouchers.voucher.VoucherExecutor; @@ -101,121 +103,166 @@ public class EpicVouchers extends SongodaPlugin { @Override public void onDataLoad() { - if (!new File(this.getDataFolder(), "vouchers.yml").exists()) + if (!new File(this.getDataFolder(), "vouchers.yml").exists()) { saveResource("vouchers.yml", false); - vouchersConfig.load(); + } + + synchronized (vouchersConfig) { + vouchersConfig.load(); + } loadVouchersFromFile(); connections.openMySQL(); - Bukkit.getScheduler().scheduleSyncRepeatingTask(this, this::saveVouchers, 6000, 6000); + // FIXME: Config system needs to be greatly redone and only write changes when changes were made - Maybe even split it into multiple smaler files + // Issue https://support.songoda.com/browse/SD-8155 has been hotfixed by writing changes to the file async and blocking the main thread when needed. This requires the use of `synchronized` + // and expects every modifying code to use it (thread-safety) + // Large vouchers.yml files cause huge performance problems otherwise... + // Example file for testing: https://support.songoda.com/secure/attachment/17258/17258_vouchers.yml + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, + () -> saveVouchersAsync(ex -> { + if (ex != null) { + ex.printStackTrace(); + } + }), 5 * 60 * 20, 5 * 60 * 20); // 5 minutes } private void loadVouchersFromFile() { - voucherManager.clearVouchers(); + synchronized (vouchersConfig) { + voucherManager.clearVouchers(); - if (vouchersConfig.contains("vouchers")) { - for (String key : vouchersConfig.getConfigurationSection("vouchers").getKeys(false)) { - Voucher voucher = new Voucher(key, this); - ConfigurationSection cs = vouchersConfig.getConfigurationSection("vouchers." + key); + if (vouchersConfig.contains("vouchers")) { + for (String key : vouchersConfig.getConfigurationSection("vouchers").getKeys(false)) { + Voucher voucher = new Voucher(key, this); + ConfigurationSection cs = vouchersConfig.getConfigurationSection("vouchers." + key); - Material material; - String stringMaterial = cs.getString("material"); + Material material; + String stringMaterial = cs.getString("material"); - if (stringMaterial == null || stringMaterial.isEmpty()) { - material = Material.PAPER; - } else { - material = Material.matchMaterial(stringMaterial); - if (material == null) material = Material.PAPER; + if (stringMaterial == null || stringMaterial.isEmpty()) { + material = Material.PAPER; + } else { + material = Material.matchMaterial(stringMaterial); + if (material == null) material = Material.PAPER; + } + + voucher.setPermission(cs.getString("permission", "")) + .setMaterial(material) + .setData((short) cs.getInt("data", 0)) + .setName(cs.getString("name", "default")) + .setLore(cs.getStringList("lore")) + .setTexture(cs.getString("texture", "")) + .setGlow(cs.getBoolean("glow", false)) + .setConfirm(cs.getBoolean("confirm", true)) + .setUnbreakable(cs.getBoolean("unbreakable", false)) + .setHideAttributes(cs.getBoolean("hide-attributes", false)) + .setRemoveItem(cs.getBoolean("remove-item", true)) + .setHealPlayer(cs.getBoolean("heal-player", false)) + .setSmiteEffect(cs.getBoolean("smite-effect", false)) + .setCoolDown(cs.getInt("coolDown", 0)) + .setBroadcasts(cs.getStringList("broadcasts")) + .setMessages(cs.getStringList("messages")) + .setCommands(cs.getStringList("commands")) + .setActionBar(cs.getString("actionbar")) + .setTitle(cs.getString("titles.title")) + .setSubTitle(cs.getString("titles.subtitle")) + .setTitleFadeIn(cs.getInt("titles.fade-in", 0)) + .setTitleStay(cs.getInt("titles.stay", 0)) + .setTitleFadeOut(cs.getInt("titles.fade-out", 0)) + .setSound(cs.getString("sounds.sound")) + .setSoundPitch(cs.getInt("sounds.pitch", 0)) + .setParticle(cs.getString("particles.particle")) + .setParticleAmount(cs.getInt("particles.amount", 0)) + .setEffect(cs.getString("effects.effect")) + .setEffectAmplifier(cs.getInt("effects.amplifier")) + .setItemStack(cs.getItemStack("itemstack", null)); + + voucherManager.addVoucher(voucher); } - - voucher.setPermission(cs.getString("permission", "")) - .setMaterial(material) - .setData((short) cs.getInt("data", 0)) - .setName(cs.getString("name", "default")) - .setLore(cs.getStringList("lore")) - .setTexture(cs.getString("texture", "")) - .setGlow(cs.getBoolean("glow", false)) - .setConfirm(cs.getBoolean("confirm", true)) - .setUnbreakable(cs.getBoolean("unbreakable", false)) - .setHideAttributes(cs.getBoolean("hide-attributes", false)) - .setRemoveItem(cs.getBoolean("remove-item", true)) - .setHealPlayer(cs.getBoolean("heal-player", false)) - .setSmiteEffect(cs.getBoolean("smite-effect", false)) - .setCoolDown(cs.getInt("coolDown", 0)) - .setBroadcasts(cs.getStringList("broadcasts")) - .setMessages(cs.getStringList("messages")) - .setCommands(cs.getStringList("commands")) - .setActionBar(cs.getString("actionbar")) - .setTitle(cs.getString("titles.title")) - .setSubTitle(cs.getString("titles.subtitle")) - .setTitleFadeIn(cs.getInt("titles.fade-in", 0)) - .setTitleStay(cs.getInt("titles.stay", 0)) - .setTitleFadeOut(cs.getInt("titles.fade-out", 0)) - .setSound(cs.getString("sounds.sound")) - .setSoundPitch(cs.getInt("sounds.pitch", 0)) - .setParticle(cs.getString("particles.particle")) - .setParticleAmount(cs.getInt("particles.amount", 0)) - .setEffect(cs.getString("effects.effect")) - .setEffectAmplifier(cs.getInt("effects.amplifier")) - .setItemStack(cs.getItemStack("itemstack", null)); - - voucherManager.addVoucher(voucher); } } } private void saveVouchers() { - Collection voucherList = voucherManager.getVouchers(); + ThreadSync tSync = new ThreadSync(); - for (String voucherName : vouchersConfig.getConfigurationSection("vouchers").getKeys(false)) { - if (voucherList.stream().noneMatch(voucher -> voucher.getKey().equals(voucherName))) { - vouchersConfig.set("vouchers." + voucherName, null); + saveVouchersAsync(ex -> { + if (ex != null) { + ex.printStackTrace(); } - } - for (Voucher voucher : voucherList) { - String prefix = "vouchers." + voucher.getKey() + "."; + tSync.release(); + }); - vouchersConfig.set(prefix + "permission", voucher.getPermission()); - vouchersConfig.set(prefix + "material", voucher.getMaterial().name()); - vouchersConfig.set(prefix + "data", voucher.getData()); - vouchersConfig.set(prefix + "name", voucher.getName()); - vouchersConfig.set(prefix + "lore", voucher.getLore()); - vouchersConfig.set(prefix + "texture", voucher.getTexture()); - vouchersConfig.set(prefix + "glow", voucher.isGlow()); - vouchersConfig.set(prefix + "confirm", voucher.isConfirm()); - vouchersConfig.set(prefix + "unbreakable", voucher.isUnbreakable()); - vouchersConfig.set(prefix + "hide-attributes", voucher.isHideAttributes()); - vouchersConfig.set(prefix + "remove-item", voucher.isRemoveItem()); - vouchersConfig.set(prefix + "heal-player", voucher.isHealPlayer()); - vouchersConfig.set(prefix + "smite-effect", voucher.isSmiteEffect()); - vouchersConfig.set(prefix + "coolDown", voucher.getCoolDown()); - vouchersConfig.set(prefix + "broadcasts", voucher.getBroadcasts()); - vouchersConfig.set(prefix + "messages", voucher.getMessages()); - vouchersConfig.set(prefix + "commands", voucher.getCommands()); - vouchersConfig.set(prefix + "actionbar", voucher.getActionBar()); - vouchersConfig.set(prefix + "titles.title", voucher.getTitle()); - vouchersConfig.set(prefix + "titles.subtitle", voucher.getSubTitle()); - vouchersConfig.set(prefix + "titles.fade-in", voucher.getTitleFadeIn()); - vouchersConfig.set(prefix + "titles.stay", voucher.getTitleStay()); - vouchersConfig.set(prefix + "titles.fade-out", voucher.getTitleFadeOut()); - vouchersConfig.set(prefix + "sounds.sound", voucher.getSound()); - vouchersConfig.set(prefix + "sounds.pitch", voucher.getSoundPitch()); - vouchersConfig.set(prefix + "particles.particle", voucher.getParticle()); - vouchersConfig.set(prefix + "particles.amount", voucher.getParticleAmount()); - vouchersConfig.set(prefix + "effects.effect", voucher.getEffect()); - vouchersConfig.set(prefix + "effects.amplifier", voucher.getEffectAmplifier()); - vouchersConfig.set(prefix + "itemstack", voucher.getItemStack()); - } + tSync.waitForRelease(); + } - vouchersConfig.saveChanges(); + private void saveVouchersAsync(Callback callback) { + new Thread(() -> { + try { + synchronized (vouchersConfig) { + Collection voucherList = voucherManager.getVouchers(); + + ConfigurationSection cfgSec = vouchersConfig.getConfigurationSection("vouchers"); + if (cfgSec != null) { + for (String voucherName : cfgSec.getKeys(false)) { + if (voucherList.stream().noneMatch(voucher -> voucher.getKey().equals(voucherName))) { + vouchersConfig.set("vouchers." + voucherName, null); + } + } + } + + for (Voucher voucher : voucherList) { + String prefix = "vouchers." + voucher.getKey() + "."; + + vouchersConfig.set(prefix + "permission", voucher.getPermission()); + vouchersConfig.set(prefix + "material", voucher.getMaterial().name()); + vouchersConfig.set(prefix + "data", voucher.getData()); + vouchersConfig.set(prefix + "name", voucher.getName()); + vouchersConfig.set(prefix + "lore", voucher.getLore()); + vouchersConfig.set(prefix + "texture", voucher.getTexture()); + vouchersConfig.set(prefix + "glow", voucher.isGlow()); + vouchersConfig.set(prefix + "confirm", voucher.isConfirm()); + vouchersConfig.set(prefix + "unbreakable", voucher.isUnbreakable()); + vouchersConfig.set(prefix + "hide-attributes", voucher.isHideAttributes()); + vouchersConfig.set(prefix + "remove-item", voucher.isRemoveItem()); + vouchersConfig.set(prefix + "heal-player", voucher.isHealPlayer()); + vouchersConfig.set(prefix + "smite-effect", voucher.isSmiteEffect()); + vouchersConfig.set(prefix + "coolDown", voucher.getCoolDown()); + vouchersConfig.set(prefix + "broadcasts", voucher.getBroadcasts()); + vouchersConfig.set(prefix + "messages", voucher.getMessages()); + vouchersConfig.set(prefix + "commands", voucher.getCommands()); + vouchersConfig.set(prefix + "actionbar", voucher.getActionBar()); + vouchersConfig.set(prefix + "titles.title", voucher.getTitle()); + vouchersConfig.set(prefix + "titles.subtitle", voucher.getSubTitle()); + vouchersConfig.set(prefix + "titles.fade-in", voucher.getTitleFadeIn()); + vouchersConfig.set(prefix + "titles.stay", voucher.getTitleStay()); + vouchersConfig.set(prefix + "titles.fade-out", voucher.getTitleFadeOut()); + vouchersConfig.set(prefix + "sounds.sound", voucher.getSound()); + vouchersConfig.set(prefix + "sounds.pitch", voucher.getSoundPitch()); + vouchersConfig.set(prefix + "particles.particle", voucher.getParticle()); + vouchersConfig.set(prefix + "particles.amount", voucher.getParticleAmount()); + vouchersConfig.set(prefix + "effects.effect", voucher.getEffect()); + vouchersConfig.set(prefix + "effects.amplifier", voucher.getEffectAmplifier()); + vouchersConfig.set(prefix + "itemstack", voucher.getItemStack()); + } + + vouchersConfig.saveChanges(); + + callback.accept(null); + } + } catch (Exception ex) { + callback.accept(ex); + } + }, getName() + "-AsyncConfigSave").start(); } @Override public void onConfigReload() { - vouchersConfig.load(); + synchronized (vouchersConfig) { + vouchersConfig.load(); + } loadVouchersFromFile(); @@ -240,10 +287,6 @@ public class EpicVouchers extends SongodaPlugin { return this.voucherExecutor; } - public Config getVouchersConfig() { - return this.vouchersConfig; - } - public CommandManager getCommandManager() { return commandManager; } diff --git a/src/main/java/com/songoda/epicvouchers/utils/Callback.java b/src/main/java/com/songoda/epicvouchers/utils/Callback.java new file mode 100644 index 0000000..0ceed6e --- /dev/null +++ b/src/main/java/com/songoda/epicvouchers/utils/Callback.java @@ -0,0 +1,6 @@ +package com.songoda.epicvouchers.utils; + +// TODO: Copied from EpicAnchors - Move to SongodaCore (maybe rename too?) +public interface Callback { + void accept(Exception ex); +} diff --git a/src/main/java/com/songoda/epicvouchers/utils/ThreadSync.java b/src/main/java/com/songoda/epicvouchers/utils/ThreadSync.java new file mode 100644 index 0000000..78b488a --- /dev/null +++ b/src/main/java/com/songoda/epicvouchers/utils/ThreadSync.java @@ -0,0 +1,31 @@ +package com.songoda.epicvouchers.utils; + +import java.util.concurrent.atomic.AtomicReference; + +// TODO: Copied from EpicAnchors - Move to SongodaCore +public class ThreadSync { + private final Object syncObj = new Object(); + private final AtomicReference waiting = new AtomicReference<>(true); + + public void waitForRelease() { + synchronized (syncObj) { + while (waiting.get()) { + try { + syncObj.wait(); + } catch (Exception ignore) { + } + } + } + } + + public void release() { + synchronized (syncObj) { + waiting.set(false); + syncObj.notifyAll(); + } + } + + public void reset() { + waiting.set(true); + } +}