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.
This commit is contained in:
tastybento 2020-08-15 10:01:16 -07:00
parent 49e56b515a
commit 000463e10c
9 changed files with 161 additions and 22 deletions

View File

@ -147,17 +147,20 @@ public class IslandLevelCalculator {
private final Results results; private final Results results;
private long duration; private long duration;
private final boolean zeroIsland;
/** /**
* Constructor to get the level for an island * Constructor to get the level for an island
* @param addon - Level addon * @param addon - Level addon
* @param island - the island to scan * @param island - the island to scan
* @param r - completable result that will be completed when the calculation is complete * @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<Results> r) { public IslandLevelCalculator(Level addon, Island island, CompletableFuture<Results> r, boolean zeroIsland) {
this.addon = addon; this.addon = addon;
this.island = island; this.island = island;
this.r = r; this.r = r;
this.zeroIsland = zeroIsland;
results = new Results(); results = new Results();
duration = System.currentTimeMillis(); duration = System.currentTimeMillis();
chunksToCheck = getChunksToScan(island); chunksToCheck = getChunksToScan(island);
@ -536,4 +539,11 @@ public class IslandLevelCalculator {
addon.getPipeliner().setTime(System.currentTimeMillis() - duration); addon.getPipeliner().setTime(System.currentTimeMillis() - duration);
// All done. // All done.
} }
/**
* @return the zeroIsland
*/
boolean isNotZeroIsland() {
return !zeroIsland;
}
} }

View File

@ -1,8 +1,8 @@
package world.bentobox.level.calculators; package world.bentobox.level.calculators;
import java.util.HashSet; import java.util.HashMap;
import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
@ -12,6 +12,7 @@ import org.bukkit.scheduler.BukkitTask;
import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.level.Level; import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
/** /**
* A pipeliner that will process one island at a time * 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 static final int START_DURATION = 10; // 10 seconds
private final Queue<IslandLevelCalculator> toProcessQueue; private final Queue<IslandLevelCalculator> toProcessQueue;
private final Set<IslandLevelCalculator> inProcessQueue; private final Map<IslandLevelCalculator, Long> inProcessQueue;
private final BukkitTask task; private final BukkitTask task;
private final Level addon; private final Level addon;
private long time; private long time;
@ -34,7 +35,7 @@ public class Pipeliner {
public Pipeliner(Level addon) { public Pipeliner(Level addon) {
this.addon = addon; this.addon = addon;
toProcessQueue = new ConcurrentLinkedQueue<>(); toProcessQueue = new ConcurrentLinkedQueue<>();
inProcessQueue = new HashSet<>(); inProcessQueue = new HashMap<>();
// Loop continuously - check every tick if there is an island to scan // Loop continuously - check every tick if there is an island to scan
task = Bukkit.getScheduler().runTaskTimer(BentoBox.getInstance(), () -> { task = Bukkit.getScheduler().runTaskTimer(BentoBox.getInstance(), () -> {
if (!BentoBox.getInstance().isEnabled()) { if (!BentoBox.getInstance().isEnabled()) {
@ -47,7 +48,7 @@ public class Pipeliner {
IslandLevelCalculator iD = toProcessQueue.poll(); IslandLevelCalculator iD = toProcessQueue.poll();
// Ignore deleted or unonwed islands // Ignore deleted or unonwed islands
if (!iD.getIsland().isDeleted() && !iD.getIsland().isUnowned()) { 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 // Start the scanning of a island with the first chunk
scanChunk(iD); scanChunk(iD);
} }
@ -82,6 +83,17 @@ public class Pipeliner {
if (!Bukkit.isPrimaryThread()) { if (!Bukkit.isPrimaryThread()) {
addon.getPlugin().logError("scanChunk not on Primary Thread!"); 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()) { if (Boolean.TRUE.equals(r) || task.isCancelled()) {
// scanNextChunk returns true if there are more chunks to scan // scanNextChunk returns true if there are more chunks to scan
scanChunk(iD); scanChunk(iD);
@ -96,18 +108,33 @@ public class Pipeliner {
/** /**
* Adds an island to the scanning queue * Adds an island to the scanning queue but only if the island is not already in the queue
* @param island - the island to scan * @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<Results> addIsland(Island island) { public CompletableFuture<Results> 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<Results> zeroIsland(Island island) {
return addToQueue(island, true);
}
private CompletableFuture<Results> addToQueue(Island island, boolean zeroing) {
CompletableFuture<Results> r = new CompletableFuture<>(); CompletableFuture<Results> r = new CompletableFuture<>();
// Check if queue already contains island toProcessQueue.add(new IslandLevelCalculator(addon, island, r, zeroing));
/*
if (processQueue.parallelStream().map(IslandLevelCalculator::getIsland).anyMatch(island::equals)) {
return CompletableFuture.completedFuture(null);
}*/
toProcessQueue.add(new IslandLevelCalculator(addon, island, r));
count++; count++;
return r; return r;
} }
@ -136,8 +163,10 @@ public class Pipeliner {
addon.log("Stopping Level queue"); addon.log("Stopping Level queue");
task.cancel(); task.cancel();
this.inProcessQueue.clear(); this.inProcessQueue.clear();
this.toProcessQueue.clear(); this.toProcessQueue.clear();
} }
} }

View File

@ -10,6 +10,20 @@ import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset; import com.google.common.collect.Multiset;
public class Results { 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<String> report; List<String> report;
final Multiset<Material> mdCount = HashMultiset.create(); final Multiset<Material> mdCount = HashMultiset.create();
final Multiset<Material> uwCount = HashMultiset.create(); final Multiset<Material> uwCount = HashMultiset.create();
@ -22,7 +36,15 @@ public class Results {
AtomicInteger deathHandicap = new AtomicInteger(0); AtomicInteger deathHandicap = new AtomicInteger(0);
AtomicLong pointsToNextLevel = new AtomicLong(0); AtomicLong pointsToNextLevel = new AtomicLong(0);
AtomicLong initialLevel = 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 * @return the deathHandicap
*/ */
@ -101,5 +123,11 @@ public class Results {
public Multiset<Material> getUwCount() { public Multiset<Material> getUwCount() {
return uwCount; return uwCount;
} }
/**
* @return the state
*/
public Result getState() {
return state;
}
} }

View File

@ -10,6 +10,7 @@ import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.level.Level; import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results; import world.bentobox.level.calculators.Results;
import world.bentobox.level.calculators.Results.Result;
public class IslandLevelCommand extends CompositeCommand { public class IslandLevelCommand extends CompositeCommand {
@ -88,6 +89,13 @@ public class IslandLevelCommand extends CompositeCommand {
long oldLevel = addon.getManager().getIslandLevel(getWorld(), playerUUID); long oldLevel = addon.getManager().getIslandLevel(getWorld(), playerUUID);
addon.getManager().calculateLevel(playerUUID, island).thenAccept(results -> { addon.getManager().calculateLevel(playerUUID, island).thenAccept(results -> {
if (results == null) return; // island was deleted or become unowned 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); showResult(user, playerUUID, island, oldLevel, results);
}); });
return true; return true;

View File

@ -18,13 +18,30 @@ public class ConfigSettings implements ConfigObject {
@ConfigComment("Level will NOT hook into these game mode addons.") @ConfigComment("Level will NOT hook into these game mode addons.")
@ConfigEntry(path = "disabled-game-modes") @ConfigEntry(path = "disabled-game-modes")
private List<String> gameModes = Collections.emptyList(); private List<String> gameModes = Collections.emptyList();
@ConfigComment("") @ConfigComment("")
@ConfigComment("Number of concurrent island calculations") @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") @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") @ConfigEntry(path = "concurrent-island-calcs")
private int concurrentIslandCalcs = 1; 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("")
@ConfigComment("Calculate island level on login") @ConfigComment("Calculate island level on login")
@ConfigComment("This silently calculates the player's island level when they 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;
}
} }

View File

@ -39,20 +39,22 @@ public class IslandActivitiesListeners implements Listener {
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onNewIsland(IslandCreatedEvent e) { public void onNewIsland(IslandCreatedEvent e) {
if (addon.getSettings().isZeroNewIslandLevels()) {
zeroIsland(e.getIsland()); zeroIsland(e.getIsland());
}
} }
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onNewIsland(IslandResettedEvent e) { public void onNewIsland(IslandResettedEvent e) {
if (addon.getSettings().isZeroNewIslandLevels()) {
zeroIsland(e.getIsland()); zeroIsland(e.getIsland());
}
} }
private void zeroIsland(final Island island) { private void zeroIsland(final Island island) {
// Clear the island setting // Clear the island setting
if (island.getOwner() != null && island.getWorld() != null) { 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())); addon.getManager().setInitialIslandLevel(island, results.getLevel()));
} }
} }

View File

@ -775,6 +775,11 @@ blocks:
ZOMBIE_HEAD: 1 ZOMBIE_HEAD: 1
ZOMBIE_WALL_HEAD: 1 ZOMBIE_WALL_HEAD: 1
worlds: worlds:
caveblock-world:
STONE: 0
GRANITE: 0
ANDESITE: 0
DIORITE: 0
acidisland_world: acidisland_world:
SAND: 0 SAND: 0
SANDSTONE: 0 SANDSTONE: 0

View File

@ -9,6 +9,17 @@ disabled-game-modes:
# Number of concurrent island calculations # 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 # 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 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 # Calculate island level on login
# This silently calculates the player's island level when they login # This silently calculates the player's island level when they login

View File

@ -35,6 +35,8 @@ island:
required-points-to-next-level: "&a [points] points required until the next level" required-points-to-next-level: "&a [points] points required until the next level"
deaths: "&c([number] deaths)" deaths: "&c([number] deaths)"
cooldown: "&c You must wait &b[time] &c seconds until you can do that again" 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: top:
description: "show the Top Ten" description: "show the Top Ten"