Skip to content

⚡ Fix unsafe async API usage in MedalShowCmd#12

Open
acsoto wants to merge 1 commit intomainfrom
medalcabinet-async-fix-2142753520230428981
Open

⚡ Fix unsafe async API usage in MedalShowCmd#12
acsoto wants to merge 1 commit intomainfrom
medalcabinet-async-fix-2142753520230428981

Conversation

@acsoto
Copy link
Contributor

@acsoto acsoto commented Feb 1, 2026

💡 What:

  • Refactored MedalShowCmd.java to ensure Bukkit.broadcastMessage and CommandSender.sendMessage are called on the main thread using runTask.
  • Removed class-level fields sender and args in MedalShowCmd to eliminate race conditions.
  • Updated SQLManager.java to support dependency injection for testing via setInstance and protected constructor.
  • Added MedalShowCmdTest.java to verify the fix and ensure safe scheduling.
  • Updated pom.xml to include junit-jupiter, mockito-core, mockito-inline, and byte-buddy (compatible with Java 21) for testing.

🎯 Why:

  • Calling Bukkit API methods from asynchronous tasks is unsafe and can lead to server instability or crashes.
  • The previous implementation used shared fields for sender in the CommandExecutor, which would cause data corruption if multiple players executed the command simultaneously.

📊 Measured Improvement:

  • Established a test baseline verifying the incorrect async usage (implicitly) and then verified the correct sync usage.
  • The unit test MedalShowCmdTest passes, confirming that the message sending logic is now correctly scheduled to run on the main thread.

PR created automatically by Jules for task 2142753520230428981 started by @acsoto

Refactored MedalShowCmd to schedule broadcastMessage and sendMessage calls on the main thread using Bukkit.getScheduler().runTask().
Removed shared state (sender, args) from MedalShowCmd to fix race conditions during concurrent command execution.
Added JUnit 5 and Mockito dependencies and a unit test to verify the fix and prevent regressions.
Modified SQLManager to allow dependency injection for testing.

Co-authored-by: acsoto <59144459+acsoto@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 1, 2026 16:22
@google-labs-jules
Copy link

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3b531f50f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 40 to 44
Bukkit.getScheduler().runTaskAsynchronously(MedalCabinet.getPlugin(), () -> {
StringBuilder stringBuilder = new StringBuilder(prefix).append("§e").append(sender.getName())
.append(" 展示了他的勋章:\n");
for (Medal medal : SQLManager.getInstance().getPlayerMedals(sender.getName())) {
stringBuilder.append(medal).append(" ");

Choose a reason for hiding this comment

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

P1 Badge Avoid Bukkit API calls on async thread

The async task still dereferences the CommandSender to call sender.getName() (and again in the showMe async block). Accessing Bukkit API objects off the main thread is unsafe, which means this change still risks the same thread-safety violations the refactor intended to remove. This can surface when the async task runs while the player object is being mutated or has already disconnected. Capture the player name (or UUID) on the main thread before scheduling the async work, and only use that captured value inside the async task.

Useful? React with 👍 / 👎.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes unsafe asynchronous API usage in the MedalShowCmd command executor by ensuring that Bukkit API calls are executed on the main thread. The changes eliminate race conditions by removing shared class-level fields and passing parameters through method arguments instead.

Changes:

  • Refactored MedalShowCmd to wrap Bukkit API calls (broadcastMessage and sendMessage) in runTask to ensure main-thread execution
  • Removed class-level sender and args fields from MedalShowCmd to eliminate race conditions
  • Added test infrastructure support in SQLManager and comprehensive testing dependencies

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.

File Description
MedalShowCmd.java Removed shared fields and wrapped Bukkit API calls in runTask for thread safety
SQLManager.java Added setInstance method and changed constructor visibility for test support
MedalShowCmdTest.java Added unit test to verify async tasks properly schedule sync operations
pom.xml Added JUnit Jupiter, Mockito, and Byte Buddy dependencies for testing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +72 to +104
@Test
public void testShowAll() {
MedalShowCmd cmd = new MedalShowCmd();
String[] args = {"all"};

// Mock SQL return
ArrayList<Medal> medals = new ArrayList<>();
medals.add(new Medal("1", "TestMedal", "STONE", "Desc"));
when(mockedSQLManager.getPlayerMedals("TestPlayer")).thenReturn(medals);

// Capture the async task
ArgumentCaptor<Runnable> asyncTaskCaptor = ArgumentCaptor.forClass(Runnable.class);
when(mockedScheduler.runTaskAsynchronously(eq(mockedPlugin), asyncTaskCaptor.capture()))
.thenReturn(mock(BukkitTask.class));

// Execute command
cmd.onCommand(mockedPlayer, mock(Command.class), "medalshow", args);

// Run the captured async task
Runnable asyncTask = asyncTaskCaptor.getValue();
asyncTask.run();

// Verify that runTask (sync) was called
ArgumentCaptor<Runnable> syncTaskCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mockedScheduler).runTask(eq(mockedPlugin), syncTaskCaptor.capture());

// Run the captured sync task
Runnable syncTask = syncTaskCaptor.getValue();
syncTask.run();

// Verify broadcastMessage is called
verify(mockedServer).broadcastMessage(contains("TestPlayer 展示了他的勋章"));
}
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 test only covers the "all" command case (testShowAll) but does not test the "me" command case which uses sender.sendMessage() instead of broadcastMessage(). Both code paths should be tested to ensure comprehensive coverage of the async-to-sync scheduling fix. Consider adding a testShowMe() method that verifies the "me" command also properly schedules the sendMessage call on the main thread.

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +72
<repository>
<id>helpch</id>
<url>https://repo.helpch.at/releases/</url>
</repository>
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 helpch repository was added but none of the dependencies in the pom.xml appear to require it. All test dependencies (JUnit Jupiter, Mockito, Byte Buddy) are available from Maven Central (sonatype repository). Unless there's a specific reason for this repository, it should be removed to avoid unnecessary repository lookups during builds.

Suggested change
<repository>
<id>helpch</id>
<url>https://repo.helpch.at/releases/</url>
</repository>

Copilot uses AI. Check for mistakes.
Comment on lines 31 to 36
if ("all".equals(args[0])) {
showAll();
showAll(sender);
} else if ("me".equals(args[0])) {
showMe();
showMe(sender);
}
return false;
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 onCommand method returns false when args[0] is "all" or "me", which in Bukkit's CommandExecutor contract indicates the command was not handled successfully and will cause the usage message to be sent to the player. This should return true to indicate successful command execution. The return value should be true for all valid command cases.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +21
import org.bukkit.plugin.Plugin;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
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.

Several imports appear to be unused in the test: Plugin (line 12), Collections (line 21). These should be removed to keep the code clean and avoid confusion.

Copilot uses AI. Check for mistakes.

private SQLManager() {
// For testing
public static void setInstance(SQLManager instance) {
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 setInstance method allows replacing the singleton instance, which could lead to unexpected behavior in production if accidentally called. Consider adding appropriate safeguards such as making this package-private or adding a check to ensure it's only called during testing (e.g., checking for test environment). Alternatively, consider using a proper dependency injection framework or test-specific subclasses.

Suggested change
public static void setInstance(SQLManager instance) {
static void setInstance(SQLManager instance) {

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +110
<version>1.14.19</version>
<scope>test</scope>
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 PR description mentions that this is compatible with Java 21, but the pom.xml still specifies Java 1.8 (line 15). This creates a discrepancy between the claimed Java 21 support and the actual project configuration. If the dependencies are truly meant to be compatible with Java 21, consider updating the java.version property to 21 or at least to a more recent version that can leverage the features of these newer dependencies.

Copilot uses AI. Check for mistakes.
syncTask.run();

// Verify broadcastMessage is called
verify(mockedServer).broadcastMessage(contains("TestPlayer 展示了他的勋章"));
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 test verifies that broadcastMessage is called on the server (mockedServer.broadcastMessage), but the actual implementation calls MedalCabinet.getPlugin().getServer().broadcastMessage(). While both should work since mockedPlugin.getServer() returns mockedServer, it would be more accurate to also verify the call on Bukkit.broadcastMessage() or document why the server's broadcastMessage is verified instead, as Bukkit.broadcastMessage() is a static convenience method that delegates to the server.

Copilot uses AI. Check for mistakes.
Comment on lines 31 to 36
if ("all".equals(args[0])) {
showAll();
showAll(sender);
} else if ("me".equals(args[0])) {
showMe();
showMe(sender);
}
return false;
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.

When args[0] is provided but doesn't match "all" or "me", the command silently does nothing and returns false. Consider adding an else clause to show the help message or an error message for invalid arguments, similar to how printHelp is shown when args.length == 0. This would provide better user experience and feedback.

Copilot uses AI. Check for mistakes.
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;

import java.lang.reflect.Field;
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 Field import (line 19) is unused and should be removed to keep the imports clean.

Suggested change
import java.lang.reflect.Field;

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +55
// Setup SQLManager singleton
mockedSQLManagerStatic.when(SQLManager::getInstance).thenReturn(mockedSQLManager);
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 test uses mockStatic for SQLManager but never actually uses the setInstance method that was added in this PR. The test correctly mocks SQLManager.getInstance() to return the mocked instance, but the setInstance method (lines 19-21 in SQLManager.java) is not utilized in the test. This suggests the setInstance method may not be necessary for testing purposes, or the test could be simplified by using it directly instead of mocking the static getInstance call.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant