Skip to content

⚡ Optimize MedalPapi with SQLManager caching#13

Open
acsoto wants to merge 1 commit intomainfrom
perf/optimize-medal-papi-caching-10773958192654320631
Open

⚡ Optimize MedalPapi with SQLManager caching#13
acsoto wants to merge 1 commit intomainfrom
perf/optimize-medal-papi-caching-10773958192654320631

Conversation

@acsoto
Copy link
Contributor

@acsoto acsoto commented Feb 1, 2026

  • 💡 What: Implemented caching in SQLManager using ConcurrentHashMap and added a PlayerQuitEvent listener to clear cache.
  • 🎯 Why: To eliminate blocking database calls in MedalPapi.java, which are frequent and synchronous.
  • 📊 Measured Improvement: Baseline tests showed getMainMedal querying DB every time. Optimized tests confirmed DB query happens only once per player until cache clear, significantly reducing DB load for PAPI requests.
  • Dependencies: Added JUnit 5 and Mockito for testing. Updated PlaceholderAPI repo to helpch.at.

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

- Implemented caching in SQLManager using ConcurrentHashMap
- Added MedalPlayerListener to clear cache on player quit
- Refactored SQLManager for testability (dependency injection)
- Added unit tests verifying cache hits, misses, and null handling
- Updated pom.xml with JUnit 5 and Mockito dependencies
- Updated PlaceholderAPI repository to helpch.at

Co-authored-by: acsoto <59144459+acsoto@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 1, 2026 16:23
@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

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 PR implements a caching mechanism in the SQLManager to optimize the performance of PlaceholderAPI (PAPI) queries by eliminating redundant database calls. The getMainMedal method previously queried the database on every invocation; with this change, results are cached in memory and only queried once per player until they disconnect.

Changes:

  • Added a ConcurrentHashMap-based cache with an EMPTY_MEDAL sentinel pattern to distinguish between "no result" and "not cached"
  • Implemented a PlayerQuitEvent listener to clear cache entries when players disconnect
  • Added JUnit 5 and Mockito dependencies and created unit tests for the caching behavior
  • Updated PlaceholderAPI version from 2.11.1 to 2.11.5 and added the helpch.at repository

Reviewed changes

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

Show a summary per file
File Description
modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java Implements caching with ConcurrentHashMap, adds cache management methods, and updates setMainMedal to maintain cache consistency
modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/listener/MedalPlayerListener.java New listener to clear player cache on disconnect
modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/MedalCabinet.java Registers the new MedalPlayerListener
modules/MedalCabinet/src/test/java/com/mcatk/medalcabinet/sql/SQLManagerTest.java Unit tests for cache hit/miss behavior and null value caching
modules/MedalCabinet/pom.xml Adds test dependencies (JUnit 5, Mockito) and updates PlaceholderAPI version and repository
Comments suppressed due to low confidence (1)

modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java:181

  • When a SQLException occurs during the database update, the cache is not invalidated. This could lead to inconsistent state where the cache contains data that may not match the database. If the update partially succeeded or if there's uncertainty about the database state, the cache should be cleared to ensure data consistency on the next read.
            } catch (SQLException e) {
                e.printStackTrace();
            }

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

Comment on lines +174 to +177
if (newMain != null) {
mainMedalCache.put(playerID, newMain);
}

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.

After successfully updating the main medal in the database, the cache should be cleared or updated regardless of whether getMedal returns null. Currently, if getMedal(medalID) returns null (e.g., if the medal was deleted from the medal table), the cache retains stale data. This causes getMainMedal to return outdated information until the player quits.

Consider either:

  1. Always clearing the cache after a successful database update: mainMedalCache.remove(playerID);
  2. Or updating the cache even when newMain is null: mainMedalCache.put(playerID, newMain != null ? newMain : EMPTY_MEDAL);
Suggested change
if (newMain != null) {
mainMedalCache.put(playerID, newMain);
}
mainMedalCache.put(playerID, newMain != null ? newMain : EMPTY_MEDAL);

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +104
@Test
void testGetMainMedalCached() throws SQLException {
// Setup mock to return a medal ID "medal1" for player "player1"

// Simulating:
// First query (get main medal ID) returns "medal1"
// Second query (get medal details) returns medal details

when(resultSet.next()).thenReturn(true, true); // Sufficient for 1 call (2 queries)
when(resultSet.getString("medal_id")).thenReturn("medal1");
when(resultSet.getString("medal_name")).thenReturn("Medal One");

// Call 1 - Should hit DB
Medal m1 = sqlManager.getMainMedal("player1");
assertNotNull(m1);
assertEquals("Medal One", m1.getName());

// Call 2 - Should NOT hit DB (cached)
Medal m2 = sqlManager.getMainMedal("player1");
assertNotNull(m2);
assertEquals("Medal One", m2.getName());
assertEquals(m1, m2); // Should be same object reference if cached

// Verify that prepareStatement was called exactly 2 times (for the first call only)
verify(connection, times(2)).prepareStatement(anyString());

// Clear cache
sqlManager.clearCache("player1");

// Setup mock for another call
when(resultSet.next()).thenReturn(true, true);
when(resultSet.getString("medal_id")).thenReturn("medal1");
when(resultSet.getString("medal_name")).thenReturn("Medal One");

// Call 3 - Should hit DB again
Medal m3 = sqlManager.getMainMedal("player1");
assertNotNull(m3);

// Verify prepareStatement called 2 more times (total 4)
verify(connection, times(4)).prepareStatement(anyString());
}

@Test
void testGetMainMedalNullCaching() throws SQLException {
// Setup mock to return no medal (false on first next())
when(resultSet.next()).thenReturn(false);

// Call 1 - Should hit DB (1 query)
Medal m1 = sqlManager.getMainMedal("noplayer");
assertNull(m1);

// Call 2 - Should NOT hit DB (cached empty)
Medal m2 = sqlManager.getMainMedal("noplayer");
assertNull(m2);

// Verify query count.
// Logic:
// 1. SELECT medal_id ... returns empty (resultSet.next() -> false).
// 2. Returns null.
// 3. Cache put(noplayer, EMPTY_MEDAL).

// So only 1 query "SELECT medal_id..." executed.
verify(connection, times(1)).prepareStatement(anyString());
}
}
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 suite doesn't cover the setMainMedal method's cache update behavior. Consider adding tests for:

  1. Verifying cache is updated when setMainMedal succeeds
  2. Verifying cache behavior when setMainMedal succeeds but getMedal returns null
  3. Verifying cache state when setMainMedal fails due to SQLException

These scenarios are critical for ensuring the caching implementation works correctly in all cases.

Copilot uses AI. Check for mistakes.
sqlManager = new SQLManager(connection);
SQLManager.setInstance(sqlManager);

lenient().when(connection.prepareStatement(anyString())).thenReturn(preparedStatement);
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 PreparedStatement is not always closed on method exit.

Copilot uses AI. Check for mistakes.
SQLManager.setInstance(sqlManager);

lenient().when(connection.prepareStatement(anyString())).thenReturn(preparedStatement);
lenient().when(preparedStatement.executeQuery()).thenReturn(resultSet);
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 ResultSet is not always closed on method exit.

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