-
Notifications
You must be signed in to change notification settings - Fork 0
⚡ Optimize ItemShop config saving #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,12 +5,28 @@ | |||||||||||||||||
| import com.mcatk.gemshop.Message; | ||||||||||||||||||
| import org.apache.commons.lang.text.StrBuilder; | ||||||||||||||||||
| import org.bukkit.configuration.ConfigurationSection; | ||||||||||||||||||
| import org.bukkit.configuration.file.YamlConfiguration; | ||||||||||||||||||
| import org.bukkit.entity.Player; | ||||||||||||||||||
| import org.bukkit.scheduler.BukkitRunnable; | ||||||||||||||||||
|
|
||||||||||||||||||
| import java.io.File; | ||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||
| import java.nio.charset.StandardCharsets; | ||||||||||||||||||
| import java.nio.file.Files; | ||||||||||||||||||
| import java.util.HashMap; | ||||||||||||||||||
| import java.util.concurrent.ExecutorService; | ||||||||||||||||||
| import java.util.concurrent.Executors; | ||||||||||||||||||
| import java.util.concurrent.ThreadFactory; | ||||||||||||||||||
|
|
||||||||||||||||||
| public class ItemShop { | ||||||||||||||||||
| private final ExecutorService ioExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { | ||||||||||||||||||
| @Override | ||||||||||||||||||
| public Thread newThread(Runnable r) { | ||||||||||||||||||
| Thread t = new Thread(r, "GemShop-IO-Thread"); | ||||||||||||||||||
| t.setDaemon(true); | ||||||||||||||||||
| return t; | ||||||||||||||||||
|
Comment on lines
+21
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because the executor thread is marked as a daemon and never shut down, any pending async config writes can be dropped on server shutdown or plugin reload, and stale queued writes from an old plugin instance can still run and overwrite newer config data. This is most visible if an item is added/removed and the server/plugin is stopped or reloaded shortly after. Consider shutting down the executor and awaiting completion (or switching to Bukkit’s async task scheduler with proper shutdown hooks) so saves are guaranteed to finish. Useful? React with 👍 / 👎. |
||||||||||||||||||
| } | ||||||||||||||||||
| }); | ||||||||||||||||||
|
Comment on lines
+21
to
+28
|
||||||||||||||||||
|
|
||||||||||||||||||
| private HashMap<String, Items> itemsMap; | ||||||||||||||||||
|
|
||||||||||||||||||
| public ItemShop() { | ||||||||||||||||||
|
|
@@ -58,14 +74,40 @@ public void addItem(Player player, String shopId, String itemId, String price) { | |||||||||||||||||
| } | ||||||||||||||||||
| itemsMap.get(shopId).getMap().put(itemId, item); | ||||||||||||||||||
| GemShop.getPlugin().getConfig().set("Items." + shopId + "." + itemId, item); | ||||||||||||||||||
| GemShop.getPlugin().saveConfig(); | ||||||||||||||||||
| saveConfigAsync(); | ||||||||||||||||||
| player.sendMessage(Message.INFO + "添加成功:" + itemId + " " + price + "宝石"); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| public void delItem(String shopId, String itemId) { | ||||||||||||||||||
| itemsMap.get(shopId).getMap().remove(itemId); | ||||||||||||||||||
|
||||||||||||||||||
| itemsMap.get(shopId).getMap().remove(itemId); | |
| Items items = itemsMap.get(shopId); | |
| if (items == null) { | |
| GemShop.getPlugin().getLogger().warning( | |
| "Attempted to delete item '" + itemId + "' from unknown shopId '" + shopId + "'."); | |
| return; | |
| } | |
| items.getMap().remove(itemId); |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If GemShop.getPlugin() returns null (which could happen during certain initialization or shutdown sequences), this will throw a NullPointerException. Consider adding a null check at the start of the method to handle this edge case gracefully.
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mkdirs() method returns a boolean indicating success or failure, but this return value is ignored. If directory creation fails, Files.write() will subsequently throw an IOException, which is caught but only logged. Consider checking the return value of mkdirs() and logging a more specific error message if directory creation fails, to help with debugging configuration issues.
| file.getParentFile().mkdirs(); | |
| boolean created = file.getParentFile().mkdirs(); | |
| if (!created && !file.getParentFile().exists()) { | |
| GemShop.getPlugin().getLogger().severe("Could not create parent directories for config.yml, aborting async save."); | |
| return; | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,113 @@ | ||||||||||||||||
| package com.mcatk.gemshop.shops.itemshop; | ||||||||||||||||
|
|
||||||||||||||||
| import com.mcatk.gemshop.GemShop; | ||||||||||||||||
| import org.bukkit.configuration.file.YamlConfiguration; | ||||||||||||||||
| import org.bukkit.entity.Player; | ||||||||||||||||
| import org.bukkit.inventory.ItemStack; | ||||||||||||||||
| import org.bukkit.inventory.PlayerInventory; | ||||||||||||||||
| import org.junit.After; | ||||||||||||||||
| import org.junit.Before; | ||||||||||||||||
| import org.junit.Test; | ||||||||||||||||
| import org.mockito.Mockito; | ||||||||||||||||
| import org.mockito.invocation.InvocationOnMock; | ||||||||||||||||
| import org.mockito.stubbing.Answer; | ||||||||||||||||
|
|
||||||||||||||||
| import java.io.File; | ||||||||||||||||
| import java.lang.reflect.Field; | ||||||||||||||||
| import java.util.logging.Logger; | ||||||||||||||||
|
|
||||||||||||||||
| import static org.junit.Assert.assertTrue; | ||||||||||||||||
|
||||||||||||||||
| import static org.junit.Assert.assertTrue; |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test writes to the system temp directory but doesn't clean up the config.yml file afterwards. This can leave test artifacts on the filesystem and potentially cause test interactions. Consider creating a unique temporary directory for each test run and cleaning it up in tearDown, or mock the file writing completely to avoid actual I/O during unit tests.
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test creates an ItemShop instance with an ExecutorService but never shuts it down. This can lead to test thread leaks and prevent the test JVM from exiting cleanly. Consider adding a cleanup step in the tearDown method to shut down the executor, or create a shutdown method in ItemShop that can be called during test cleanup.
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a RuntimeException for test failures is not the standard JUnit pattern. The test should use JUnit assertions like assertTrue(duration < 50, "Execution time too slow...") or assertThat(duration).isLessThan(50). This would provide better test reporting and integration with test runners.
| if (duration >= 50) { | |
| throw new RuntimeException("Execution time too slow: " + duration + "ms. Optimization failed?"); | |
| } | |
| } | |
| assertTrue("Execution time too slow: " + duration + "ms. Optimization failed?", duration < 50); | |
| } | |
| } |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test measures execution time immediately after calling addItem, but the actual file write happens asynchronously. This test doesn't verify that the async write actually completes successfully - it only verifies that the main thread doesn't block. Consider adding a mechanism to wait for the async operation to complete (e.g., using CountDownLatch or similar) and verify that the file write was actually attempted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While daemon threads prevent JVM hang on shutdown, they also mean that pending writes can be abruptly terminated when the plugin disables, potentially leading to data loss or corrupted config files. Consider making the thread non-daemon, or implementing a graceful shutdown mechanism that waits for pending writes to complete using
awaitTermination()with a reasonable timeout in a shutdown method.