From bde43e78cae2f0cfa0d13fd91de6c8869c7eef8b Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Thu, 14 Aug 2025 01:09:58 +0200 Subject: [PATCH] Prevents book migration from locking up the server thread --- .../BooksWithoutBorders.java | 64 +++++--- .../command/CommandMigrate.java | 117 +++----------- .../container/MigrationRequest.java | 15 ++ .../thread/MigrationQueueThread.java | 150 ++++++++++++++++++ 4 files changed, 225 insertions(+), 121 deletions(-) create mode 100644 src/main/java/net/knarcraft/bookswithoutborders/container/MigrationRequest.java create mode 100644 src/main/java/net/knarcraft/bookswithoutborders/thread/MigrationQueueThread.java diff --git a/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java b/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java index d0a6de7..8dbab18 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java @@ -30,6 +30,7 @@ import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; import net.knarcraft.bookswithoutborders.config.BwBCommand; import net.knarcraft.bookswithoutborders.config.StaticMessage; import net.knarcraft.bookswithoutborders.config.Translatable; +import net.knarcraft.bookswithoutborders.container.MigrationRequest; import net.knarcraft.bookswithoutborders.handler.BookshelfHandler; import net.knarcraft.bookswithoutborders.listener.BookEventListener; import net.knarcraft.bookswithoutborders.listener.BookshelfListener; @@ -56,9 +57,11 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Queue; import java.util.UUID; import java.util.logging.Level; @@ -67,15 +70,17 @@ import java.util.logging.Level; */ public class BooksWithoutBorders extends JavaPlugin { - private static ItemFactory itemFactory; - private static @NotNull Map> playerBooksList = new HashMap<>(); - private static @NotNull List publicBooksList = new ArrayList<>(); - private static Map publicLetterIndex; - private static Map> playerLetterIndex; private static BooksWithoutBorders booksWithoutBorders; - private static BookshelfHandler bookshelfHandler; - private static StringFormatter stringFormatter; - private static BooksWithoutBordersConfig booksWithoutBordersConfig; + + private ItemFactory itemFactory; + private @NotNull Map> playerBooksList = new HashMap<>(); + private @NotNull List publicBooksList = new ArrayList<>(); + private Map publicLetterIndex; + private Map> playerLetterIndex; + private BookshelfHandler bookshelfHandler; + private StringFormatter stringFormatter; + private BooksWithoutBordersConfig booksWithoutBordersConfig; + private final Queue migrationQueue = new LinkedList<>(); /** * Logs a message to the console @@ -93,7 +98,7 @@ public class BooksWithoutBorders extends JavaPlugin { * @return

The BwB configuration

*/ public static BooksWithoutBordersConfig getConfiguration() { - return booksWithoutBordersConfig; + return getInstance().booksWithoutBordersConfig; } /** @@ -102,7 +107,16 @@ public class BooksWithoutBorders extends JavaPlugin { * @return

The string formatter

*/ public static StringFormatter getStringFormatter() { - return stringFormatter; + return getInstance().stringFormatter; + } + + /** + * Gets the migration queue + * + * @return

The migration queue

+ */ + public static Queue getMigrationQueue() { + return getInstance().migrationQueue; } /** @@ -115,17 +129,17 @@ public class BooksWithoutBorders extends JavaPlugin { @NotNull public static List getAvailableBooks(@NotNull CommandSender sender, boolean getPublic) { if (getPublic) { - return new ArrayList<>(publicBooksList); + return new ArrayList<>(getInstance().publicBooksList); } else if (sender instanceof Player player) { UUID playerUUID = player.getUniqueId(); - if (!playerBooksList.containsKey(playerUUID)) { + if (!getInstance().playerBooksList.containsKey(playerUUID)) { List newFiles = BookFileHelper.listFiles(sender, false); if (newFiles != null) { - playerBooksList.put(playerUUID, newFiles); - playerLetterIndex.put(playerUUID, BookFileHelper.populateLetterIndices(newFiles)); + getInstance().playerBooksList.put(playerUUID, newFiles); + getInstance().playerLetterIndex.put(playerUUID, BookFileHelper.populateLetterIndices(newFiles)); } } - return new ArrayList<>(playerBooksList.get(playerUUID)); + return new ArrayList<>(getInstance().playerBooksList.get(playerUUID)); } else { return new ArrayList<>(); } @@ -140,9 +154,9 @@ public class BooksWithoutBorders extends JavaPlugin { @NotNull public static Map getLetterIndex(@Nullable UUID playerIndex) { if (playerIndex == null) { - return publicLetterIndex; + return getInstance().publicLetterIndex; } else { - Map letterIndex = playerLetterIndex.get(playerIndex); + Map letterIndex = getInstance().playerLetterIndex.get(playerIndex); return Objects.requireNonNullElseGet(letterIndex, HashMap::new); } } @@ -159,11 +173,11 @@ public class BooksWithoutBorders extends JavaPlugin { return; } if (updatePublic) { - publicBooksList = newFiles; - publicLetterIndex = BookFileHelper.populateLetterIndices(newFiles); + getInstance().publicBooksList = newFiles; + getInstance().publicLetterIndex = BookFileHelper.populateLetterIndices(newFiles); } else if (sender instanceof Player player) { - playerBooksList.put(player.getUniqueId(), newFiles); - playerLetterIndex.put(player.getUniqueId(), BookFileHelper.populateLetterIndices(newFiles)); + getInstance().playerBooksList.put(player.getUniqueId(), newFiles); + getInstance().playerLetterIndex.put(player.getUniqueId(), BookFileHelper.populateLetterIndices(newFiles)); } } @@ -171,8 +185,8 @@ public class BooksWithoutBorders extends JavaPlugin { * Clears book data such as per-player lists and per-player character indexes */ public static void clearBookData() { - playerBooksList = new HashMap<>(); - playerLetterIndex = new HashMap<>(); + getInstance().playerBooksList = new HashMap<>(); + getInstance().playerLetterIndex = new HashMap<>(); } @@ -308,7 +322,7 @@ public class BooksWithoutBorders extends JavaPlugin { * @return

The bookshelf handler

*/ public static BookshelfHandler getBookshelfHandler() { - return bookshelfHandler; + return getInstance().bookshelfHandler; } /** @@ -318,7 +332,7 @@ public class BooksWithoutBorders extends JavaPlugin { */ @NotNull public static ItemFactory getItemFactory() { - return itemFactory; + return getInstance().itemFactory; } /** diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandMigrate.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandMigrate.java index 8e1b00e..f3313a7 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandMigrate.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandMigrate.java @@ -2,24 +2,19 @@ package net.knarcraft.bookswithoutborders.command; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.config.Translatable; -import net.knarcraft.bookswithoutborders.utility.BookFileHelper; -import net.knarcraft.bookswithoutborders.utility.BookHelper; -import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; +import net.knarcraft.bookswithoutborders.container.MigrationRequest; +import net.knarcraft.bookswithoutborders.thread.MigrationQueueThread; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; import org.bukkit.entity.Player; -import org.bukkit.inventory.meta.BookMeta; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; -import java.io.IOException; +import java.util.LinkedList; import java.util.List; -import java.util.UUID; +import java.util.Queue; import java.util.logging.Level; /** @@ -35,12 +30,15 @@ public class CommandMigrate implements TabExecutor { return false; } File bookDirectory = new File(BooksWithoutBorders.getConfiguration().getBookFolder()); - boolean success = migrateFiles(bookDirectory, player); - if (success) { - BooksWithoutBorders.sendSuccessMessage(player, "Successfully migrated all books!"); - } else { - BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate all books!"); - } + BooksWithoutBorders.sendSuccessMessage(player, "Starting book migration..."); + Queue filesToMigrate = new LinkedList<>(); + findFilesToMigrate(bookDirectory, filesToMigrate, player); + BooksWithoutBorders.getMigrationQueue().addAll(filesToMigrate); + + BooksWithoutBorders instance = BooksWithoutBorders.getInstance(); + MigrationQueueThread queueThread = new MigrationQueueThread(); + int taskId = instance.getServer().getScheduler().runTaskTimer(instance, queueThread, 1L, 1L).getTaskId(); + queueThread.setTaskId(taskId); return true; } @@ -52,98 +50,25 @@ public class CommandMigrate implements TabExecutor { } /** - * Migrates the given folder recursively + * Finds all books that should be migrated * - * @param folder

The folder to migrate files for

- * @param player

The player causing this code to be executed

- * @return

If all migrations were successful

+ * @param folder

The folder to search for books

+ * @param queue

The list to append found files to

+ * @param player

The player that initiated the migration

*/ - private boolean migrateFiles(@NotNull File folder, @NotNull Player player) { + private void findFilesToMigrate(@NotNull File folder, @NotNull Queue queue, @NotNull Player player) { File[] files = folder.listFiles(); if (files == null) { BooksWithoutBorders.log(Level.WARNING, "Unable to access directory " + folder.getName() + " !"); - return false; + return; } - boolean success = true; for (File file : files) { if (file.isDirectory()) { - success = success & migrateFiles(file, player); + findFilesToMigrate(file, queue, player); } else if (file.isFile()) { - success = success & migrateFile(file, player); + queue.add(new MigrationRequest(file, player)); } } - return success; - } - - /** - * Migrates a single book file - * - * @param file

The file to migrate

- * @param player

The player causing this code to be executed

- * @return

True if the migration completed successfully

- */ - private boolean migrateFile(@NotNull File file, @NotNull Player player) { - BookMeta bookMeta = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITTEN_BOOK); - if (bookMeta == null) { - return false; - } - BookMeta loadedBook; - String extension = BookFileHelper.getExtensionFromPath(file.getName()); - if (extension.equalsIgnoreCase("yml")) { - loadedBook = BookToFromTextHelper.encryptedBookFromYml(file, bookMeta, "", true); - } else if (extension.equalsIgnoreCase("txt")) { - loadedBook = BookToFromTextHelper.bookFromFile(file, bookMeta); - } else { - BooksWithoutBorders.log(Level.WARNING, "File with unexpected extension " + extension + " encountered!"); - return true; - } - - if (loadedBook == null) { - BooksWithoutBorders.log(Level.SEVERE, "Unable to load book: " + file.getName()); - return false; - } - - // Attempt to retain UUID naming - boolean isPublic = true; - OfflinePlayer author = player; - try { - UUID authorId = UUID.fromString(file.getParentFile().getName()); - author = Bukkit.getOfflinePlayer(authorId); - isPublic = false; - } catch (IllegalArgumentException ignored) { - } - - try { - String newName = BookHelper.getBookFile(loadedBook, author, isPublic); - return saveBook(file.getParentFile(), newName, loadedBook, file); - } catch (IllegalArgumentException exception) { - BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate book: " + file.getName() + " Cause:"); - BooksWithoutBorders.sendErrorMessage(player, exception.getMessage()); - return false; - } - } - - /** - * Saves a migrated book - * - * @param parent

The parent folder the file belongs to

- * @param newName

The new name of the file

- * @param bookMeta

The metadata of the book to migrate

- * @param oldFile

The old file path, in case it should be deleted

- * @return

True if successfully saved

- */ - private boolean saveBook(@NotNull File parent, @NotNull String newName, @NotNull BookMeta bookMeta, - @NotNull File oldFile) { - try { - BookToFromTextHelper.bookToYml(parent.getAbsolutePath(), newName, bookMeta); - if (!oldFile.getAbsolutePath().equalsIgnoreCase(new File(parent, newName + ".yml").getAbsolutePath())) { - return oldFile.delete(); - } - return true; - } catch (IOException exception) { - BooksWithoutBorders.log(Level.SEVERE, "Failed to save migrated book: " + newName); - return false; - } } } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/container/MigrationRequest.java b/src/main/java/net/knarcraft/bookswithoutborders/container/MigrationRequest.java new file mode 100644 index 0000000..59049dd --- /dev/null +++ b/src/main/java/net/knarcraft/bookswithoutborders/container/MigrationRequest.java @@ -0,0 +1,15 @@ +package net.knarcraft.bookswithoutborders.container; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +/** + * A request for migrating a book + * + * @param file

The file to migrate

+ * @param player

The player that initiated the migration

+ */ +public record MigrationRequest(@NotNull File file, @NotNull Player player) { +} diff --git a/src/main/java/net/knarcraft/bookswithoutborders/thread/MigrationQueueThread.java b/src/main/java/net/knarcraft/bookswithoutborders/thread/MigrationQueueThread.java new file mode 100644 index 0000000..9963a5d --- /dev/null +++ b/src/main/java/net/knarcraft/bookswithoutborders/thread/MigrationQueueThread.java @@ -0,0 +1,150 @@ +package net.knarcraft.bookswithoutborders.thread; + +import net.knarcraft.bookswithoutborders.BooksWithoutBorders; +import net.knarcraft.bookswithoutborders.container.MigrationRequest; +import net.knarcraft.bookswithoutborders.utility.BookFileHelper; +import net.knarcraft.bookswithoutborders.utility.BookHelper; +import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.BookMeta; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; +import java.util.logging.Level; + +/** + * A thread for doing book migrations without locking up the main thread + */ +public class MigrationQueueThread implements Runnable { + + private Boolean success = null; + private int taskId; + + @Override + public void run() { + long systemTime = System.nanoTime(); + //Repeat for at most 0.025 seconds + while (System.nanoTime() - systemTime < 25000000) { + if (pollQueue()) { + break; + } + } + } + + /** + * Sets the task id used for stopping this task + * + * @param taskId

The id of this task

+ */ + public void setTaskId(int taskId) { + this.taskId = taskId; + } + + /** + * Polls the migration queue for any waiting requests + * + * @return

True if the queue is empty and it's safe to quit

+ */ + public boolean pollQueue() { + MigrationRequest migrationRequest = BooksWithoutBorders.getMigrationQueue().poll(); + if (migrationRequest == null) { + return true; + } + + if (success == null) { + success = true; + } + + success = success & migrateFile(migrationRequest.file(), migrationRequest.player()); + + if (BooksWithoutBorders.getMigrationQueue().peek() == null) { + Player player = migrationRequest.player(); + if (success) { + BooksWithoutBorders.sendSuccessMessage(player, "Successfully migrated all books"); + } else { + BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate all books"); + } + BooksWithoutBorders.getInstance().getServer().getScheduler().cancelTask(this.taskId); + success = null; + return true; + } + return false; + } + + /** + * Migrates a single book file + * + * @param file

The file to migrate

+ * @param player

The player causing this code to be executed

+ * @return

True if the migration completed successfully

+ */ + private boolean migrateFile(@NotNull File file, @NotNull Player player) { + BookMeta bookMeta = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITTEN_BOOK); + if (bookMeta == null) { + return false; + } + BookMeta loadedBook; + String extension = BookFileHelper.getExtensionFromPath(file.getName()); + if (extension.equalsIgnoreCase("yml")) { + loadedBook = BookToFromTextHelper.encryptedBookFromYml(file, bookMeta, "", true); + } else if (extension.equalsIgnoreCase("txt")) { + loadedBook = BookToFromTextHelper.bookFromFile(file, bookMeta); + } else { + BooksWithoutBorders.log(Level.WARNING, "File with unexpected extension " + extension + " encountered!"); + return true; + } + + if (loadedBook == null) { + BooksWithoutBorders.log(Level.SEVERE, "Unable to load book: " + file.getName()); + return false; + } + + // Attempt to retain UUID naming + boolean isPublic = true; + OfflinePlayer author = player; + try { + UUID authorId = UUID.fromString(file.getParentFile().getName()); + author = Bukkit.getOfflinePlayer(authorId); + isPublic = false; + } catch (IllegalArgumentException ignored) { + } + + try { + String newName = BookHelper.getBookFile(loadedBook, author, isPublic); + return saveBook(file.getParentFile(), newName, loadedBook, file); + } catch (IllegalArgumentException exception) { + BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate book: " + file.getName() + " Cause:"); + BooksWithoutBorders.sendErrorMessage(player, exception.getMessage()); + return false; + } + } + + /** + * Saves a migrated book + * + * @param parent

The parent folder the file belongs to

+ * @param newName

The new name of the file

+ * @param bookMeta

The metadata of the book to migrate

+ * @param oldFile

The old file path, in case it should be deleted

+ * @return

True if successfully saved

+ */ + private boolean saveBook(@NotNull File parent, @NotNull String newName, @NotNull BookMeta bookMeta, + @NotNull File oldFile) { + try { + BookToFromTextHelper.bookToYml(parent.getAbsolutePath(), newName, bookMeta); + if (!oldFile.getAbsolutePath().equalsIgnoreCase(new File(parent, newName + ".yml").getAbsolutePath())) { + return oldFile.delete(); + } + return true; + } catch (IOException exception) { + BooksWithoutBorders.log(Level.SEVERE, "Failed to save migrated book: " + newName); + return false; + } + } + +}