Skip to content

Commit 327a84b

Browse files
committed
Start leaderboard cache only after PAPI registration and stop refreshes cleanly on disable
1 parent 6ba6dbe commit 327a84b

4 files changed

Lines changed: 58 additions & 3 deletions

File tree

src/main/java/com/gmail/nossr50/mcMMO.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,14 @@ public void onEnable() {
393393
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
394394
// Keep a reference for explicit shutdown/unregister in onDisable().
395395
papiExpansion = new PapiExpansion();
396-
papiExpansion.register();
396+
if (papiExpansion.register()) {
397+
// Only spend refresh work on the leaderboard cache once PlaceholderAPI has
398+
// actually accepted the expansion.
399+
papiExpansion.startLeaderboardCache();
400+
} else {
401+
getLogger().warning(
402+
"Failed to register the mcMMO PlaceholderAPI expansion, mcMMO placeholders will be unavailable.");
403+
}
397404
}
398405
}
399406

src/main/java/com/gmail/nossr50/placeholders/LeaderboardPlaceholderCache.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public class LeaderboardPlaceholderCache {
3939
// Single-flight guard to avoid overlapping rebuild work.
4040
private final AtomicBoolean refreshInProgress = new AtomicBoolean(false);
4141
private volatile @Nullable WrappedTask refreshTask;
42+
// Once stopped, late-firing refreshes become no-ops so they cannot race database shutdown
43+
// during plugin disable.
44+
private volatile boolean stopped;
4245

4346
/**
4447
* Constructor.
@@ -112,9 +115,11 @@ private boolean isDatabaseReady() {
112115
}
113116

114117
/**
115-
* Stops the periodic refresh task if active.
118+
* Stops the periodic refresh task if active and turns any further refresh attempts into
119+
* no-ops.
116120
*/
117121
public void shutdown() {
122+
stopped = true;
118123
final WrappedTask localTask = refreshTask;
119124
if (localTask != null) {
120125
localTask.cancel();
@@ -152,6 +157,11 @@ public String getValue(@Nullable PrimarySkillType skill, int position) {
152157
* was skipped (already running) or failed.
153158
*/
154159
boolean refreshNow() {
160+
// A stopped cache never refreshes; its snapshot only serves until the plugin disables.
161+
if (stopped) {
162+
return false;
163+
}
164+
155165
// Runtime guard: never execute refresh logic on the primary thread.
156166
if (plugin != null && Bukkit.isPrimaryThread()) {
157167
plugin.getFoliaLib().getScheduler().runAsync(task -> refreshNow());
@@ -171,7 +181,10 @@ boolean refreshNow() {
171181
snapshot.set(buildSnapshot());
172182
return true;
173183
} catch (RuntimeException e) {
174-
logger.log(Level.WARNING, "Failed to refresh PlaceholderAPI leaderboard cache", e);
184+
// A refresh interrupted by plugin disable is expected; don't warn about it.
185+
if (!stopped) {
186+
logger.log(Level.WARNING, "Failed to refresh PlaceholderAPI leaderboard cache", e);
187+
}
175188
return false;
176189
} finally {
177190
refreshInProgress.set(false);

src/main/java/com/gmail/nossr50/placeholders/PapiExpansion.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ public PapiExpansion() {
4343
refreshIntervalTicks
4444
);
4545
init();
46+
}
47+
48+
/**
49+
* Starts the leaderboard cache's async warm-up and periodic refreshes. Call only after this
50+
* expansion registered successfully, so no refresh work runs for an expansion that
51+
* PlaceholderAPI rejected.
52+
*/
53+
public void startLeaderboardCache() {
4654
leaderboardPlaceholderCache.start();
4755
}
4856

src/test/java/com/gmail/nossr50/placeholders/LeaderboardPlaceholderCacheTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,33 @@ void shouldKeepLastGoodSnapshotWhenRefreshFails() {
114114
assertThat(cache.getValue(null, 1)).isEqualTo("1000");
115115
}
116116

117+
/**
118+
* A cache that has been shut down must stop touching its data source entirely: a refresh
119+
* firing during plugin disable would otherwise race database shutdown.
120+
*/
121+
@Test
122+
void refreshNowShouldNotReadDataSourceWhenCacheIsShutDown() {
123+
// Given - a cache with one good snapshot that is then shut down.
124+
RecordingDataSource dataSource = new RecordingDataSource();
125+
dataSource.overall = List.of(new PlayerStat("survivor", 1000));
126+
127+
LeaderboardPlaceholderCache cache = new LeaderboardPlaceholderCache(5, dataSource,
128+
Logger.getAnonymousLogger());
129+
assertThat(cache.refreshNow()).isTrue();
130+
int readsBeforeShutdown = dataSource.calls.size();
131+
132+
// When - shutting down and attempting another refresh.
133+
cache.shutdown();
134+
boolean refreshed = cache.refreshNow();
135+
136+
// Then - the refresh is rejected and no further data source reads happen.
137+
assertThat(refreshed).isFalse();
138+
assertThat(dataSource.calls).hasSize(readsBeforeShutdown);
139+
140+
// And - the existing snapshot still serves lookups until the plugin fully disables.
141+
assertThat(cache.getPlayerName(null, 1)).isEqualTo("survivor");
142+
}
143+
117144
private static final class RecordingDataSource implements
118145
LeaderboardPlaceholderCache.LeaderboardDataSource {
119146
private final List<DataSourceCall> calls = new CopyOnWriteArrayList<>();

0 commit comments

Comments
 (0)