Skip to content

Commit f9ccbf2

Browse files
committed
Keep cache cleanup off Bukkit state
1 parent 9b8c064 commit f9ccbf2

3 files changed

Lines changed: 163 additions & 7 deletions

File tree

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/usercache/UserDataManager.java‎

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ public class UserDataManager {
7474
private final Set<UUID> sharedCachePopulations = ConcurrentHashMap.newKeySet();
7575
private final Set<UUID> completedSharedCachePopulations = ConcurrentHashMap.newKeySet();
7676
private final ConcurrentHashMap<UUID, SharedCachePopulationState> sharedCachePopulationStates = new ConcurrentHashMap<>();
77+
/** Live storage identities, maintained from platform join/quit events. */
78+
private final Set<UUID> onlineUserSessions = ConcurrentHashMap.newKeySet();
7779

7880
private static final class SharedCachePopulationState {
7981
private long generation;
@@ -1162,19 +1164,91 @@ public void clearCacheBasic() {
11621164
if (plugin.getStorageType().equals(UserStorage.MYSQL)) plugin.getMysql().clearCacheBasic();
11631165
}
11641166

1167+
/**
1168+
* Capture Bukkit-owned online-player state on the platform scheduler, then do
1169+
* cache/storage eviction on the user-storage worker.
1170+
*/
11651171
public void clearNonNeededCachedUsers() {
1172+
Runnable capture = () -> {
1173+
java.util.HashSet<UUID> online = new java.util.HashSet<>();
1174+
for (Player player : Bukkit.getOnlinePlayers()) {
1175+
UUID storageUuid = onlineStorageUuid(player);
1176+
if (storageUuid != null) {
1177+
online.add(storageUuid);
1178+
onlineUserSessions.add(storageUuid);
1179+
}
1180+
}
1181+
try {
1182+
timer.execute(() -> {
1183+
try { clearNonNeededCachedUsers(online); }
1184+
catch (RuntimeException | Error failure) {
1185+
reportDeferredStorageFailure(failure);
1186+
throw failure;
1187+
}
1188+
});
1189+
} catch (RejectedExecutionException rejected) {
1190+
if (plugin != null && plugin.isEnabled()) reportDeferredStorageFailure(rejected);
1191+
}
1192+
};
1193+
if (Bukkit.getServer() == null) capture.run();
1194+
else plugin.getBukkitScheduler().runTask(plugin, capture);
1195+
}
1196+
1197+
private UUID onlineStorageUuid(Player player) {
1198+
if (player == null) return null;
1199+
if (plugin.getOptions().isOnlineMode()) return player.getUniqueId();
1200+
try {
1201+
String resolved = UuidLookup.getInstance().getUUID(player.getName());
1202+
return resolved == null || resolved.isEmpty() ? null : UUID.fromString(resolved);
1203+
} catch (RuntimeException failure) {
1204+
plugin.debug(failure);
1205+
return null;
1206+
}
1207+
}
1208+
1209+
public void markUserOnline(UUID uuid) {
1210+
if (uuid != null) onlineUserSessions.add(uuid);
1211+
}
1212+
1213+
public void markUserOffline(UUID uuid) {
1214+
if (uuid != null) onlineUserSessions.remove(uuid);
1215+
}
1216+
1217+
private void clearNonNeededCachedUsers(Set<UUID> onlineSnapshot) {
11661218
plugin.devDebug("Clearing cache for non online players (if any)");
1167-
ArrayList<UUID> onlineUUIDS = new ArrayList<>();
1168-
for (Player p : Bukkit.getOnlinePlayers()) onlineUUIDS.add(p.getUniqueId());
11691219
int removed = 0;
1170-
for (UUID uuid : userDataCache.keySet()) {
1171-
if (!onlineUUIDS.contains(uuid)) { removeCache(uuid, null); removed++; }
1220+
for (UUID uuid : Set.copyOf(userDataCache.keySet())) {
1221+
if (onlineSnapshot.contains(uuid) || onlineUserSessions.contains(uuid)) continue;
1222+
UserDataCache expected = userDataCache.get(uuid);
1223+
if (expected == null) continue;
1224+
long expectedVersion = expected.getSharedSnapshotVersion();
1225+
java.util.concurrent.atomic.AtomicBoolean retired = new java.util.concurrent.atomic.AtomicBoolean();
1226+
withSharedSqlBackendExclusive(uuid, () -> {
1227+
if (onlineUserSessions.contains(uuid)) return;
1228+
UserDataCache current = userDataCache.get(uuid);
1229+
if (current != expected || current.getSharedSnapshotVersion() != expectedVersion) return;
1230+
current.beginRemoval();
1231+
if (onlineUserSessions.contains(uuid)) {
1232+
current.cancelRemoval();
1233+
return;
1234+
}
1235+
try {
1236+
current.clearCache();
1237+
if (sharedSqlRoute != null) current.retireAfterSharedFlush();
1238+
} catch (RuntimeException | Error failure) {
1239+
current.cancelRemoval();
1240+
throw failure;
1241+
}
1242+
boolean cacheRemoved = retireSharedCache(uuid, current);
1243+
Consumer<UUID> listener = sharedCacheRemovalListener;
1244+
if (cacheRemoved && listener != null) listener.accept(uuid);
1245+
retired.set(cacheRemoved);
1246+
});
1247+
if (retired.get()) removed++;
11721248
}
11731249
if (removed > 0) plugin.devDebug("Removed " + removed + " cached users who are no longer online");
11741250
}
11751251

1176-
public boolean containsKey(UUID fromString) { return userDataCache.containsKey(fromString); }
1177-
/** Return an already-published cache snapshot without creating or populating one. */
11781252
public UserDataCache getPublishedCache(UUID uuid) {
11791253
UserDataCache cache = userDataCache.get(uuid);
11801254
return cache != null && cache.hasPublishedStorageSnapshot() ? cache : null;

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/listeners/PlayerJoinEvent.java‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public void onJoin(AdvancedCoreLoginEvent event) {
4242
return;
4343
}
4444

45+
try { plugin.getUserManager().getDataManager().markUserOnline(java.util.UUID.fromString(event.getUuid())); }
46+
catch (IllegalArgumentException failure) { plugin.debug(failure); }
4547
plugin.getUserManager().getDataManager().cacheUser(player.getUniqueId(), player.getName());
4648

4749
plugin.getBedrockHandle().learn(user);
@@ -156,7 +158,12 @@ public void onPlayerQuit(PlayerQuitEvent event) {
156158
}
157159

158160
Player player = event.getPlayer();
159-
plugin.debug("Logout: " + player.getName() + " (" + player.getUniqueId() + ")");
161+
plugin.debug("Logout: " + player.getName() + " (" + player.getUniqueId() + ")");
162+
try {
163+
java.util.UUID storageUuid = plugin.getOptions().isOnlineMode() ? player.getUniqueId()
164+
: java.util.UUID.fromString(UuidLookup.getInstance().getUUID(player.getName()));
165+
plugin.getUserManager().getDataManager().markUserOffline(storageUuid);
166+
} catch (RuntimeException failure) { plugin.debug(failure); }
160167

161168
if (plugin.getPermissionHandler() != null) {
162169
plugin.getPermissionHandler().login(player);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.bencodez.advancedcore.tests.user;
2+
3+
import static org.junit.jupiter.api.Assertions.assertTrue;
4+
import static org.mockito.ArgumentMatchers.*;
5+
import static org.mockito.Mockito.*;
6+
7+
import java.util.UUID;
8+
9+
import org.bukkit.Bukkit;
10+
import org.bukkit.Server;
11+
import org.junit.jupiter.api.Test;
12+
import org.mockito.ArgumentCaptor;
13+
import org.mockito.MockedStatic;
14+
15+
import com.bencodez.advancedcore.AdvancedCorePlugin;
16+
import com.bencodez.advancedcore.api.user.usercache.UserDataCache;
17+
import com.bencodez.advancedcore.api.user.usercache.UserDataManager;
18+
import com.bencodez.simpleapi.scheduler.BukkitScheduler;
19+
import com.bencodez.simpleapi.sql.data.DataValueInt;
20+
21+
class UserDataManagerCacheCleanupThreadingTest {
22+
@Test
23+
void capturesOnlinePlayersOnlyFromPlatformTask() {
24+
AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class, RETURNS_DEEP_STUBS);
25+
BukkitScheduler scheduler = mock(BukkitScheduler.class);
26+
when(plugin.getBukkitScheduler()).thenReturn(scheduler);
27+
when(plugin.isEnabled()).thenReturn(true);
28+
when(plugin.getOptions().isOnlineMode()).thenReturn(true);
29+
UserDataManager manager = new UserDataManager(plugin);
30+
Server server = mock(Server.class);
31+
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
32+
bukkit.when(Bukkit::getServer).thenReturn(server);
33+
bukkit.when(Bukkit::getOnlinePlayers).thenReturn(java.util.List.of());
34+
manager.clearNonNeededCachedUsers();
35+
bukkit.verify(Bukkit::getOnlinePlayers, never());
36+
ArgumentCaptor<Runnable> platform = ArgumentCaptor.forClass(Runnable.class);
37+
verify(scheduler).runTask(eq(plugin), platform.capture());
38+
platform.getValue().run();
39+
bukkit.verify(Bukkit::getOnlinePlayers);
40+
} finally { manager.getTimer().shutdownNow(); }
41+
}
42+
43+
@Test
44+
void joinAfterPlatformSnapshotPreventsWorkerEviction() throws Exception {
45+
AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class, RETURNS_DEEP_STUBS);
46+
BukkitScheduler scheduler = mock(BukkitScheduler.class);
47+
when(plugin.getBukkitScheduler()).thenReturn(scheduler);
48+
when(plugin.isEnabled()).thenReturn(true);
49+
when(plugin.getOptions().isOnlineMode()).thenReturn(true);
50+
UserDataManager manager = new UserDataManager(plugin);
51+
manager.getTimer().shutdownNow();
52+
java.util.concurrent.ScheduledExecutorService worker = mock(java.util.concurrent.ScheduledExecutorService.class);
53+
java.lang.reflect.Field timerField = UserDataManager.class.getDeclaredField("timer");
54+
timerField.setAccessible(true);
55+
timerField.set(manager, worker);
56+
UUID uuid = UUID.randomUUID();
57+
UserDataCache cache = new UserDataCache(manager, uuid);
58+
cache.updateCache(new java.util.HashMap<>(java.util.Map.of("Points", new DataValueInt(1))));
59+
manager.getUserDataCache().put(uuid, cache);
60+
Server server = mock(Server.class);
61+
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
62+
bukkit.when(Bukkit::getServer).thenReturn(server);
63+
bukkit.when(Bukkit::getOnlinePlayers).thenReturn(java.util.List.of());
64+
ArgumentCaptor<Runnable> platform = ArgumentCaptor.forClass(Runnable.class);
65+
ArgumentCaptor<Runnable> storage = ArgumentCaptor.forClass(Runnable.class);
66+
manager.clearNonNeededCachedUsers();
67+
verify(scheduler).runTask(eq(plugin), platform.capture());
68+
platform.getValue().run();
69+
verify(worker).execute(storage.capture());
70+
manager.markUserOnline(uuid);
71+
storage.getValue().run();
72+
assertTrue(manager.containsKey(uuid));
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)