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
+15 -2
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);
+10 -1
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() {
+31 -7
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
+4
View File
@@ -266,4 +266,8 @@ public class Wakeup {
} }
} }
public static void onUnload(String worldName) {
wakeups.removeIf(wakeup -> wakeup.loc.getWorld().getName().equals(worldName));
}
} }
+80 -11
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()) {
final FileConfiguration finalData = data;
final List<World> worlds = P.p.getServer().getWorlds();
P.p.getServer().getScheduler().runTaskAsynchronously(P.p, () -> {
if (!acquireDataLoadMutex()) return; // Tries for 60 sec
try {
for (World world : worlds) {
P.p.log("World Init: " + world.getName());
if (world.getName().startsWith("DXL_")) { if (world.getName().startsWith("DXL_")) {
loadWorldData(BUtil.getDxlName(world.getName()), world, data); loadWorldData(BUtil.getDxlName(world.getName()), world, finalData);
} else { } else {
loadWorldData(world.getUID().toString(), world, data); 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();
} }
} }
+15 -3
View File
@@ -34,14 +34,21 @@ public class DataSave extends BukkitRunnable {
@Override @Override
public void run() { public void run() {
long saveTime = System.nanoTime(); long saveTime = System.nanoTime();
// Mutex has been acquired in ReadOldData
try {
FileConfiguration oldData; FileConfiguration oldData;
if (read != null) { if (read != null) {
if (!read.done) { if (!read.done) {
// Wait for async thread to load old data // Wait for async thread to load old data
if (System.currentTimeMillis() - time > 30000) { if (System.currentTimeMillis() - time > 50000) {
P.p.errorLog("Old Data took too long to load!"); P.p.errorLog("Old Data took too long to load! Mutex: " + BData.dataMutex.get());
try {
cancel(); cancel();
return; read.cancel();
} catch (IllegalStateException ignored) {
}
running = null;
BData.dataMutex.set(0);
} }
return; return;
} }
@@ -104,6 +111,11 @@ public class DataSave extends BukkitRunnable {
} 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);
}
} }
// Finish the collection of data immediately // Finish the collection of data immediately
@@ -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();
+2 -1
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);
} }
} }
@@ -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_")) { if (world.getName().startsWith("DXL_")) {
BData.loadWorldData(BUtil.getDxlName(world.getName()), world, null); BData.loadWorldData(BUtil.getDxlName(world.getName()), world, null);
} else { } else {
BData.loadWorldData(world.getUID().toString(), world, null); BData.loadWorldData(world.getUID().toString(), world, null);
} }
} catch (Exception e) {
e.printStackTrace();
} finally {
BData.releaseDataLoadMutex();
}
});
} }
@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);
} }
} }