Implemented Async World-Data Loading

Uses Mutex to assure no concurrent loading and saving
Merges Loaded data in Main Thread
This commit is contained in:
Sn0wStorm 2020-04-07 12:45:55 +02:00
parent f4bfdfa595
commit cae2b9dfff
9 changed files with 251 additions and 86 deletions

View File

@ -36,7 +36,6 @@ public class BCauldron {
public BCauldron(Block block) { public BCauldron(Block block) {
this.block = block; this.block = block;
bcauldrons.put(block, this);
} }
// loading from file // loading from file
@ -44,7 +43,6 @@ public class BCauldron {
this.block = block; this.block = block;
this.state = state; this.state = state;
this.ingredients = ingredients; this.ingredients = ingredients;
bcauldrons.put(block, this);
} }
public void onUpdate() { public void onUpdate() {
@ -74,6 +72,20 @@ public class BCauldron {
} }
} }
/**
* Get the Block that this BCauldron represents
*/
public Block getBlock() {
return block;
}
/**
* Get the State (Time in Minutes) that this Cauldron currently has
*/
public int getState() {
return state;
}
// get cauldron by Block // get cauldron by Block
@Nullable @Nullable
public static BCauldron get(Block block) { public static BCauldron get(Block block) {
@ -99,6 +111,7 @@ public class BCauldron {
BCauldron bcauldron = get(block); BCauldron bcauldron = get(block);
if (bcauldron == null) { if (bcauldron == null) {
bcauldron = new BCauldron(block); bcauldron = new BCauldron(block);
BCauldron.bcauldrons.put(block, bcauldron);
} }
IngedientAddEvent event = new IngedientAddEvent(player, block, bcauldron, ingredient.clone(), rItem); IngedientAddEvent event = new IngedientAddEvent(player, block, bcauldron, ingredient.clone(), rItem);

View File

@ -59,6 +59,15 @@ public class Barrel implements InventoryHolder {
* load from file * load from file
*/ */
public Barrel(Block spigot, byte sign, BoundingBox bounds, Map<String, Object> items, float time) { public Barrel(Block spigot, byte sign, BoundingBox bounds, Map<String, Object> items, float time) {
this(spigot, sign, bounds, items, time, false);
}
/**
* Load from File
* <p>If async: true, The Barrel Bounds will not be recreated when missing/corrupt, getBody().getBounds() will be null if it needs recreating
*
*/
public Barrel(Block spigot, byte sign, BoundingBox bounds, Map<String, Object> items, float time, boolean async) {
this.spigot = spigot; this.spigot = spigot;
if (isLarge()) { if (isLarge()) {
this.inventory = P.p.getServer().createInventory(this, 27, P.p.languageReader.get("Etc_Barrel")); this.inventory = P.p.getServer().createInventory(this, 27, P.p.languageReader.get("Etc_Barrel"));
@ -74,7 +83,7 @@ public class Barrel implements InventoryHolder {
} }
this.time = time; this.time = time;
body = new BarrelBody(this, sign, bounds); body = new BarrelBody(this, sign, bounds, async);
} }
public static void onUpdate() { public static void onUpdate() {

View File

@ -30,17 +30,17 @@ public class BarrelBody {
/** /**
* Loading from file * Loading from file
*/ */
public BarrelBody(Barrel barrel, byte signoffset, BoundingBox bounds) { public BarrelBody(Barrel barrel, byte signoffset, BoundingBox bounds, boolean async) {
this(barrel, signoffset); this(barrel, signoffset);
if (bounds == null || bounds.area() > 64 ) { if (boundsSeemBad(bounds)) {
if (async) {
this.bounds = null;
return;
}
// If loading from old data, or block locations are missing, or other error, regenerate BoundingBox // If loading from old data, or block locations are missing, or other error, regenerate BoundingBox
// This will only be done in those extreme cases. // This will only be done in those extreme cases.
P.p.log("Regenerating Barrel BoundingBox: " + (bounds == null ? "was null" : "area=" + bounds.area())); regenerateBounds();
Block broken = getBrokenBlock(true);
if (broken != null) {
barrel.remove(broken, null, true);
}
} else { } else {
this.bounds = bounds; this.bounds = bounds;
} }
@ -79,6 +79,15 @@ public class BarrelBody {
signoffset = 0; signoffset = 0;
} }
/**
* Quick check if the bounds are valid or seem corrupt
*/
public static boolean boundsSeemBad(BoundingBox bounds) {
if (bounds == null) return true;
long area = bounds.area();
return area > 64 || area < 4;
}
/** /**
* direction of the barrel from the spigot * direction of the barrel from the spigot
*/ */
@ -218,6 +227,21 @@ public class BarrelBody {
return block; return block;
} }
/**
* Regenerate the Barrel Bounds.
*
* @return true if successful, false if Barrel was broken and should be removed.
*/
public boolean regenerateBounds() {
P.p.log("Regenerating Barrel BoundingBox: " + (bounds == null ? "was null" : "area=" + bounds.area()));
Block broken = getBrokenBlock(true);
if (broken != null) {
barrel.remove(broken, null, true);
return false;
}
return true;
}
/** /**
* returns null if Barrel is correctly placed; the block that is missing when not. * returns null if Barrel is correctly placed; the block that is missing when not.
* <p>the barrel needs to be formed correctly * <p>the barrel needs to be formed correctly

View File

@ -266,4 +266,8 @@ public class Wakeup {
} }
} }
public static void onUnload(String worldName) {
wakeups.removeIf(wakeup -> wakeup.loc.getWorld().getName().equals(worldName));
}
} }

View File

@ -23,9 +23,12 @@ import java.io.DataInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
public class BData { public class BData {
public static AtomicInteger dataMutex = new AtomicInteger(0); // -1 = Saving, 0 = Free, >= 1 = Loading
// load all Data // load all Data
public static void readData() { public static void readData() {
@ -136,7 +139,7 @@ public class BData {
P.p.bad = 0; P.p.bad = 0;
P.p.terr = 0; P.p.terr = 0;
if (!Brew.noLegacy()) { if (!Brew.noLegacy()) {
for (Brew brew : Brew.legacyPotions.values()) { for (int i = Brew.legacyPotions.size(); i > 0; i--) {
P.p.metricsForCreate(false); P.p.metricsForCreate(false);
} }
} }
@ -186,13 +189,27 @@ public class BData {
} }
} }
for (World world : P.p.getServer().getWorlds()) {
if (world.getName().startsWith("DXL_")) { final FileConfiguration finalData = data;
loadWorldData(BUtil.getDxlName(world.getName()), world, data); final List<World> worlds = P.p.getServer().getWorlds();
} else { P.p.getServer().getScheduler().runTaskAsynchronously(P.p, () -> {
loadWorldData(world.getUID().toString(), world, data); if (!acquireDataLoadMutex()) return; // Tries for 60 sec
try {
for (World world : worlds) {
P.p.log("World Init: " + world.getName());
if (world.getName().startsWith("DXL_")) {
loadWorldData(BUtil.getDxlName(world.getName()), world, finalData);
} else {
loadWorldData(world.getUID().toString(), world, finalData);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
releaseDataLoadMutex();
} }
} });
} else { } else {
P.p.log("No data.yml found, will create new one!"); P.p.log("No data.yml found, will create new one!");
@ -266,6 +283,7 @@ public class BData {
} }
// load Block locations of given world // load Block locations of given world
// can be run async
public static void loadWorldData(String uuid, World world, FileConfiguration data) { public static void loadWorldData(String uuid, World world, FileConfiguration data) {
if (data == null) { if (data == null) {
@ -278,6 +296,7 @@ public class BData {
} }
// loading BCauldron // loading BCauldron
final Map<Block, BCauldron> initCauldrons = new HashMap<>();
if (data.contains("BCauldron." + uuid)) { if (data.contains("BCauldron." + uuid)) {
ConfigurationSection section = data.getConfigurationSection("BCauldron." + uuid); ConfigurationSection section = data.getConfigurationSection("BCauldron." + uuid);
for (String cauldron : section.getKeys(false)) { for (String cauldron : section.getKeys(false)) {
@ -291,7 +310,7 @@ public class BData {
BIngredients ingredients = loadCauldronIng(section, cauldron + ".ingredients"); BIngredients ingredients = loadCauldronIng(section, cauldron + ".ingredients");
int state = section.getInt(cauldron + ".state", 0); int state = section.getInt(cauldron + ".state", 0);
new BCauldron(worldBlock, ingredients, state); initCauldrons.put(worldBlock, new BCauldron(worldBlock, ingredients, state));
} else { } else {
P.p.errorLog("Incomplete Block-Data in data.yml: " + section.getCurrentPath() + "." + cauldron); P.p.errorLog("Incomplete Block-Data in data.yml: " + section.getCurrentPath() + "." + cauldron);
} }
@ -302,6 +321,8 @@ public class BData {
} }
// loading Barrel // loading Barrel
final List<Barrel> initBarrels = new ArrayList<>();
final List<Barrel> initBadBarrels = new ArrayList<>();
if (data.contains("Barrel." + uuid)) { if (data.contains("Barrel." + uuid)) {
ConfigurationSection section = data.getConfigurationSection("Barrel." + uuid); ConfigurationSection section = data.getConfigurationSection("Barrel." + uuid);
for (String barrel : section.getKeys(false)) { for (String barrel : section.getKeys(false)) {
@ -346,16 +367,17 @@ public class BData {
Barrel b; Barrel b;
if (invSection != null) { if (invSection != null) {
b = new Barrel(block, sign, box, invSection.getValues(true), time); b = new Barrel(block, sign, box, invSection.getValues(true), time, true);
} else { } else {
// Barrel has no inventory // Barrel has no inventory
b = new Barrel(block, sign, box, null, time); b = new Barrel(block, sign, box, null, time, true);
} }
// In case Barrel Block locations were missing and could not be recreated: do not add the barrel
if (b.getBody().getBounds() != null) { if (b.getBody().getBounds() != null) {
Barrel.barrels.add(b); initBarrels.add(b);
} else {
// The Barrel Bounds need recreating, as they were missing or corrupt
initBadBarrels.add(b);
} }
} else { } else {
@ -368,6 +390,7 @@ public class BData {
} }
// loading Wakeup // loading Wakeup
final List<Wakeup> initWakeups = new ArrayList<>();
if (data.contains("Wakeup." + uuid)) { if (data.contains("Wakeup." + uuid)) {
ConfigurationSection section = data.getConfigurationSection("Wakeup." + uuid); ConfigurationSection section = data.getConfigurationSection("Wakeup." + uuid);
for (String wakeup : section.getKeys(false)) { for (String wakeup : section.getKeys(false)) {
@ -384,7 +407,7 @@ public class BData {
float yaw = NumberUtils.toFloat(splitted[4]); float yaw = NumberUtils.toFloat(splitted[4]);
Location location = new Location(world, x, y, z, yaw, pitch); Location location = new Location(world, x, y, z, yaw, pitch);
Wakeup.wakeups.add(new Wakeup(location)); initWakeups.add(new Wakeup(location));
} else { } else {
P.p.errorLog("Incomplete Location-Data in data.yml: " + section.getCurrentPath() + "." + wakeup); P.p.errorLog("Incomplete Location-Data in data.yml: " + section.getCurrentPath() + "." + wakeup);
@ -393,5 +416,51 @@ public class BData {
} }
} }
// Merge Loaded Data in Main Thred
P.p.getServer().getScheduler().runTask(P.p, () -> {
if (P.p.getServer().getWorld(world.getUID()) == null) {
return;
}
if (!initCauldrons.isEmpty()) {
BCauldron.bcauldrons.putAll(initCauldrons);
}
if (!initBarrels.isEmpty()) {
Barrel.barrels.addAll(initBarrels);
}
if (!initBadBarrels.isEmpty()) {
for (Barrel badBarrel : initBadBarrels) {
if (badBarrel.getBody().regenerateBounds()) {
Barrel.barrels.add(badBarrel);
}
// In case Barrel Block locations were missing and could not be recreated: do not add the barrel
}
}
if (!initWakeups.isEmpty()) {
Wakeup.wakeups.addAll(initWakeups);
}
});
}
public static boolean acquireDataLoadMutex() {
int wait = 0;
// Increment the Data Mutex if it is not -1
while (BData.dataMutex.updateAndGet(i -> i >= 0 ? i + 1 : i) <= 0) {
wait++;
if (wait > 60) {
P.p.errorLog("Could not load World Data, Mutex: " + BData.dataMutex.get());
return false;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
return false;
}
}
return true;
}
public static void releaseDataLoadMutex() {
dataMutex.decrementAndGet();
} }
} }

View File

@ -34,75 +34,87 @@ public class DataSave extends BukkitRunnable {
@Override @Override
public void run() { public void run() {
long saveTime = System.nanoTime(); long saveTime = System.nanoTime();
FileConfiguration oldData; // Mutex has been acquired in ReadOldData
if (read != null) { try {
if (!read.done) { FileConfiguration oldData;
// Wait for async thread to load old data if (read != null) {
if (System.currentTimeMillis() - time > 30000) { if (!read.done) {
P.p.errorLog("Old Data took too long to load!"); // Wait for async thread to load old data
cancel(); if (System.currentTimeMillis() - time > 50000) {
P.p.errorLog("Old Data took too long to load! Mutex: " + BData.dataMutex.get());
try {
cancel();
read.cancel();
} catch (IllegalStateException ignored) {
}
running = null;
BData.dataMutex.set(0);
}
return; return;
} }
return; oldData = read.getData();
} else {
oldData = new YamlConfiguration();
}
try {
cancel();
} catch (IllegalStateException ignored) {
} }
oldData = read.getData();
} else {
oldData = new YamlConfiguration();
}
try {
cancel();
} catch (IllegalStateException ignored) {
}
FileConfiguration configFile = new YamlConfiguration(); FileConfiguration configFile = new YamlConfiguration();
configFile.set("installTime", Brew.installTime); configFile.set("installTime", Brew.installTime);
configFile.set("MCBarrelTime", MCBarrel.mcBarrelTime); configFile.set("MCBarrelTime", MCBarrel.mcBarrelTime);
Brew.writePrevSeeds(configFile); Brew.writePrevSeeds(configFile);
List<Integer> brewsCreated = new ArrayList<>(7); List<Integer> brewsCreated = new ArrayList<>(7);
brewsCreated.add(P.p.brewsCreated); brewsCreated.add(P.p.brewsCreated);
brewsCreated.add(P.p.brewsCreatedCmd); brewsCreated.add(P.p.brewsCreatedCmd);
brewsCreated.add(P.p.exc); brewsCreated.add(P.p.exc);
brewsCreated.add(P.p.good); brewsCreated.add(P.p.good);
brewsCreated.add(P.p.norm); brewsCreated.add(P.p.norm);
brewsCreated.add(P.p.bad); brewsCreated.add(P.p.bad);
brewsCreated.add(P.p.terr); brewsCreated.add(P.p.terr);
configFile.set("brewsCreated", brewsCreated); configFile.set("brewsCreated", brewsCreated);
configFile.set("brewsCreatedH", brewsCreated.hashCode()); configFile.set("brewsCreatedH", brewsCreated.hashCode());
if (!Brew.legacyPotions.isEmpty()) { if (!Brew.legacyPotions.isEmpty()) {
Brew.saveLegacy(configFile.createSection("Brew")); Brew.saveLegacy(configFile.createSection("Brew"));
} }
if (!BCauldron.bcauldrons.isEmpty() || oldData.contains("BCauldron")) { if (!BCauldron.bcauldrons.isEmpty() || oldData.contains("BCauldron")) {
BCauldron.save(configFile.createSection("BCauldron"), oldData.getConfigurationSection("BCauldron")); BCauldron.save(configFile.createSection("BCauldron"), oldData.getConfigurationSection("BCauldron"));
} }
if (!Barrel.barrels.isEmpty() || oldData.contains("Barrel")) { if (!Barrel.barrels.isEmpty() || oldData.contains("Barrel")) {
Barrel.save(configFile.createSection("Barrel"), oldData.getConfigurationSection("Barrel")); Barrel.save(configFile.createSection("Barrel"), oldData.getConfigurationSection("Barrel"));
} }
if (!BPlayer.isEmpty()) { if (!BPlayer.isEmpty()) {
BPlayer.save(configFile.createSection("Player")); BPlayer.save(configFile.createSection("Player"));
} }
if (!Wakeup.wakeups.isEmpty() || oldData.contains("Wakeup")) { if (!Wakeup.wakeups.isEmpty() || oldData.contains("Wakeup")) {
Wakeup.save(configFile.createSection("Wakeup"), oldData.getConfigurationSection("Wakeup")); Wakeup.save(configFile.createSection("Wakeup"), oldData.getConfigurationSection("Wakeup"));
} }
saveWorldNames(configFile, oldData.getConfigurationSection("Worlds")); saveWorldNames(configFile, oldData.getConfigurationSection("Worlds"));
configFile.set("Version", dataVersion); configFile.set("Version", dataVersion);
collected = true; collected = true;
P.p.debugLog("saving: " + ((System.nanoTime() - saveTime) / 1000000.0) + "ms"); P.p.debugLog("saving: " + ((System.nanoTime() - saveTime) / 1000000.0) + "ms");
if (P.p.isEnabled()) { if (P.p.isEnabled()) {
P.p.getServer().getScheduler().runTaskAsynchronously(P.p, new WriteData(configFile)); P.p.getServer().getScheduler().runTaskAsynchronously(P.p, new WriteData(configFile));
} else { } else {
new WriteData(configFile).run(); new WriteData(configFile).run();
}
// Mutex will be released in WriteData
} catch (Exception e) {
e.printStackTrace();
BData.dataMutex.set(0);
} }
} }

View File

@ -17,6 +17,22 @@ public class ReadOldData extends BukkitRunnable {
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
@Override @Override
public void run() { public void run() {
int wait = 0;
// Set the Data Mutex to -1 if it is 0=Free
while (!BData.dataMutex.compareAndSet(0, -1)) {
if (wait > 300) {
P.p.errorLog("Loading Process active for too long while trying to save! Mutex: " + BData.dataMutex.get());
return;
}
wait++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
return;
}
}
File datafile = new File(P.p.getDataFolder(), "data.yml"); File datafile = new File(P.p.getDataFolder(), "data.yml");
if (!datafile.exists()) { if (!datafile.exists()) {
data = new YamlConfiguration(); data = new YamlConfiguration();

View File

@ -25,11 +25,12 @@ public class WriteData implements Runnable {
try { try {
data.save(datafile); data.save(datafile);
} catch (IOException e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
DataSave.lastSave = 1; DataSave.lastSave = 1;
DataSave.running = null; DataSave.running = null;
BData.dataMutex.set(0);
} }
} }

View File

@ -2,6 +2,8 @@ package com.dre.brewery.listeners;
import com.dre.brewery.BCauldron; import com.dre.brewery.BCauldron;
import com.dre.brewery.Barrel; import com.dre.brewery.Barrel;
import com.dre.brewery.P;
import com.dre.brewery.Wakeup;
import com.dre.brewery.utility.BUtil; import com.dre.brewery.utility.BUtil;
import com.dre.brewery.filedata.BData; import com.dre.brewery.filedata.BData;
import com.dre.brewery.filedata.DataSave; import com.dre.brewery.filedata.DataSave;
@ -16,20 +18,35 @@ public class WorldListener implements Listener {
@EventHandler @EventHandler
public void onWorldLoad(WorldLoadEvent event) { public void onWorldLoad(WorldLoadEvent event) {
World world = event.getWorld(); final World world = event.getWorld();
P.p.log("World Load: " + event.getWorld().getName());
P.p.getServer().getScheduler().runTaskAsynchronously(P.p, () -> {
if (!BData.acquireDataLoadMutex()) return; // Tries for 60 sec
try {
if (world.getName().startsWith("DXL_")) {
BData.loadWorldData(BUtil.getDxlName(world.getName()), world, null);
} else {
BData.loadWorldData(world.getUID().toString(), world, null);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
BData.releaseDataLoadMutex();
}
});
if (world.getName().startsWith("DXL_")) {
BData.loadWorldData(BUtil.getDxlName(world.getName()), world, null);
} else {
BData.loadWorldData(world.getUID().toString(), world, null);
}
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onWorldUnload(WorldUnloadEvent event) { public void onWorldUnload(WorldUnloadEvent event) {
DataSave.save(true); DataSave.save(true);
Barrel.onUnload(event.getWorld().getName()); String worldName = event.getWorld().getName();
BCauldron.onUnload(event.getWorld().getName()); Barrel.onUnload(worldName);
BCauldron.onUnload(worldName);
Wakeup.onUnload(worldName);
} }
} }