Improved islands async deletion from database (#605)

Addresses https://github.com/BentoBoxWorld/BentoBox/issues/591
This commit is contained in:
tastybento 2019-03-11 00:41:41 -07:00 committed by Florian CUNY
parent 55b813b746
commit 75bbc13cf5
10 changed files with 85 additions and 43 deletions

View File

@ -120,8 +120,7 @@ public abstract class AbstractDatabaseHandler<T> {
/** /**
* Attempts to delete the object with the uniqueId * Attempts to delete the object with the uniqueId
* @param uniqueId - uniqueId of object * @param uniqueId - uniqueId of object
* @return true if successful, false if there is no such uniqueId
* @since 1.1 * @since 1.1
*/ */
public abstract boolean deleteID(String uniqueId); public abstract void deleteID(String uniqueId);
} }

View File

@ -79,9 +79,9 @@ public class Database<T> {
} }
/** /**
* Save config object * Save config object. Saving may be done async.
* @param instance to save * @param instance to save
* @return true if successfully saved * @return true if no immediate errors. If async, errors may occur later.
*/ */
public boolean saveObject(T instance) { public boolean saveObject(T instance) {
try { try {
@ -106,11 +106,10 @@ public class Database<T> {
/** /**
* Attempts to delete the object with the uniqueId * Attempts to delete the object with the uniqueId
* @param uniqueId - uniqueId of object * @param uniqueId - uniqueId of object
* @return true if successful, false if there is no such uniqueId
* @since 1.1 * @since 1.1
*/ */
public boolean deleteID(String uniqueId) { public void deleteID(String uniqueId) {
return handler.deleteID(uniqueId); handler.deleteID(uniqueId);
} }
/** /**

View File

@ -127,7 +127,7 @@ public class JSONDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
} }
@Override @Override
public boolean deleteID(String uniqueId) { public void deleteID(String uniqueId) {
// The filename of the JSON file is the value of uniqueId field plus .json. Sometimes the .json is already appended. // The filename of the JSON file is the value of uniqueId field plus .json. Sometimes the .json is already appended.
if (!uniqueId.endsWith(JSON)) { if (!uniqueId.endsWith(JSON)) {
uniqueId = uniqueId + JSON; uniqueId = uniqueId + JSON;
@ -140,12 +140,10 @@ public class JSONDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
File file = new File(tableFolder, uniqueId); File file = new File(tableFolder, uniqueId);
try { try {
Files.delete(file.toPath()); Files.delete(file.toPath());
return true;
} catch (IOException e) { } catch (IOException e) {
plugin.logError("Could not delete json database object! " + file.getName() + " - " + e.getMessage()); plugin.logError("Could not delete json database object! " + file.getName() + " - " + e.getMessage());
} }
} }
return false;
} }
@Override @Override

View File

@ -158,7 +158,7 @@ public class MariaDBDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
} }
@Override @Override
public boolean deleteID(String uniqueId) { public void deleteID(String uniqueId) {
String sb = "DELETE FROM `" + String sb = "DELETE FROM `" +
dataObject.getCanonicalName() + dataObject.getCanonicalName() +
"` WHERE uniqueId = ?"; "` WHERE uniqueId = ?";
@ -166,10 +166,8 @@ public class MariaDBDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
// UniqueId needs to be placed in quotes // UniqueId needs to be placed in quotes
preparedStatement.setString(1, "\"" + uniqueId + "\""); preparedStatement.setString(1, "\"" + uniqueId + "\"");
preparedStatement.execute(); preparedStatement.execute();
return preparedStatement.getUpdateCount() > 0;
} catch (Exception e) { } catch (Exception e) {
plugin.logError("Could not delete object " + dataObject.getCanonicalName() + " " + uniqueId + " " + e.getMessage()); plugin.logError("Could not delete object " + dataObject.getCanonicalName() + " " + uniqueId + " " + e.getMessage());
return false;
} }
} }

View File

@ -117,12 +117,11 @@ public class MongoDBDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
} }
@Override @Override
public boolean deleteID(String uniqueId) { public void deleteID(String uniqueId) {
try { try {
return collection.findOneAndDelete(new Document(MONGO_ID, uniqueId)) != null; collection.findOneAndDelete(new Document(MONGO_ID, uniqueId));
} catch (Exception e) { } catch (Exception e) {
plugin.logError("Could not delete object " + dataObject.getCanonicalName() + " " + uniqueId + " " + e.getMessage()); plugin.logError("Could not delete object " + dataObject.getCanonicalName() + " " + uniqueId + " " + e.getMessage());
return false;
} }
} }

View File

@ -42,12 +42,12 @@ public class MySQLDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
private Connection connection; private Connection connection;
/** /**
* FIFO queue for saves. Note that the assumption here is that most database objects will be held * FIFO queue for saves or deletions. Note that the assumption here is that most database objects will be held
* in memory because loading is not handled with this queue. That means that it is theoretically * in memory because loading is not handled with this queue. That means that it is theoretically
* possible to load something before it has been saved. So, in general, load your objects and then * possible to load something before it has been saved. So, in general, load your objects and then
* save them async only when you do not need the data again immediately. * save them async only when you do not need the data again immediately.
*/ */
private Queue<Runnable> saveQueue; private Queue<Runnable> processQueue;
/** /**
* Async save task that runs repeatedly * Async save task that runs repeatedly
@ -72,13 +72,13 @@ public class MySQLDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
} }
// Check if the table exists in the database and if not, create it // Check if the table exists in the database and if not, create it
createSchema(); createSchema();
saveQueue = new ConcurrentLinkedQueue<>(); processQueue = new ConcurrentLinkedQueue<>();
if (plugin.isEnabled()) { if (plugin.isEnabled()) {
asyncSaveTask = Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { asyncSaveTask = Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Loop continuously // Loop continuously
while (plugin.isEnabled() || !saveQueue.isEmpty()) { while (plugin.isEnabled() || !processQueue.isEmpty()) {
while (!saveQueue.isEmpty()) { while (!processQueue.isEmpty()) {
saveQueue.poll().run(); processQueue.poll().run();
} }
// Clear the queue and then sleep // Clear the queue and then sleep
try { try {
@ -189,7 +189,7 @@ public class MySQLDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
String toStore = gson.toJson(instance); String toStore = gson.toJson(instance);
if (plugin.isEnabled()) { if (plugin.isEnabled()) {
// Async // Async
saveQueue.add(() -> store(instance, toStore, sb)); processQueue.add(() -> store(instance, toStore, sb));
} else { } else {
// Sync // Sync
store(instance, toStore, sb); store(instance, toStore, sb);
@ -206,8 +206,19 @@ public class MySQLDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
} }
} }
/* (non-Javadoc)
* @see world.bentobox.bentobox.database.AbstractDatabaseHandler#deleteID(java.lang.String)
*/
@Override @Override
public boolean deleteID(String uniqueId) { public void deleteID(String uniqueId) {
if (plugin.isEnabled()) {
processQueue.add(() -> delete(uniqueId));
} else {
delete(uniqueId);
}
}
private void delete(String uniqueId) {
String sb = "DELETE FROM `" + String sb = "DELETE FROM `" +
dataObject.getCanonicalName() + dataObject.getCanonicalName() +
"` WHERE uniqueId = ?"; "` WHERE uniqueId = ?";
@ -215,10 +226,8 @@ public class MySQLDatabaseHandler<T> extends AbstractJSONDatabaseHandler<T> {
// UniqueId needs to be placed in quotes // UniqueId needs to be placed in quotes
preparedStatement.setString(1, "\"" + uniqueId + "\""); preparedStatement.setString(1, "\"" + uniqueId + "\"");
preparedStatement.execute(); preparedStatement.execute();
return preparedStatement.getUpdateCount() > 0;
} catch (Exception e) { } catch (Exception e) {
plugin.logError("Could not delete object " + dataObject.getCanonicalName() + " " + uniqueId + " " + e.getMessage()); plugin.logError("Could not delete object " + dataObject.getCanonicalName() + " " + uniqueId + " " + e.getMessage());
return false;
} }
} }

View File

@ -44,11 +44,15 @@ import world.bentobox.bentobox.util.Util;
* Managed by IslandsManager * Managed by IslandsManager
* Responsible for team information as well. * Responsible for team information as well.
* *
* @author Tastybento * @author tastybento
* @author Poslovitch * @author Poslovitch
*/ */
public class Island implements DataObject { public class Island implements DataObject {
// True if this island is deleted and pending deletion from the database
@Expose
private boolean deleted = false;
@Expose @Expose
private String uniqueId = UUID.randomUUID().toString(); private String uniqueId = UUID.randomUUID().toString();
@ -850,4 +854,18 @@ public class Island implements DataObject {
public void setDoNotLoad(boolean doNotLoad) { public void setDoNotLoad(boolean doNotLoad) {
this.doNotLoad = doNotLoad; this.doNotLoad = doNotLoad;
} }
/**
* @return the deleted
*/
public boolean isDeleted() {
return deleted;
}
/**
* @param deleted the deleted to set
*/
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
} }

View File

@ -65,7 +65,7 @@ public class YamlDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
* possible to load something before it has been saved. So, in general, load your objects and then * possible to load something before it has been saved. So, in general, load your objects and then
* save them async only when you do not need the data again immediately. * save them async only when you do not need the data again immediately.
*/ */
private Queue<Runnable> saveQueue; private Queue<Runnable> processQueue;
/** /**
* Async save task that runs repeatedly * Async save task that runs repeatedly
@ -87,13 +87,13 @@ public class YamlDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
*/ */
YamlDatabaseHandler(BentoBox plugin, Class<T> type, DatabaseConnector databaseConnector) { YamlDatabaseHandler(BentoBox plugin, Class<T> type, DatabaseConnector databaseConnector) {
super(plugin, type, databaseConnector); super(plugin, type, databaseConnector);
saveQueue = new ConcurrentLinkedQueue<>(); processQueue = new ConcurrentLinkedQueue<>();
if (plugin.isEnabled()) { if (plugin.isEnabled()) {
asyncSaveTask = Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { asyncSaveTask = Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Loop continuously // Loop continuously
while (plugin.isEnabled() || !saveQueue.isEmpty()) { while (plugin.isEnabled() || !processQueue.isEmpty()) {
while (!saveQueue.isEmpty()) { while (!processQueue.isEmpty()) {
saveQueue.poll().run(); processQueue.poll().run();
} }
// Clear the queue and then sleep // Clear the queue and then sleep
try { try {
@ -439,7 +439,7 @@ public class YamlDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
private void save(String name, String data, String path, Map<String, String> yamlComments) { private void save(String name, String data, String path, Map<String, String> yamlComments) {
if (plugin.isEnabled()) { if (plugin.isEnabled()) {
// Async // Async
saveQueue.add(() -> ((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments)); processQueue.add(() -> ((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments));
} else { } else {
// Sync for shutdown // Sync for shutdown
((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments); ((YamlDatabaseConnector)databaseConnector).saveYamlFile(data, path, name, yamlComments);
@ -640,7 +640,15 @@ public class YamlDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
} }
@Override @Override
public boolean deleteID(String uniqueId) { public void deleteID(String uniqueId) {
if (plugin.isEnabled()) {
processQueue.add(() -> delete(uniqueId));
} else {
delete(uniqueId);
}
}
private void delete(String uniqueId) {
// The filename of the YAML file is the value of uniqueId field plus .yml. Sometimes the .yml is already appended. // The filename of the YAML file is the value of uniqueId field plus .yml. Sometimes the .yml is already appended.
if (!uniqueId.endsWith(YML)) { if (!uniqueId.endsWith(YML)) {
uniqueId = uniqueId + YML; uniqueId = uniqueId + YML;
@ -653,12 +661,10 @@ public class YamlDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
File file = new File(tableFolder, uniqueId); File file = new File(tableFolder, uniqueId);
try { try {
Files.delete(file.toPath()); Files.delete(file.toPath());
return true;
} catch (IOException e) { } catch (IOException e) {
plugin.logError("Could not delete yml database object! " + file.getName() + " - " + e.getMessage()); plugin.logError("Could not delete yml database object! " + file.getName() + " - " + e.getMessage());
} }
} }
return false;
} }
/* (non-Javadoc) /* (non-Javadoc)
@ -668,7 +674,7 @@ public class YamlDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
public void deleteObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException { public void deleteObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException {
// Null check // Null check
if (instance == null) { if (instance == null) {
plugin.logError("YAM database request to delete a null. "); plugin.logError("YAML database request to delete a null. ");
return; return;
} }
if (!(instance instanceof DataObject)) { if (!(instance instanceof DataObject)) {

View File

@ -34,6 +34,7 @@ import world.bentobox.bentobox.api.events.IslandBaseEvent;
import world.bentobox.bentobox.api.events.island.IslandEvent; import world.bentobox.bentobox.api.events.island.IslandEvent;
import world.bentobox.bentobox.api.events.island.IslandEvent.Reason; import world.bentobox.bentobox.api.events.island.IslandEvent.Reason;
import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.localization.TextVariables;
import world.bentobox.bentobox.api.logs.LogEntry;
import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.Database; import world.bentobox.bentobox.database.Database;
import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.database.objects.Island;
@ -72,8 +73,12 @@ public class IslandsManager {
// Island Cache // Island Cache
@NonNull @NonNull
private IslandCache islandCache; private IslandCache islandCache;
// Quarantined islands
@NonNull @NonNull
private Map<UUID, List<Island>> quarantineCache; private Map<UUID, List<Island>> quarantineCache;
// Deleted islands
@NonNull
private List<String> deletedIslands;
/** /**
* Islands Manager * Islands Manager
@ -87,6 +92,9 @@ public class IslandsManager {
quarantineCache = new HashMap<>(); quarantineCache = new HashMap<>();
spawn = new HashMap<>(); spawn = new HashMap<>();
last = new HashMap<>(); last = new HashMap<>();
// This list should always be empty unless database deletion failed
// In that case a purge utility may be required in the future
deletedIslands = new ArrayList<>();
} }
/** /**
@ -266,7 +274,13 @@ public class IslandsManager {
if (removeBlocks) { if (removeBlocks) {
// Remove island from the cache // Remove island from the cache
islandCache.deleteIslandFromCache(island); islandCache.deleteIslandFromCache(island);
// Remove the island from the database // Log the deletion (it shouldn't matter but may be useful)
island.log(new LogEntry.Builder("DELETED").build());
// Set the delete flag which will prevent it from being loaded even if database deletion fails
island.setDeleted(true);
// Save the island
handler.saveObject(island);
// Delete the island
handler.deleteObject(island); handler.deleteObject(island);
// Remove players from island // Remove players from island
removePlayersFromIsland(island); removePlayersFromIsland(island);
@ -703,9 +717,12 @@ public class IslandsManager {
islandCache.clear(); islandCache.clear();
quarantineCache.clear(); quarantineCache.clear();
List<Island> toQuarantine = new ArrayList<>(); List<Island> toQuarantine = new ArrayList<>();
// Only load non-quarantined island // Attempt to load islands
handler.loadObjects().stream().forEach(island -> { handler.loadObjects().stream().forEach(island -> {
if (island.isDoNotLoad() && island.getWorld() != null && island.getCenter() != null) { if (island.isDeleted()) {
// These will be deleted later
deletedIslands.add(island.getUniqueId());
} else if (island.isDoNotLoad() && island.getWorld() != null && island.getCenter() != null) {
// Add to quarantine cache // Add to quarantine cache
quarantineCache.computeIfAbsent(island.getOwner(), k -> new ArrayList<>()).add(island); quarantineCache.computeIfAbsent(island.getOwner(), k -> new ArrayList<>()).add(island);
} else { } else {
@ -809,8 +826,7 @@ public class IslandsManager {
} }
/** /**
* This removes players from an island overworld and nether - used when reseting or deleting an island * This teleports players away from an island - used when reseting or deleting an island
* Mobs are killed when the chunks are refreshed.
* @param island to remove players from * @param island to remove players from
*/ */
public void removePlayersFromIsland(Island island) { public void removePlayersFromIsland(Island island) {