Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions modules/ItemManager/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,29 @@
<version>1.12.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mockito-inline 5.2.0 requires a newer Java runtime than this module targets (java.version is 1.8), so tests/build will fail under Java 8. Use a Mockito version compatible with Java 8 (e.g., Mockito 4.x or older) or raise the module’s Java target consistently across the build.

Suggested change
<version>5.2.0</version>
<version>4.11.0</version>

Copilot uses AI. Check for mistakes.
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.17</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.17</version>
<scope>test</scope>
</dependency>
Comment on lines +85 to +96
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicit byte-buddy / byte-buddy-agent test dependencies are typically already pulled in (and version-aligned) by Mockito; pinning them separately can cause dependency/version conflicts. Prefer removing these direct dependencies unless there’s a specific need to override Mockito’s managed versions.

Suggested change
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.17</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.17</version>
<scope>test</scope>
</dependency>

Copilot uses AI. Check for mistakes.
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package com.mcatk.itemmanager;

import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;

public final class ItemManager extends JavaPlugin {

private static ItemManager plugin;
private static ItemSort itemSort;
private ExecutorService ioExecutor;

@Override
public void onEnable() {
plugin = this;
ioExecutor = Executors.newSingleThreadExecutor();
saveConfig();
itemSort = new ItemSort();
regCommand();
Expand All @@ -20,6 +31,9 @@ public void onEnable() {

@Override
public void onDisable() {
if (ioExecutor != null) {
ioExecutor.shutdown();
}
Comment on lines 33 to +36

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Flush async config writes before shutdown

Because saveConfigAsync() now queues writes on ioExecutor, calling shutdown() without waiting means any queued or in-progress config writes can be abandoned when the server stops or reloads. This is a regression from the previous synchronous saveConfig() behavior: if an item is added shortly before disable/reload, config.yml updates may never reach disk. Consider awaiting termination (or performing a final synchronous save) so that pending writes complete before shutdown.

Useful? React with 👍 / 👎.

Comment on lines 33 to +36
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onDisable() only calls shutdown() and returns immediately. Any queued config writes may continue after the plugin is disabled (e.g., during /reload), which can race with a newly enabled plugin instance and overwrite newer config.yml contents. Consider awaiting termination (with a timeout) and/or cancelling queued tasks to ensure no background writes run after disable completes.

Copilot uses AI. Check for mistakes.
}

public static ItemManager getPlugin() {
Expand All @@ -46,4 +60,27 @@ public static ItemStack getItem(String sortId, String itemId) {
public static void addItem(String sortId, String itemId, ItemStack itemStack) {
itemSort.addItem(sortId, itemId, itemStack);
}

public void setIoExecutor(ExecutorService ioExecutor) {
this.ioExecutor = ioExecutor;
}

public void saveConfigAsync() {
if (!isEnabled()) {
return;
}
if (getConfig() instanceof YamlConfiguration) {
final String data = ((YamlConfiguration) getConfig()).saveToString();
final File file = new File(getDataFolder(), "config.yml");
ioExecutor.submit(() -> {
try {
Comment on lines +68 to +76
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveConfigAsync() submits work to ioExecutor without guarding against it being null or already shut down; submit() can throw RejectedExecutionException, which would currently propagate and potentially break callers. Add a defensive check/fallback (e.g., if executor is null/shutdown, call saveConfig() or log and return) and handle task submission failures gracefully.

Copilot uses AI. Check for mistakes.
Files.write(file.toPath(), data.getBytes(StandardCharsets.UTF_8));
} catch (IOException ex) {
getLogger().log(Level.SEVERE, "Could not save config to " + file, ex);
}
});
Comment on lines +73 to +81
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files.write(...) to config.yml is not atomic; a crash/interruption mid-write can leave a partially written/empty config file. Prefer writing to a temporary file and then atomically moving/replacing the target (and ensure the data folder exists) to make async saves more robust.

Copilot uses AI. Check for mistakes.
} else {
saveConfig();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void addItem(String id1, String id2, ItemStack itemStack) {
}
itemsHashMap.get(id1).getItemStackHashMap().put(id2, itemStack);
ItemManager.getPlugin().getConfig().set(id1 + "." + id2, itemStack);
ItemManager.getPlugin().saveConfig();
ItemManager.getPlugin().saveConfigAsync();
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change triggers a full config serialization + queued disk write on every addItem call. Even though it’s off the main thread, high-frequency calls can build up a large executor queue and cause sustained disk churn. Consider coalescing/debouncing saves (e.g., schedule a single save after a short delay and collapse multiple calls) to keep throughput predictable.

Copilot uses AI. Check for mistakes.
}

public ItemStack getItem(String id1, String id2) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.mcatk.itemmanager;

import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.ItemStack;
import org.junit.Before;
import org.junit.Test;
import org.mockito.stubbing.Answer;

import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.logging.Logger;

import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;

public class ItemSortBenchmarkTest {

private ItemManager itemManager;
private YamlConfiguration config;
private ItemSort itemSort;

@Before
public void setUp() throws Exception {
itemManager = mock(ItemManager.class);
config = mock(YamlConfiguration.class);
Logger logger = mock(Logger.class);

// Set static plugin field
Field pluginField = ItemManager.class.getDeclaredField("plugin");
pluginField.setAccessible(true);
pluginField.set(null, itemManager);

Comment on lines +28 to +32
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test mutates ItemManager’s private static plugin field via reflection but never restores it. Add an @After teardown to reset the static back to null (or the prior value) to avoid cross-test pollution if additional tests are added/run in the same JVM.

Copilot uses AI. Check for mistakes.
when(itemManager.getConfig()).thenReturn(config);
when(itemManager.getLogger()).thenReturn(logger);
when(config.getKeys(false)).thenReturn(new HashSet<>());

// Mock saveConfig to simulate I/O delay
doAnswer((Answer<Void>) invocation -> {
try {
Thread.sleep(10); // Simulate 10ms disk I/O
} catch (InterruptedException e) {
e.printStackTrace();
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the InterruptedException catch block, printing the stack trace loses the interrupt signal. Restore the interrupt flag (and avoid noisy stdout/stderr in tests) so the test runner can react appropriately.

Suggested change
e.printStackTrace();
Thread.currentThread().interrupt();

Copilot uses AI. Check for mistakes.
}
return null;
}).when(itemManager).saveConfig();

itemSort = new ItemSort();
}

@Test
public void benchmarkAddItem() {
long startTime = System.currentTimeMillis();
int iterations = 50;

for (int i = 0; i < iterations; i++) {
itemSort.addItem("sort1", "item" + i, mock(ItemStack.class));
}

long endTime = System.currentTimeMillis();
long duration = endTime - startTime;

System.out.println("Time taken for " + iterations + " adds: " + duration + "ms");

// Now it should be fast (< 100ms) because saveConfigAsync (mocked) does nothing
assertTrue("Should be fast (< 100ms) with async saving", duration < 100);

// Verify saveConfigAsync was called 50 times
verify(itemManager, times(iterations)).saveConfigAsync();

// Verify synchronous saveConfig was NOT called (except maybe in constructor? No, constructor calls config.getKeys)
// ItemSort constructor calls getConfig().getKeys(), not saveConfig().
Comment on lines +64 to +71
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duration-based assertion (duration < 100) is likely to be flaky across environments/CI load because it depends on wall-clock timing and Mockito/mocking overhead. Prefer a deterministic assertion that the synchronous saveConfig() path is never invoked (which you already verify) and drop/relax the timing threshold (or make it a generous upper bound with rationale).

Suggested change
// Now it should be fast (< 100ms) because saveConfigAsync (mocked) does nothing
assertTrue("Should be fast (< 100ms) with async saving", duration < 100);
// Verify saveConfigAsync was called 50 times
verify(itemManager, times(iterations)).saveConfigAsync();
// Verify synchronous saveConfig was NOT called (except maybe in constructor? No, constructor calls config.getKeys)
// ItemSort constructor calls getConfig().getKeys(), not saveConfig().
// Verify saveConfigAsync was called 50 times
verify(itemManager, times(iterations)).saveConfigAsync();
// Verify synchronous saveConfig was NOT called (except maybe in constructor? No, constructor calls config.getKeys)
// ItemSort constructor calls getConfig().getKeys(), not saveConfig().
// Verify synchronous saveConfig was NOT called (except maybe in constructor? No, constructor calls config.getKeys)
// ItemSort constructor calls getConfig().getKeys(), not saveConfig().

Copilot uses AI. Check for mistakes.
// So saveConfig() should be called 0 times.
verify(itemManager, times(0)).saveConfig();
}
}
Loading