Caught the ConcurrentModificationException and added a bunch of logging-statements for testing

This commit is contained in:
Artemis-the-gr8 2022-06-04 22:03:57 +02:00
parent 2fed00fc57
commit 5d0333b823
7 changed files with 185 additions and 76 deletions

View File

@ -1,6 +1,7 @@
package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.TestFileHandler;
import com.gmail.artemis.the.gr8.playerstats.reload.ReloadThread;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread;
@ -11,9 +12,12 @@ import org.bukkit.command.CommandSender;
public class ThreadManager {
private static final int threshold = 10;
private final Main plugin;
private final BukkitAudiences adventure;
private static ConfigHandler config;
private static TestFileHandler testFile;
private final MessageFactory messageFactory;
private ReloadThread reloadThread;
@ -26,16 +30,17 @@ public class ThreadManager {
config = c;
messageFactory = m;
testFile = new TestFileHandler(plugin);
startReloadThread(null, true);
}
public void startReloadThread(CommandSender sender, boolean firstTimeLoading) {
reloadThread = new ReloadThread(config, plugin, statThread, sender, firstTimeLoading);
reloadThread = new ReloadThread(threshold, config, testFile, plugin, statThread, sender, firstTimeLoading);
reloadThread.start();
}
public void startStatThread(StatRequest request) {
statThread = new StatThread(request, reloadThread, adventure, config, messageFactory, plugin);
statThread = new StatThread(threshold, request, reloadThread, adventure, config, testFile, messageFactory, plugin);
statThread.start();
}

View File

@ -11,28 +11,43 @@ import java.io.IOException;
public class TestFileHandler {
private static File testFile;
private static FileConfiguration testConf;
private static ConfigurationSection playerCount;
private File testFile;
private FileConfiguration testConf;
private ConfigurationSection number;
private final Main plugin;
private String onEnable;
private String reload;
private String debugging;
private String topStat;
public TestFileHandler(Main p) {
plugin = p;
loadFile();
onEnable = "onEnable";
reload = "reload";
debugging = "exception-debugging";
topStat = "top-stat";
}
public static void savePlayerCount(int count) {
/**
* Creates a new config section for the given threshold. Only needs to be called once, unless threshold changes.
* @param count amount of players to calculate statistics with
* @param threshold how small the subTasks have to become
*/
public void saveThreshold(int count, int threshold) {
loadFile(count);
String path = threshold + " threshold";
try {
playerCount = testConf.getConfigurationSection(count + " players");
if (playerCount == null) {
playerCount = testConf.createSection(count + " players");
playerCount.createSection("onEnable");
playerCount.createSection("individual-stat");
playerCount.createSection("top-stat");
number = testConf.getConfigurationSection(path);
if (number == null) {
number = testConf.createSection(path);
number.createSection(onEnable);
number.createSection(reload);
number.createSection(debugging);
number.createSection(topStat);
}
else {
playerCount = testConf.getConfigurationSection(count + " players");
number = testConf.getConfigurationSection(path);
}
saveFile();
}
@ -41,18 +56,49 @@ public class TestFileHandler {
}
}
public static void saveTimeTaken(long time, String timeDescription) {
public void logRunCount(boolean errorEncountered) {
try {
if (timeDescription.equalsIgnoreCase("onEnable")) {
saveToSection(time, playerCount.getConfigurationSection("onEnable"));
ConfigurationSection section = number.getConfigurationSection(debugging);
if (section != null) {
int runs = section.getInt("runs");
section.set("runs", runs +1);
if (errorEncountered) {
int errors = section.getInt("errors");
section.set("errors", errors + 1);
String path = "error-" + (errors + 1) + "-during-run";
int lastError = section.getInt("error-" + errors + "-during-run");
int runsUntilError = runs - lastError;
String path2 = "until-error-" + (errors + 1);
section.set(path2, runsUntilError);
section.set(path, runs);
}
saveFile();
}
else if (timeDescription.equalsIgnoreCase("individual-stat")) {
saveToSection(time, playerCount.getConfigurationSection("individual-stat"));
saveFile();
} catch (Exception e) {
e.printStackTrace();
}
else if (timeDescription.equalsIgnoreCase("top-stat")) {
saveToSection(time, playerCount.getConfigurationSection("top-stat"));
}
/**
* Logs how long a certain method took for the earlier set threshold. Always make sure saveThreshold has been
* called once before this method is called.
* @param time how long the given action took
* @param scenario describes which section to get. 1 means onEnable, 2 means reload, and 3 means top-stat
*/
public void saveTimeTaken(long time, int scenario) {
String path = "";
if (scenario == 1) path = onEnable;
else if (scenario == 2) path = reload;
else if (scenario == 3) path = topStat;
try {
ConfigurationSection section = number.getConfigurationSection(path);
if (section != null) {
saveTimeToSection(time, section);
saveFile();
}
}
@ -61,7 +107,7 @@ public class TestFileHandler {
}
}
private static void saveToSection(long time, ConfigurationSection section) {
private void saveTimeToSection(long time, ConfigurationSection section) {
if (section.contains("average")) {
long average = section.getLong("average");
long newAverage = ((average * (section.getKeys(false).size() -1)) + time)/section.getKeys(false).size();
@ -75,9 +121,11 @@ public class TestFileHandler {
}
}
private void loadFile() {
testFile = new File(plugin.getDataFolder(), "test.yml");
private void loadFile(int players) {
String fileName = "test_" + players + ".yml";
testFile = new File(plugin.getDataFolder(), fileName);
if (!testFile.exists()) {
plugin.getLogger().info("Attempting to create testFile...");
createFile();
}
@ -91,24 +139,23 @@ public class TestFileHandler {
saveFile();
}
private static void createFile() {
private void createFile() {
testFile.getParentFile().mkdirs();
try {
testFile.createNewFile();
plugin.getLogger().info("Even though this would return false, secretly a file has been created anyway");
}
catch (IOException e) {
e.printStackTrace();
}
}
private static boolean saveFile() {
private void saveFile() {
try {
testConf.save(testFile);
return true;
}
catch (Exception e) {
e.printStackTrace();
return false;
}
}
}

View File

@ -1,9 +1,9 @@
package com.gmail.artemis.the.gr8.playerstats.reload;
import com.gmail.artemis.the.gr8.playerstats.utils.UnixTimeHandler;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -11,18 +11,34 @@ import java.util.concurrent.RecursiveAction;
public class ReloadAction extends RecursiveAction {
private static final int threshold = 10;
private final int threshold;
private final OfflinePlayer[] players;
private final int start;
private final int end;
private final boolean whitelistOnly;
private final boolean excludeBanned;
private final int lastPlayedLimit;
private final ConcurrentHashMap<String, UUID> offlinePlayerUUIDs;
public ReloadAction(OfflinePlayer[] players,
public ReloadAction(int threshold, OfflinePlayer[] players,
boolean whitelistOnly, boolean excludeBanned, int lastPlayedLimit,
ConcurrentHashMap<String, UUID> offlinePlayerUUIDs) {
this(threshold, players, 0, players.length,
whitelistOnly, excludeBanned, lastPlayedLimit, offlinePlayerUUIDs);
}
protected ReloadAction(int threshold, OfflinePlayer[] players, int start, int end,
boolean whitelistOnly, boolean excludeBanned, int lastPlayedLimit,
ConcurrentHashMap<String, UUID> offlinePlayerUUIDs) {
this.threshold = threshold;
this.players = players;
this.start = start;
this.end = end;
this.whitelistOnly = whitelistOnly;
this.excludeBanned = excludeBanned;
this.lastPlayedLimit = lastPlayedLimit;
@ -31,13 +47,15 @@ public class ReloadAction extends RecursiveAction {
@Override
protected void compute() {
if (players.length < threshold) {
final int length = end - start;
if (length < threshold) {
process();
}
else {
ReloadAction subTask1 = new ReloadAction(Arrays.copyOfRange(players, 0, players.length/2),
final int split = length / 2;
final ReloadAction subTask1 = new ReloadAction(threshold, players, start, (start + split),
whitelistOnly, excludeBanned, lastPlayedLimit, offlinePlayerUUIDs);
ReloadAction subTask2 = new ReloadAction(Arrays.copyOfRange(players, players.length/2, players.length),
final ReloadAction subTask2 = new ReloadAction(threshold, players, (start + split), end,
whitelistOnly, excludeBanned, lastPlayedLimit, offlinePlayerUUIDs);
try {
@ -51,7 +69,9 @@ public class ReloadAction extends RecursiveAction {
}
private void process() {
for (OfflinePlayer player : players) {
try {
for (int i = start; i < end; i++) {
OfflinePlayer player = players[i];
if (player.getName() != null &&
(!whitelistOnly || player.isWhitelisted()) &&
(!excludeBanned || !player.isBanned()) &&
@ -59,5 +79,9 @@ public class ReloadAction extends RecursiveAction {
offlinePlayerUUIDs.put(player.getName(), player.getUniqueId());
}
}
} catch (ConcurrentModificationException e) {
Bukkit.getLogger().warning("ReloadAction has thrown a ConcurrentModificationException because of " + e.getCause());
e.printStackTrace();
}
}
}

View File

@ -15,19 +15,25 @@ import org.jetbrains.annotations.Nullable;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ForkJoinPool;
public class ReloadThread extends Thread {
private final int threshold;
private static ConfigHandler config;
private static TestFileHandler testFile;
private final Main plugin;
private final StatThread statThread;
private final CommandSender sender;
private final boolean firstTimeLoading;
public ReloadThread(ConfigHandler c, Main p, @Nullable StatThread s, @Nullable CommandSender se, boolean firstTime) {
public ReloadThread(int threshold, ConfigHandler c, TestFileHandler t, Main p, @Nullable StatThread s, @Nullable CommandSender se, boolean firstTime) {
this.threshold = threshold;
config = c;
testFile = t;
plugin = p;
statThread = s;
@ -55,6 +61,7 @@ public class ReloadThread extends Thread {
if (config.reloadConfig()) {
OfflinePlayerHandler.updateOfflinePlayerList(getPlayerMap(false));
testFile.saveTimeTaken(System.currentTimeMillis() - time, 2);
plugin.getLogger().info("Amount of relevant players: " + OfflinePlayerHandler.getOfflinePlayerCount());
plugin.logTimeTaken("ReloadThread", "loading offline players", time);
if (sender != null) {
@ -66,8 +73,8 @@ public class ReloadThread extends Thread {
plugin.getLogger().info("Loading offline players...");
OfflinePlayerHandler.updateOfflinePlayerList(getPlayerMap(true));
TestFileHandler.savePlayerCount(OfflinePlayerHandler.getOfflinePlayerCount());
TestFileHandler.saveTimeTaken(System.currentTimeMillis() - time, "onEnable");
testFile.saveThreshold(OfflinePlayerHandler.getOfflinePlayerCount(), threshold);
testFile.saveTimeTaken(System.currentTimeMillis() - time, 1);
plugin.getLogger().info("Amount of relevant players: " + OfflinePlayerHandler.getOfflinePlayerCount());
plugin.logTimeTaken("ReloadThread", "loading offline players", time);
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
@ -80,18 +87,21 @@ public class ReloadThread extends Thread {
ConcurrentHashMap<String, UUID> playerMap = new ConcurrentHashMap<>(size);
ReloadAction task = new ReloadAction(offlinePlayers, config.whitelistOnly(), config.excludeBanned(), config.lastPlayedLimit(), playerMap);
ReloadAction task = new ReloadAction(threshold, offlinePlayers, config.whitelistOnly(), config.excludeBanned(), config.lastPlayedLimit(), playerMap);
ForkJoinPool commonPool = ForkJoinPool.commonPool();
commonPool.invoke(task);
ConcurrentHashMap<String, UUID> newPlayerMap = new ConcurrentHashMap<>(playerMap.size() * 11);
ConcurrentHashMap<String, UUID> newPlayerMap = new ConcurrentHashMap<>(playerMap.size());
/*
for (int i = 0; i < 11; i++) {
for (String key : playerMap.keySet()) {
newPlayerMap.put(key + i, playerMap.get(key));
}
}
//newPlayerMap.putAll(playerMap);
*/
newPlayerMap.putAll(playerMap);
return newPlayerMap;
}
}

View File

@ -7,6 +7,7 @@ import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.MessageFactory;
import com.google.common.collect.ImmutableList;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
@ -19,26 +20,31 @@ import java.util.ConcurrentModificationException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
public class StatThread extends Thread {
private final int threshold;
private final StatRequest request;
private final ReloadThread reloadThread;
private final BukkitAudiences adventure;
private final ConfigHandler config;
private static ConfigHandler config;
private static TestFileHandler testFile;
private final MessageFactory messageFactory;
private final Main plugin;
//constructor (called on thread creation)
public StatThread(StatRequest s, @Nullable ReloadThread r, BukkitAudiences b, ConfigHandler c, MessageFactory o, Main p) {
public StatThread(int threshold, StatRequest s, @Nullable ReloadThread r, BukkitAudiences b, ConfigHandler c, TestFileHandler t, MessageFactory o, Main p) {
this.threshold = threshold;
request = s;
reloadThread = r;
adventure = b;
config = c;
testFile = t;
messageFactory = o;
plugin = p;
plugin.getLogger().info("StatThread created!");
@ -84,11 +90,13 @@ public class StatThread extends Thread {
adventure.sender(sender).sendMessage(messageFactory.formatTopStats(
getTopStatistics(), statName, subStatEntry));
TestFileHandler.saveTimeTaken(System.currentTimeMillis() - time, "top-stat");
testFile.saveTimeTaken(System.currentTimeMillis() - time, 3);
testFile.logRunCount(false);
plugin.logTimeTaken("StatThread", "calculating top stat", time);
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
} catch (ConcurrentModificationException e) {
testFile.logRunCount(true);
adventure.sender(sender).sendMessage(messageFactory.unknownError());
} catch (Exception e) {
sender.sendMessage(messageFactory.formatExceptions(e.toString()));
@ -101,7 +109,6 @@ public class StatThread extends Thread {
adventure.sender(sender).sendMessage(
messageFactory.formatPlayerStat(
playerName, statName, subStatEntry, getStatistic()));
TestFileHandler.saveTimeTaken(System.currentTimeMillis() - time, "individual-stat");
plugin.logTimeTaken("StatThread", "calculating individual stat", time);
} catch (Exception e) {
@ -125,8 +132,10 @@ public class StatThread extends Thread {
//invokes a bunch of worker pool threads to divide and conquer (get the statistics for all players in the list)
private LinkedHashMap<String, Integer> getTopStatistics() throws ConcurrentModificationException {
ConcurrentHashMap<String, Integer> playerStats = new ConcurrentHashMap<>((int) (getOfflinePlayerCount() * 1.05));
String[] playerNames = OfflinePlayerHandler.getOfflinePlayerNames().toArray(new String[0]);
TopStatAction task = new TopStatAction(playerNames,
//ConcurrentLinkedDeque<String> playerNames = new ConcurrentLinkedDeque<>(OfflinePlayerHandler.getOfflinePlayerNames());
//String[] playerNames = OfflinePlayerHandler.getOfflinePlayerNames().toArray(new String[0]);
ImmutableList<String> playerNames = ImmutableList.copyOf(OfflinePlayerHandler.getOfflinePlayerNames());
TopStatAction task = new TopStatAction(threshold, playerNames,
request, playerStats);
ForkJoinPool commonPool = ForkJoinPool.commonPool();
@ -134,7 +143,17 @@ public class StatThread extends Thread {
commonPool.invoke(task);
} catch (ConcurrentModificationException e) {
e.printStackTrace();
try {
if (!task.cancel(true)) {
plugin.getLogger().severe("Tried to cancel task, but failed. You might need to shut down the server and reboot");
throw new ConcurrentModificationException(e.toString());
} else {
plugin.getLogger().warning("Canceling task because of a ConcurrentModificationException...");
}
} catch (ConcurrentModificationException ex) {
ex.printStackTrace();
throw new ConcurrentModificationException(ex.toString());
}
}
return playerStats.entrySet().stream()

View File

@ -1,6 +1,7 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import com.google.common.collect.ImmutableList;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
@ -11,9 +12,9 @@ import java.util.concurrent.RecursiveAction;
public class TopStatAction extends RecursiveAction {
private static final int threshold = 11;
private final int threshold;
private final String[] playerNames;
private final ImmutableList<String> playerNames;
private final StatRequest request;
private final ConcurrentHashMap<String, Integer> playerStats;
@ -25,23 +26,22 @@ public class TopStatAction extends RecursiveAction {
* @param playerStats the ConcurrentHashMap to put the results on
*/
public TopStatAction(String[] playerNames, StatRequest statRequest, ConcurrentHashMap<String, Integer> playerStats) {
public TopStatAction(int threshold, ImmutableList<String> playerNames, StatRequest statRequest, ConcurrentHashMap<String, Integer> playerStats) {
this.threshold = threshold;
this.playerNames = playerNames;
request = statRequest;
this.request = statRequest;
this.playerStats = playerStats;
}
@Override
protected void compute() {
if (playerNames.length < threshold) {
if (playerNames.size() < threshold) {
getStatsDirectly();
}
else {
Bukkit.getLogger().info("playerNames length: " + playerNames.length);
TopStatAction subTask1 = new TopStatAction(Arrays.copyOfRange(playerNames, 0, playerNames.length/2),
request, playerStats);
TopStatAction subTask2 = new TopStatAction(Arrays.copyOfRange(playerNames, playerNames.length/2, playerNames.length),
request, playerStats);
final TopStatAction subTask1 = new TopStatAction(threshold, playerNames.subList(0, playerNames.size()/2), request, playerStats);
final TopStatAction subTask2 = new TopStatAction(threshold, playerNames.subList(playerNames.size()/2, playerNames.size()), request, playerStats);
//queue and compute all subtasks in the right order
invokeAll(subTask1, subTask2);
@ -49,9 +49,10 @@ public class TopStatAction extends RecursiveAction {
}
private void getStatsDirectly() throws IllegalArgumentException, ConcurrentModificationException {
Bukkit.getLogger().info("ArrayCopy Length: " + playerNames.length);
for (String playerName : playerNames) {
try {
Iterator<String> iterator = playerNames.iterator();
while (iterator.hasNext()) {
String playerName = iterator.next();
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName);
int statistic = 0;
switch (request.getStatType()) {
@ -60,16 +61,17 @@ public class TopStatAction extends RecursiveAction {
case BLOCK -> statistic = player.getStatistic(request.getStatEnum(), request.getBlock());
case ITEM -> statistic = player.getStatistic(request.getStatEnum(), request.getItem());
}
if (statistic > 0) {
playerStats.put(playerName, statistic);
}
} catch (IllegalArgumentException ignored) {
}
catch (ConcurrentModificationException e) {
Bukkit.getLogger().warning("A ConcurrentModificationException has occurred");
} catch (NoSuchElementException e) {
e.printStackTrace();
} catch (IllegalArgumentException ignored) {
} catch (ConcurrentModificationException e) {
Bukkit.getLogger().warning("A ConcurrentModificationException has occurred" + e.getCause());
e.printStackTrace();
throw new ConcurrentModificationException(e.toString());
}
}
}
}

View File

@ -9,6 +9,7 @@ import java.util.concurrent.ConcurrentHashMap;
public class OfflinePlayerHandler {
private static ConcurrentHashMap<String, UUID> offlinePlayerUUIDs;
private static ArrayList<String> playerNames;
private OfflinePlayerHandler() {
}
@ -23,7 +24,7 @@ public class OfflinePlayerHandler {
}
public static ArrayList<String> getOfflinePlayerNames() {
return new ArrayList<>(offlinePlayerUUIDs.keySet());
return playerNames;
}
/**
@ -32,6 +33,7 @@ public class OfflinePlayerHandler {
*/
public static void updateOfflinePlayerList(ConcurrentHashMap<String, UUID> playerList) {
offlinePlayerUUIDs = playerList;
playerNames = Collections.list(offlinePlayerUUIDs.keys());
}
/**