From 000463e10c9d1d484384b33feab041021b91fd54 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 15 Aug 2020 10:01:16 -0700 Subject: [PATCH] Adds new config options. Prevents queue stuffing. Players without cooldowns will not be able to stuff the level queue with calculation requests. Only one island level calculation at a time. Watch dog timeout on calculations added. Default 5 minutes. Config option to not use island zeroing. --- .../calculators/IslandLevelCalculator.java | 12 +++- .../bentobox/level/calculators/Pipeliner.java | 59 ++++++++++++++----- .../bentobox/level/calculators/Results.java | 28 +++++++++ .../level/commands/IslandLevelCommand.java | 8 +++ .../bentobox/level/config/ConfigSettings.java | 46 ++++++++++++++- .../listeners/IslandActivitiesListeners.java | 12 ++-- src/main/resources/blockconfig.yml | 5 ++ src/main/resources/config.yml | 11 ++++ src/main/resources/locales/en-US.yml | 2 + 9 files changed, 161 insertions(+), 22 deletions(-) diff --git a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java index 49f2798..db9d620 100644 --- a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java +++ b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java @@ -147,17 +147,20 @@ public class IslandLevelCalculator { private final Results results; private long duration; + private final boolean zeroIsland; /** * Constructor to get the level for an island * @param addon - Level addon * @param island - the island to scan * @param r - completable result that will be completed when the calculation is complete + * @param zeroIsland - true if the calculation is due to an island zeroing */ - public IslandLevelCalculator(Level addon, Island island, CompletableFuture r) { + public IslandLevelCalculator(Level addon, Island island, CompletableFuture r, boolean zeroIsland) { this.addon = addon; this.island = island; this.r = r; + this.zeroIsland = zeroIsland; results = new Results(); duration = System.currentTimeMillis(); chunksToCheck = getChunksToScan(island); @@ -536,4 +539,11 @@ public class IslandLevelCalculator { addon.getPipeliner().setTime(System.currentTimeMillis() - duration); // All done. } + + /** + * @return the zeroIsland + */ + boolean isNotZeroIsland() { + return !zeroIsland; + } } diff --git a/src/main/java/world/bentobox/level/calculators/Pipeliner.java b/src/main/java/world/bentobox/level/calculators/Pipeliner.java index dfc9462..ae8582a 100644 --- a/src/main/java/world/bentobox/level/calculators/Pipeliner.java +++ b/src/main/java/world/bentobox/level/calculators/Pipeliner.java @@ -1,8 +1,8 @@ package world.bentobox.level.calculators; -import java.util.HashSet; +import java.util.HashMap; +import java.util.Map; import java.util.Queue; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; @@ -12,6 +12,7 @@ import org.bukkit.scheduler.BukkitTask; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.level.Level; +import world.bentobox.level.calculators.Results.Result; /** * A pipeliner that will process one island at a time @@ -22,7 +23,7 @@ public class Pipeliner { private static final int START_DURATION = 10; // 10 seconds private final Queue toProcessQueue; - private final Set inProcessQueue; + private final Map inProcessQueue; private final BukkitTask task; private final Level addon; private long time; @@ -34,7 +35,7 @@ public class Pipeliner { public Pipeliner(Level addon) { this.addon = addon; toProcessQueue = new ConcurrentLinkedQueue<>(); - inProcessQueue = new HashSet<>(); + inProcessQueue = new HashMap<>(); // Loop continuously - check every tick if there is an island to scan task = Bukkit.getScheduler().runTaskTimer(BentoBox.getInstance(), () -> { if (!BentoBox.getInstance().isEnabled()) { @@ -47,7 +48,7 @@ public class Pipeliner { IslandLevelCalculator iD = toProcessQueue.poll(); // Ignore deleted or unonwed islands if (!iD.getIsland().isDeleted() && !iD.getIsland().isUnowned()) { - inProcessQueue.add(iD); + inProcessQueue.put(iD, System.currentTimeMillis()); // Start the scanning of a island with the first chunk scanChunk(iD); } @@ -82,6 +83,17 @@ public class Pipeliner { if (!Bukkit.isPrimaryThread()) { addon.getPlugin().logError("scanChunk not on Primary Thread!"); } + // Timeout check + if (System.currentTimeMillis() - inProcessQueue.get(iD) > addon.getSettings().getCalculationTimeout() * 60000) { + // Done + inProcessQueue.remove(iD); + iD.getR().complete(new Results(Result.TIMEOUT)); + addon.logError("Level calculation timed out after " + addon.getSettings().getCalculationTimeout() + "m for island: " + iD.getIsland()); + if (!iD.isNotZeroIsland()) { + addon.logError("Island level was being zeroed."); + } + return; + } if (Boolean.TRUE.equals(r) || task.isCancelled()) { // scanNextChunk returns true if there are more chunks to scan scanChunk(iD); @@ -96,18 +108,33 @@ public class Pipeliner { /** - * Adds an island to the scanning queue - * @param island - the island to scan - * + * Adds an island to the scanning queue but only if the island is not already in the queue + * @param island - the island to scan + * @return CompletableFuture of the results. Results will be null if the island is already in the queue */ public CompletableFuture addIsland(Island island) { + // Check if queue already contains island and it's not an island zero calculation + if (inProcessQueue.keySet().parallelStream().filter(IslandLevelCalculator::isNotZeroIsland) + .map(IslandLevelCalculator::getIsland).anyMatch(island::equals) + || toProcessQueue.parallelStream().filter(IslandLevelCalculator::isNotZeroIsland) + .map(IslandLevelCalculator::getIsland).anyMatch(island::equals)) { + return CompletableFuture.completedFuture(new Results(Result.IN_PROGRESS)); + } + return addToQueue(island, false); + } + + /** + * Adds an island to the scanning queue + * @param island - the island to scan + * @return CompletableFuture of the results + */ + public CompletableFuture zeroIsland(Island island) { + return addToQueue(island, true); + } + + private CompletableFuture addToQueue(Island island, boolean zeroing) { CompletableFuture r = new CompletableFuture<>(); - // Check if queue already contains island - /* - if (processQueue.parallelStream().map(IslandLevelCalculator::getIsland).anyMatch(island::equals)) { - return CompletableFuture.completedFuture(null); - }*/ - toProcessQueue.add(new IslandLevelCalculator(addon, island, r)); + toProcessQueue.add(new IslandLevelCalculator(addon, island, r, zeroing)); count++; return r; } @@ -136,8 +163,10 @@ public class Pipeliner { addon.log("Stopping Level queue"); task.cancel(); this.inProcessQueue.clear(); - this.toProcessQueue.clear(); + this.toProcessQueue.clear(); } + + } diff --git a/src/main/java/world/bentobox/level/calculators/Results.java b/src/main/java/world/bentobox/level/calculators/Results.java index 24a85b7..ec57548 100644 --- a/src/main/java/world/bentobox/level/calculators/Results.java +++ b/src/main/java/world/bentobox/level/calculators/Results.java @@ -10,6 +10,20 @@ import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; public class Results { + public enum Result { + /** + * A level calc is already in progress + */ + IN_PROGRESS, + /** + * Results will be available + */ + AVAILABLE, + /** + * Result if calculation timed out + */ + TIMEOUT + } List report; final Multiset mdCount = HashMultiset.create(); final Multiset uwCount = HashMultiset.create(); @@ -22,7 +36,15 @@ public class Results { AtomicInteger deathHandicap = new AtomicInteger(0); AtomicLong pointsToNextLevel = new AtomicLong(0); AtomicLong initialLevel = new AtomicLong(0); + final Result state; + public Results(Result state) { + this.state = state; + } + + public Results() { + this.state = Result.AVAILABLE; + } /** * @return the deathHandicap */ @@ -101,5 +123,11 @@ public class Results { public Multiset getUwCount() { return uwCount; } + /** + * @return the state + */ + public Result getState() { + return state; + } } \ No newline at end of file diff --git a/src/main/java/world/bentobox/level/commands/IslandLevelCommand.java b/src/main/java/world/bentobox/level/commands/IslandLevelCommand.java index 7642453..7db0584 100644 --- a/src/main/java/world/bentobox/level/commands/IslandLevelCommand.java +++ b/src/main/java/world/bentobox/level/commands/IslandLevelCommand.java @@ -10,6 +10,7 @@ import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.level.Level; import world.bentobox.level.calculators.Results; +import world.bentobox.level.calculators.Results.Result; public class IslandLevelCommand extends CompositeCommand { @@ -88,6 +89,13 @@ public class IslandLevelCommand extends CompositeCommand { long oldLevel = addon.getManager().getIslandLevel(getWorld(), playerUUID); addon.getManager().calculateLevel(playerUUID, island).thenAccept(results -> { if (results == null) return; // island was deleted or become unowned + if (results.getState().equals(Result.IN_PROGRESS)) { + user.sendMessage("island.level.in-progress"); + return; + } else if (results.getState().equals(Result.TIMEOUT)) { + user.sendMessage("island.level.time-out"); + return; + } showResult(user, playerUUID, island, oldLevel, results); }); return true; diff --git a/src/main/java/world/bentobox/level/config/ConfigSettings.java b/src/main/java/world/bentobox/level/config/ConfigSettings.java index ea24e65..ebebae7 100644 --- a/src/main/java/world/bentobox/level/config/ConfigSettings.java +++ b/src/main/java/world/bentobox/level/config/ConfigSettings.java @@ -18,13 +18,30 @@ public class ConfigSettings implements ConfigObject { @ConfigComment("Level will NOT hook into these game mode addons.") @ConfigEntry(path = "disabled-game-modes") private List gameModes = Collections.emptyList(); - + @ConfigComment("") @ConfigComment("Number of concurrent island calculations") @ConfigComment("If your CPU can handle it, you can run parallel island calcs if there are more than one in the queue") @ConfigEntry(path = "concurrent-island-calcs") private int concurrentIslandCalcs = 1; + @ConfigComment("") + @ConfigComment("Island level calculation timeout in minutes.") + @ConfigComment("If an island takes longer that this time to calculate, then the calculation will abort.") + @ConfigComment("Generally, calculation should only take a few seconds, so if this ever triggers then something is not right.") + @ConfigEntry(path = "calculation-timeout") + private int calculationTimeout = 5; + + + @ConfigComment("") + @ConfigComment("Zero island levels on new island or island reset") + @ConfigComment("If true, Level will calculate the starter island's level and remove it from any future level calculations.") + @ConfigComment("If false, the player's starter island and will count towards their level.") + @ConfigComment("This will reduce CPU if it isn't used.") + @ConfigEntry(path = "zero-new-island-levels") + private boolean zeroNewIslandLevels = true; + + @ConfigComment("") @ConfigComment("Calculate island level on login") @ConfigComment("This silently calculates the player's island level when they login") @@ -316,8 +333,35 @@ public class ConfigSettings implements ConfigObject { } + /** + * @return the zeroNewIslandLevels + */ + public boolean isZeroNewIslandLevels() { + return zeroNewIslandLevels; + } + /** + * @param zeroNewIslandLevels the zeroNewIslandLevels to set + */ + public void setZeroNewIslandLevels(boolean zeroNewIslandLevels) { + this.zeroNewIslandLevels = zeroNewIslandLevels; + } + /** + * @return the calculationTimeout + */ + public int getCalculationTimeout() { + return calculationTimeout; + } + + + /** + * @param calculationTimeout the calculationTimeout to set + */ + public void setCalculationTimeout(int calculationTimeout) { + this.calculationTimeout = calculationTimeout; + } + } diff --git a/src/main/java/world/bentobox/level/listeners/IslandActivitiesListeners.java b/src/main/java/world/bentobox/level/listeners/IslandActivitiesListeners.java index bc7792b..716a788 100644 --- a/src/main/java/world/bentobox/level/listeners/IslandActivitiesListeners.java +++ b/src/main/java/world/bentobox/level/listeners/IslandActivitiesListeners.java @@ -39,20 +39,22 @@ public class IslandActivitiesListeners implements Listener { @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onNewIsland(IslandCreatedEvent e) { - - zeroIsland(e.getIsland()); + if (addon.getSettings().isZeroNewIslandLevels()) { + zeroIsland(e.getIsland()); + } } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onNewIsland(IslandResettedEvent e) { - - zeroIsland(e.getIsland()); + if (addon.getSettings().isZeroNewIslandLevels()) { + zeroIsland(e.getIsland()); + } } private void zeroIsland(final Island island) { // Clear the island setting if (island.getOwner() != null && island.getWorld() != null) { - addon.getPipeliner().addIsland(island).thenAccept(results -> + addon.getPipeliner().zeroIsland(island).thenAccept(results -> addon.getManager().setInitialIslandLevel(island, results.getLevel())); } } diff --git a/src/main/resources/blockconfig.yml b/src/main/resources/blockconfig.yml index 6b2fdf8..688f442 100644 --- a/src/main/resources/blockconfig.yml +++ b/src/main/resources/blockconfig.yml @@ -775,6 +775,11 @@ blocks: ZOMBIE_HEAD: 1 ZOMBIE_WALL_HEAD: 1 worlds: + caveblock-world: + STONE: 0 + GRANITE: 0 + ANDESITE: 0 + DIORITE: 0 acidisland_world: SAND: 0 SANDSTONE: 0 diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 77fda09..2ea0343 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -9,6 +9,17 @@ disabled-game-modes: # Number of concurrent island calculations # If your CPU can handle it, you can run parallel island calcs if there are more than one in the queue concurrent-island-calcs: 1 +# +# Island level calculation timeout in minutes. +# If an island takes longer that this time to calculate, then the calculation will abort. +# Generally, calculation should only take a few seconds, so if this ever triggers then something is not right. +calculation-timeout: 5 +# +# Zero island levels on new island or island reset +# If true, Level will calculate the starter island's level and remove it from any future level calculations. +# If this is false, the player's starter island and will count towards their level. +# This will reduce CPU if it isn't used. +zero-new-island-levels: true # # Calculate island level on login # This silently calculates the player's island level when they login diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index d9e99a1..6b75387 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -35,6 +35,8 @@ island: required-points-to-next-level: "&a [points] points required until the next level" deaths: "&c([number] deaths)" cooldown: "&c You must wait &b[time] &c seconds until you can do that again" + in-progress: "&6 Island level calculation is in progress..." + time-out: "&c The level calculation took too long. Please try again later." top: description: "show the Top Ten"