@@ -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 );
0 commit comments