diff options
author | Keuin <[email protected]> | 2020-04-27 17:42:07 +0800 |
---|---|---|
committer | keuin <[email protected]> | 2020-04-27 17:42:07 +0800 |
commit | 7444c2b1f281b5b147717ba2a2ed6798c66a057b (patch) | |
tree | 8d8952b2f3aaf1e497e8667b80fc59b91536c822 /src/main/java/com/keuin/kbackupfabric/operation | |
parent | 33857f4bc061d8dc01a6d9d10e108da7cd7c6d18 (diff) |
Refactored code for a better implementation of async and blocking task.
Diffstat (limited to 'src/main/java/com/keuin/kbackupfabric/operation')
14 files changed, 496 insertions, 38 deletions
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/AbstractConfirmableOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/AbstractConfirmableOperation.java deleted file mode 100644 index d666a34..0000000 --- a/src/main/java/com/keuin/kbackupfabric/operation/AbstractConfirmableOperation.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.keuin.kbackupfabric.operation; - -import com.mojang.brigadier.context.CommandContext; -import net.minecraft.server.command.ServerCommandSource; - -public abstract class AbstractConfirmableOperation { - - public static AbstractConfirmableOperation createRestoreOperation(CommandContext<ServerCommandSource> context, String backupName) { - return new RestoreOperation(context, backupName); - } - - public static AbstractConfirmableOperation createDeleteOperation(CommandContext<ServerCommandSource> context, String backupName) { - return new DeleteOperation(context, backupName); - } - - public abstract boolean confirm(); - - @Override - public abstract String toString(); -} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java new file mode 100644 index 0000000..7d3a508 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java @@ -0,0 +1,105 @@ +package com.keuin.kbackupfabric.operation; + +import com.keuin.kbackupfabric.metadata.BackupMetadata; +import com.keuin.kbackupfabric.operation.abstracts.InvokableAsyncBlockingOperation; +import com.keuin.kbackupfabric.util.PrintUtil; +import com.keuin.kbackupfabric.util.ZipUtil; +import com.keuin.kbackupfabric.util.ZipUtilException; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.world.World; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.*; +import static com.keuin.kbackupfabric.util.PrintUtil.msgInfo; + +public class BackupOperation extends InvokableAsyncBlockingOperation { + + private final CommandContext<ServerCommandSource> context; + private final String backupName; + private final Map<World, Boolean> oldWorldsSavingDisabled = new HashMap<>(); + private final BackupMetadata backupMetadata; + private long startTime; + + + public BackupOperation(CommandContext<ServerCommandSource> context, String backupName, BackupMetadata backupMetadata) { + super("BackupWorker"); + this.context = context; + this.backupName = backupName; + this.backupMetadata = backupMetadata; + } + + @Override + protected void async() { + String backupSaveDirectory = ""; + MinecraftServer server = context.getSource().getMinecraftServer(); + try { + //// Do our main backup logic + + // Create backup saving directory + File backupSaveDirectoryFile = getBackupSaveDirectory(server); + backupSaveDirectory = backupSaveDirectoryFile.getName(); + if (!backupSaveDirectoryFile.isDirectory() && !backupSaveDirectoryFile.mkdir()) { + msgInfo(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", backupSaveDirectory)); + return; + } + + // Make zip + String levelPath = getLevelPath(server); + String backupFileName = getBackupFileName(backupName); + PrintUtil.info(String.format("zip(srcPath=%s, destPath=%s)", levelPath, backupSaveDirectoryFile.toString())); + PrintUtil.info("Compressing level ..."); + ZipUtil.makeBackupZip(levelPath, backupSaveDirectoryFile.toString(), backupFileName, backupMetadata); + File backupZipFile = new File(backupSaveDirectoryFile, backupFileName); + + // Restore old autosave switch stat + server.getWorlds().forEach(world -> world.savingDisabled = oldWorldsSavingDisabled.getOrDefault(world, true)); + + // Print finish message: time elapsed and file size + long timeElapsedMillis = System.currentTimeMillis() - startTime; + String msgText = String.format("Backup finished. Time elapsed: %.2fs.", timeElapsedMillis / 1000.0); + try { + msgText += String.format(" File size: %s.", humanFileSize(backupZipFile.length())); + } catch (SecurityException ignored) { + } + PrintUtil.msgInfo(context, msgText, true); + + } catch (SecurityException e) { + msgInfo(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", backupSaveDirectory)); + } catch (IOException | ZipUtilException e) { + msgInfo(context, "Failed to make zip: " + e.getMessage()); + } + } + + @Override + protected boolean sync() { + //// Save world, save old autosave configs + + PrintUtil.broadcast(String.format("Making backup %s, please wait ...", backupName)); + + // Get server + MinecraftServer server = context.getSource().getMinecraftServer(); + + // Save old autosave switch stat temporally + oldWorldsSavingDisabled.clear(); + server.getWorlds().forEach(world -> { + oldWorldsSavingDisabled.put(world, world.savingDisabled); + world.savingDisabled = true; + }); + + // Force to save all player data and worlds + PrintUtil.msgInfo(context, "Saving players ..."); + server.getPlayerManager().saveAllPlayerData(); + PrintUtil.msgInfo(context, "Saving worlds ..."); + server.save(true, true, true); + + // Log start time + startTime = System.currentTimeMillis(); + return true; + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java index be7734d..ee1a57e 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java @@ -1,5 +1,7 @@ package com.keuin.kbackupfabric.operation; +import com.keuin.kbackupfabric.operation.abstracts.InvokableAsyncBlockingOperation; +import com.keuin.kbackupfabric.util.BackupNameSuggestionProvider; import com.keuin.kbackupfabric.util.PrintUtil; import com.mojang.brigadier.context.CommandContext; import net.minecraft.server.MinecraftServer; @@ -14,19 +16,30 @@ import static com.keuin.kbackupfabric.util.PrintUtil.msgErr; import static com.keuin.kbackupfabric.util.PrintUtil.msgInfo; import static org.apache.commons.io.FileUtils.forceDelete; -class DeleteOperation extends AbstractConfirmableOperation { +public class DeleteOperation extends InvokableAsyncBlockingOperation { //private static final Logger LOGGER = LogManager.getLogger(); private final String backupName; private final CommandContext<ServerCommandSource> context; - DeleteOperation(CommandContext<ServerCommandSource> context, String backupName) { + public DeleteOperation(CommandContext<ServerCommandSource> context, String backupName) { + super("BackupDeletingWorker"); this.backupName = backupName; this.context = context; } @Override - public boolean confirm() { + public String toString() { + return String.format("deletion of %s", backupName); + } + + @Override + protected void async() { + delete(); + BackupNameSuggestionProvider.updateCandidateList(); + } + + private void delete() { MinecraftServer server = context.getSource().getMinecraftServer(); String backupFileName = getBackupFileName(backupName); PrintUtil.info("Deleting backup " + backupName); @@ -37,7 +50,7 @@ class DeleteOperation extends AbstractConfirmableOperation { String msg = "Failed to delete file " + backupFileName; PrintUtil.error(msg); msgErr(context, msg); - return false; + return; } try { if (!backupFile.delete()) @@ -48,11 +61,5 @@ class DeleteOperation extends AbstractConfirmableOperation { } while (backupFile.exists()); PrintUtil.info("Deleted backup " + backupName); msgInfo(context, "Deleted backup " + backupName); - return true; - } - - @Override - public String toString() { - return String.format("deletion of %s", backupName); } } diff --git a/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java index 4b129d9..cc19627 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java @@ -1,37 +1,49 @@ package com.keuin.kbackupfabric.operation; +import com.keuin.kbackupfabric.operation.abstracts.InvokableBlockingOperation; import com.keuin.kbackupfabric.util.PrintUtil; -import com.keuin.kbackupfabric.worker.RestoreWorker; +import com.keuin.kbackupfabric.util.ZipUtil; +import com.keuin.kbackupfabric.util.ZipUtilException; import com.mojang.brigadier.context.CommandContext; import net.minecraft.server.MinecraftServer; import net.minecraft.server.command.ServerCommandSource; import java.io.File; +import java.io.IOException; -import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.*; +import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.getBackupFileName; +import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.getBackupSaveDirectory; +import static org.apache.commons.io.FileUtils.forceDelete; -class RestoreOperation extends AbstractConfirmableOperation { +public class RestoreOperation extends InvokableBlockingOperation { //private static final Logger LOGGER = LogManager.getLogger(); private final String backupName; + private final Thread serverThread; + private final String backupFilePath; + private final String levelDirectory; private final CommandContext<ServerCommandSource> context; + private final MinecraftServer server; - RestoreOperation(CommandContext<ServerCommandSource> context, String backupName) { + public RestoreOperation(CommandContext<ServerCommandSource> context, String backupFilePath, String levelDirectory, String backupName) { + server = context.getSource().getMinecraftServer(); this.backupName = backupName; + this.serverThread = server.getThread(); + this.backupFilePath = backupFilePath; + this.levelDirectory = levelDirectory; this.context = context; } @Override - public boolean confirm() { + protected boolean blockingContext() { // do restore to backupName - MinecraftServer server = context.getSource().getMinecraftServer(); PrintUtil.broadcast(String.format("Restoring to previous world %s ...", backupName)); String backupFileName = getBackupFileName(backupName); PrintUtil.debug("Backup file name: " + backupFileName); File backupFile = new File(getBackupSaveDirectory(server), backupFileName); - PrintUtil.msgInfo(context, "Server will shutdown in a few seconds, depended on your world size and the disk speed, the restore progress may take seconds or minutes.", true); + PrintUtil.msgInfo(context, "Server will shutdown in a few seconds, depending on world size and disk speed, the progress may take from seconds to minutes.", true); PrintUtil.msgInfo(context, "Please do not force the server stop, or the level would be broken.", true); PrintUtil.msgInfo(context, "After it shuts down, please restart the server manually.", true); final int WAIT_SECONDS = 10; @@ -42,7 +54,14 @@ class RestoreOperation extends AbstractConfirmableOperation { } } PrintUtil.broadcast("Shutting down ..."); - RestoreWorker.invoke(server, backupFile.getPath(), getLevelPath(server)); + //RestoreWorker worker = new RestoreWorker(server.getThread(), backupFilePath, levelDirectory); + Thread workerThread = new Thread(new WorkerThread(), "RestoreWorker"); + workerThread.start(); + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + server.stop(false); return true; } @@ -50,4 +69,61 @@ class RestoreOperation extends AbstractConfirmableOperation { public String toString() { return String.format("restoration from %s", backupName); } + + private class WorkerThread implements Runnable { + + @Override + public void run() { + try { + // Wait server thread die + PrintUtil.info("Waiting for the server thread to exit ..."); + while (serverThread.isAlive()) { + try { + serverThread.join(); + } catch (InterruptedException ignored) { + } + } + + PrintUtil.info("Wait for 5 seconds ..."); + try { + Thread.sleep(5000); + } catch (InterruptedException ignored) { + } + + // Delete old level + PrintUtil.info("Server stopped. Deleting old level ..."); + File levelDirFile = new File(levelDirectory); + long startTime = System.currentTimeMillis(); + + int failedCounter = 0; + final int MAX_RETRY_TIMES = 20; + while (failedCounter < MAX_RETRY_TIMES) { + System.gc(); + if (!levelDirFile.delete() && levelDirFile.exists()) { + System.gc(); + forceDelete(levelDirFile); // Try to force delete. + } + if (!levelDirFile.exists()) + break; + ++failedCounter; + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + } + if (levelDirFile.exists()) { + PrintUtil.error(String.format("Cannot restore: failed to delete old level %s .", levelDirFile.getName())); + return; + } + + // Decompress archive + PrintUtil.info("Decompressing archived level ..."); + ZipUtil.unzip(backupFilePath, levelDirectory, false); + long endTime = System.currentTimeMillis(); + PrintUtil.info(String.format("Restore complete! (%.2fs) Please restart the server manually.", (endTime - startTime) / 1000.0)); + } catch (SecurityException | IOException | ZipUtilException e) { + PrintUtil.error("An exception occurred while restoring: " + e.getMessage()); + } + } + } } diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractAsyncOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractAsyncOperation.java new file mode 100644 index 0000000..f1a19de --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractAsyncOperation.java @@ -0,0 +1,53 @@ +package com.keuin.kbackupfabric.operation.abstracts; + +public abstract class AbstractAsyncOperation extends AbstractSerializedOperation { + + private final Thread thread; + private final String name; + private final Object sync = new Object(); + + protected AbstractAsyncOperation(String name) { + this.name = name; + this.thread = new Thread(this::async, name); + } + + /** + * Start the worker thread. + * + * @return true if succeed starting, false if already started. + */ + @Override + protected final boolean operate() { + synchronized (sync) { + if (thread.isAlive()) + return false; + if (!sync()) + return false; + thread.start(); + return true; + } + } + + /** + * Implement your async operation here. + * When this method returns, the operation must finish. + */ + protected abstract void async(); + + /** + * If necessary, implement your sync operations here. + * It will be invoked before starting the async thread. + */ + protected boolean sync() { + return true; + } + + public final String getName() { + return name; + } + + @Override + public String toString() { + return "operation " + name; + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractBlockingOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractBlockingOperation.java new file mode 100644 index 0000000..de672cd --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractBlockingOperation.java @@ -0,0 +1,58 @@ +package com.keuin.kbackupfabric.operation.abstracts; + +import com.keuin.kbackupfabric.operation.abstracts.i.Blocking; + +public abstract class AbstractBlockingOperation extends AbstractSerializedOperation implements Blocking { + + private static final Object sync = new Object(); + private static boolean isBlocking = false; + private boolean wasBlocked = false; + private boolean noUnblocking = false; + + @Override + protected final boolean operate() { + synchronized (sync) { + if (isBlocking) { + System.out.println("blocked."); + wasBlocked = true; + return false; + } else { + System.out.println("not blocked."); + wasBlocked = false; + isBlocking = true; + } + } + boolean exitCode = blockingContext(); + if (!noUnblocking) + isBlocking = false; + return exitCode; + } + + public final boolean isBlocked() { + return wasBlocked; + } + + protected final void block(boolean blockState) { + isBlocking = blockState; + } + + protected void noUnblocking(boolean b) { + noUnblocking = b; + } + + protected boolean blockAndGet() { + synchronized (sync) { + boolean b = isBlocking; + isBlocking = true; + return b; + } + } + + /** + * Implement your blocked operation here. + * + * @return the stat code. + */ + protected abstract boolean blockingContext(); + +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractConfirmableOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractConfirmableOperation.java new file mode 100644 index 0000000..0a56008 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractConfirmableOperation.java @@ -0,0 +1,11 @@ +package com.keuin.kbackupfabric.operation.abstracts; + +public abstract class AbstractConfirmableOperation extends AbstractSerializedOperation { + + public final boolean confirm() { + return operate(); + } + + @Override + public abstract String toString(); +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractSerializedOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractSerializedOperation.java new file mode 100644 index 0000000..3168e8c --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/AbstractSerializedOperation.java @@ -0,0 +1,12 @@ +package com.keuin.kbackupfabric.operation.abstracts; + +public abstract class AbstractSerializedOperation { + /** + * Do your operation here. + * This method is not designed to be public. + * When this method returns, the operation must have finished. + * + * @return the stat code. + */ + protected abstract boolean operate(); +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableAsyncBlockingOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableAsyncBlockingOperation.java new file mode 100644 index 0000000..6268d5a --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableAsyncBlockingOperation.java @@ -0,0 +1,66 @@ +package com.keuin.kbackupfabric.operation.abstracts; + +import com.keuin.kbackupfabric.operation.abstracts.i.Blocking; +import com.keuin.kbackupfabric.operation.abstracts.i.Invokable; + +public abstract class InvokableAsyncBlockingOperation implements Invokable, Blocking { + + private final InvokableAsyncOperation asyncOperation; + private final HackedBlockingOperation blockingOperation; + + public InvokableAsyncBlockingOperation(String name) { + asyncOperation = new InvokableAsyncOperation(name) { + @Override + protected void async() { + InvokableAsyncBlockingOperation.this.async(); + // When the async operation finishes, unblock + blockingOperation.noUnblocking(false); + blockingOperation.block(false); + } + + @Override + protected boolean sync() { + return InvokableAsyncBlockingOperation.this.sync(); + } + }; + + blockingOperation = new HackedBlockingOperation(); + } + + @Override + public boolean invoke() { + return blockingOperation.invoke(); + } + + @Override + public boolean isBlocked() { + return blockingOperation.isBlocked(); + } + + protected abstract void async(); + + protected boolean sync() { + return true; + } + + private class HackedBlockingOperation extends InvokableBlockingOperation { + + @Override + protected boolean blockingContext() { + + noUnblocking(true); + return asyncOperation.invoke(); + } + + @Override + public void noUnblocking(boolean b) { + super.noUnblocking(b); + } + + @Override + public void block(boolean blockState) { + super.block(blockState); + } + } + +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableAsyncOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableAsyncOperation.java new file mode 100644 index 0000000..518f670 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableAsyncOperation.java @@ -0,0 +1,32 @@ +package com.keuin.kbackupfabric.operation.abstracts; + +public abstract class InvokableAsyncOperation extends InvokableOperation { + + private final AbstractAsyncOperation asyncOperation; + + + public InvokableAsyncOperation(String name) { + asyncOperation = new AbstractAsyncOperation(name) { + @Override + protected void async() { + InvokableAsyncOperation.this.async(); + } + + @Override + protected boolean sync() { + return InvokableAsyncOperation.this.sync(); + } + }; + } + + protected abstract void async(); + + protected boolean sync() { + return true; + } + + @Override + protected boolean operate() { + return asyncOperation.operate(); + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableBlockingOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableBlockingOperation.java new file mode 100644 index 0000000..b1a3748 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableBlockingOperation.java @@ -0,0 +1,39 @@ +package com.keuin.kbackupfabric.operation.abstracts; + +import com.keuin.kbackupfabric.operation.abstracts.i.Blocking; + +public abstract class InvokableBlockingOperation extends InvokableOperation implements Blocking { + + private final AbstractBlockingOperation operation = new AbstractBlockingOperation() { + @Override + protected boolean blockingContext() { + return InvokableBlockingOperation.this.blockingContext(); + } + }; + + @Override + protected final boolean operate() { + return operation.operate(); + } + + /** + * Implement your blocked operation here. + * + * @return stat code. + */ + protected abstract boolean blockingContext(); + + protected void block(boolean blockState) { + operation.block(blockState); + } + + protected void noUnblocking(boolean b) { + operation.noUnblocking(b); + } + + @Override + public final boolean isBlocked() { + return operation.isBlocked(); + } + +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableOperation.java new file mode 100644 index 0000000..47c1ca8 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/InvokableOperation.java @@ -0,0 +1,9 @@ +package com.keuin.kbackupfabric.operation.abstracts; + +import com.keuin.kbackupfabric.operation.abstracts.i.Invokable; + +public abstract class InvokableOperation extends AbstractSerializedOperation implements Invokable { + public boolean invoke() { + return operate(); + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/i/Blocking.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/i/Blocking.java new file mode 100644 index 0000000..ba1b003 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/i/Blocking.java @@ -0,0 +1,5 @@ +package com.keuin.kbackupfabric.operation.abstracts.i; + +public interface Blocking { + boolean isBlocked(); +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/abstracts/i/Invokable.java b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/i/Invokable.java new file mode 100644 index 0000000..74051e3 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/abstracts/i/Invokable.java @@ -0,0 +1,5 @@ +package com.keuin.kbackupfabric.operation.abstracts.i; + +public interface Invokable { + boolean invoke(); +} |