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) {
this.block = block;
bcauldrons.put(block, this);
}
// loading from file
@ -44,7 +43,6 @@ public class BCauldron {
this.block = block;
this.state = state;
this.ingredients = ingredients;
bcauldrons.put(block, this);
}
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
@Nullable
public static BCauldron get(Block block) {
@ -99,6 +111,7 @@ public class BCauldron {
BCauldron bcauldron = get(block);
if (bcauldron == null) {
bcauldron = new BCauldron(block);
BCauldron.bcauldrons.put(block, bcauldron);
}
IngedientAddEvent event = new IngedientAddEvent(player, block, bcauldron, ingredient.clone(), rItem);

View File

@ -59,6 +59,15 @@ public class Barrel implements InventoryHolder {
* load from file
*/
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;
if (isLarge()) {
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;
body = new BarrelBody(this, sign, bounds);
body = new BarrelBody(this, sign, bounds, async);
}
public static void onUpdate() {

View File

@ -30,17 +30,17 @@ public class BarrelBody {
/**
* Loading from file
*/
public BarrelBody(Barrel barrel, byte signoffset, BoundingBox bounds) {
public BarrelBody(Barrel barrel, byte signoffset, BoundingBox bounds, boolean async) {
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
// This will only be done in those extreme cases.
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);
}
regenerateBounds();
} else {
this.bounds = bounds;
}
@ -79,6 +79,15 @@ public class BarrelBody {
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
*/
@ -218,6 +227,21 @@ public class BarrelBody {
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.
* <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.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
public class BData {
public static AtomicInteger dataMutex = new AtomicInteger(0); // -1 = Saving, 0 = Free, >= 1 = Loading
// load all Data
public static void readData() {
@ -136,7 +139,7 @@ public class BData {
P.p.bad = 0;
P.p.terr = 0;
if (!Brew.noLegacy()) {
for (Brew brew : Brew.legacyPotions.values()) {
for (int i = Brew.legacyPotions.size(); i > 0; i--) {
P.p.metricsForCreate(false);
}
}
@ -186,13 +189,27 @@ public class BData {
}
}
for (World world : P.p.getServer().getWorlds()) {
if (world.getName().startsWith("DXL_")) {
loadWorldData(BUtil.getDxlName(world.getName()), world, data);
} else {
loadWorldData(world.getUID().toString(), world, data);
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_")) {
loadWorldData(BUtil.getDxlName(world.getName()), world, finalData);
} else {
loadWorldData(world.getUID().toString(), world, finalData);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
releaseDataLoadMutex();
}
}
});
} else {
P.p.log("No data.yml found, will create new one!");
@ -266,6 +283,7 @@ public class BData {
}
// load Block locations of given world
// can be run async
public static void loadWorldData(String uuid, World world, FileConfiguration data) {
if (data == null) {
@ -278,6 +296,7 @@ public class BData {
}
// loading BCauldron
final Map<Block, BCauldron> initCauldrons = new HashMap<>();
if (data.contains("BCauldron." + uuid)) {
ConfigurationSection section = data.getConfigurationSection("BCauldron." + uuid);
for (String cauldron : section.getKeys(false)) {
@ -291,7 +310,7 @@ public class BData {
BIngredients ingredients = loadCauldronIng(section, cauldron + ".ingredients");
int state = section.getInt(cauldron + ".state", 0);
new BCauldron(worldBlock, ingredients, state);
initCauldrons.put(worldBlock, new BCauldron(worldBlock, ingredients, state));
} else {
P.p.errorLog("Incomplete Block-Data in data.yml: " + section.getCurrentPath() + "." + cauldron);
}
@ -302,6 +321,8 @@ public class BData {
}
// loading Barrel
final List<Barrel> initBarrels = new ArrayList<>();
final List<Barrel> initBadBarrels = new ArrayList<>();
if (data.contains("Barrel." + uuid)) {
ConfigurationSection section = data.getConfigurationSection("Barrel." + uuid);
for (String barrel : section.getKeys(false)) {
@ -346,16 +367,17 @@ public class BData {
Barrel b;
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 {
// 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) {
Barrel.barrels.add(b);
initBarrels.add(b);
} else {
// The Barrel Bounds need recreating, as they were missing or corrupt
initBadBarrels.add(b);
}
} else {
@ -368,6 +390,7 @@ public class BData {
}
// loading Wakeup
final List<Wakeup> initWakeups = new ArrayList<>();
if (data.contains("Wakeup." + uuid)) {
ConfigurationSection section = data.getConfigurationSection("Wakeup." + uuid);
for (String wakeup : section.getKeys(false)) {
@ -384,7 +407,7 @@ public class BData {
float yaw = NumberUtils.toFloat(splitted[4]);
Location location = new Location(world, x, y, z, yaw, pitch);
Wakeup.wakeups.add(new Wakeup(location));
initWakeups.add(new Wakeup(location));
} else {
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
public void run() {
long saveTime = System.nanoTime();
FileConfiguration oldData;
if (read != null) {
if (!read.done) {
// Wait for async thread to load old data
if (System.currentTimeMillis() - time > 30000) {
P.p.errorLog("Old Data took too long to load!");
cancel();
// Mutex has been acquired in ReadOldData
try {
FileConfiguration oldData;
if (read != null) {
if (!read.done) {
// Wait for async thread to load old data
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;
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("MCBarrelTime", MCBarrel.mcBarrelTime);
configFile.set("installTime", Brew.installTime);
configFile.set("MCBarrelTime", MCBarrel.mcBarrelTime);
Brew.writePrevSeeds(configFile);
Brew.writePrevSeeds(configFile);
List<Integer> brewsCreated = new ArrayList<>(7);
brewsCreated.add(P.p.brewsCreated);
brewsCreated.add(P.p.brewsCreatedCmd);
brewsCreated.add(P.p.exc);
brewsCreated.add(P.p.good);
brewsCreated.add(P.p.norm);
brewsCreated.add(P.p.bad);
brewsCreated.add(P.p.terr);
configFile.set("brewsCreated", brewsCreated);
configFile.set("brewsCreatedH", brewsCreated.hashCode());
List<Integer> brewsCreated = new ArrayList<>(7);
brewsCreated.add(P.p.brewsCreated);
brewsCreated.add(P.p.brewsCreatedCmd);
brewsCreated.add(P.p.exc);
brewsCreated.add(P.p.good);
brewsCreated.add(P.p.norm);
brewsCreated.add(P.p.bad);
brewsCreated.add(P.p.terr);
configFile.set("brewsCreated", brewsCreated);
configFile.set("brewsCreatedH", brewsCreated.hashCode());
if (!Brew.legacyPotions.isEmpty()) {
Brew.saveLegacy(configFile.createSection("Brew"));
}
if (!Brew.legacyPotions.isEmpty()) {
Brew.saveLegacy(configFile.createSection("Brew"));
}
if (!BCauldron.bcauldrons.isEmpty() || oldData.contains("BCauldron")) {
BCauldron.save(configFile.createSection("BCauldron"), oldData.getConfigurationSection("BCauldron"));
}
if (!BCauldron.bcauldrons.isEmpty() || oldData.contains("BCauldron")) {
BCauldron.save(configFile.createSection("BCauldron"), oldData.getConfigurationSection("BCauldron"));
}
if (!Barrel.barrels.isEmpty() || oldData.contains("Barrel")) {
Barrel.save(configFile.createSection("Barrel"), oldData.getConfigurationSection("Barrel"));
}
if (!Barrel.barrels.isEmpty() || oldData.contains("Barrel")) {
Barrel.save(configFile.createSection("Barrel"), oldData.getConfigurationSection("Barrel"));
}
if (!BPlayer.isEmpty()) {
BPlayer.save(configFile.createSection("Player"));
}
if (!BPlayer.isEmpty()) {
BPlayer.save(configFile.createSection("Player"));
}
if (!Wakeup.wakeups.isEmpty() || oldData.contains("Wakeup")) {
Wakeup.save(configFile.createSection("Wakeup"), oldData.getConfigurationSection("Wakeup"));
}
if (!Wakeup.wakeups.isEmpty() || oldData.contains("Wakeup")) {
Wakeup.save(configFile.createSection("Wakeup"), oldData.getConfigurationSection("Wakeup"));
}
saveWorldNames(configFile, oldData.getConfigurationSection("Worlds"));
configFile.set("Version", dataVersion);
saveWorldNames(configFile, oldData.getConfigurationSection("Worlds"));
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()) {
P.p.getServer().getScheduler().runTaskAsynchronously(P.p, new WriteData(configFile));
} else {
new WriteData(configFile).run();
if (P.p.isEnabled()) {
P.p.getServer().getScheduler().runTaskAsynchronously(P.p, new WriteData(configFile));
} else {
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")
@Override
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");
if (!datafile.exists()) {
data = new YamlConfiguration();

View File

@ -25,11 +25,12 @@ public class WriteData implements Runnable {
try {
data.save(datafile);
} catch (IOException e) {
} catch (Exception e) {
e.printStackTrace();
}
DataSave.lastSave = 1;
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.Barrel;
import com.dre.brewery.P;
import com.dre.brewery.Wakeup;
import com.dre.brewery.utility.BUtil;
import com.dre.brewery.filedata.BData;
import com.dre.brewery.filedata.DataSave;
@ -16,20 +18,35 @@ public class WorldListener implements Listener {
@EventHandler
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)
public void onWorldUnload(WorldUnloadEvent event) {
DataSave.save(true);
Barrel.onUnload(event.getWorld().getName());
BCauldron.onUnload(event.getWorld().getName());
String worldName = event.getWorld().getName();
Barrel.onUnload(worldName);
BCauldron.onUnload(worldName);
Wakeup.onUnload(worldName);
}
}