Skip to content

Commit bf11544

Browse files
committed
Fence cache snapshots against concurrent quits
1 parent c933219 commit bf11544

2 files changed

Lines changed: 55 additions & 11 deletions

File tree

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

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ public class UserDataManager {
7575
private final Set<UUID> completedSharedCachePopulations = ConcurrentHashMap.newKeySet();
7676
private final ConcurrentHashMap<UUID, SharedCachePopulationState> sharedCachePopulationStates = new ConcurrentHashMap<>();
7777
/** Live storage identities, maintained from platform join/quit events. */
78-
private final Set<UUID> onlineUserSessions = ConcurrentHashMap.newKeySet();
78+
// TRUE/FALSE records the latest player-event state. Offline tombstones keep a
79+
// concurrent Folia snapshot from re-adding a player whose quit event won the
80+
// race; a later snapshot removes tombstones once Bukkit no longer lists them.
81+
private final ConcurrentHashMap<UUID, Boolean> onlineUserSessions = new ConcurrentHashMap<>();
7982

8083
private static final class SharedCachePopulationState {
8184
private long generation;
@@ -1170,14 +1173,19 @@ public void clearCacheBasic() {
11701173
*/
11711174
public void clearNonNeededCachedUsers() {
11721175
Runnable capture = () -> {
1173-
java.util.HashSet<UUID> online = new java.util.HashSet<>();
1176+
java.util.HashSet<UUID> platformOnline = new java.util.HashSet<>();
11741177
for (Player player : Bukkit.getOnlinePlayers()) {
11751178
UUID storageUuid = onlineStorageUuid(player);
1176-
if (storageUuid != null) {
1177-
online.add(storageUuid);
1178-
onlineUserSessions.add(storageUuid);
1179-
}
1179+
if (storageUuid != null) platformOnline.add(storageUuid);
1180+
}
1181+
java.util.HashSet<UUID> online = new java.util.HashSet<>();
1182+
for (UUID uuid : platformOnline) {
1183+
Boolean state = onlineUserSessions.compute(uuid,
1184+
(ignored, current) -> Boolean.FALSE.equals(current) ? Boolean.FALSE : Boolean.TRUE);
1185+
if (Boolean.TRUE.equals(state)) online.add(uuid);
11801186
}
1187+
onlineUserSessions.entrySet().removeIf(entry -> Boolean.FALSE.equals(entry.getValue())
1188+
&& !platformOnline.contains(entry.getKey()));
11811189
try {
11821190
timer.execute(() -> {
11831191
try { clearNonNeededCachedUsers(online); }
@@ -1212,7 +1220,7 @@ private UUID onlineStorageUuid(Player player) {
12121220
}
12131221

12141222
public void markUserOnline(UUID uuid) {
1215-
if (uuid != null) onlineUserSessions.add(uuid);
1223+
if (uuid != null) onlineUserSessions.put(uuid, Boolean.TRUE);
12161224
}
12171225

12181226
/** Capture an online session from a Bukkit/Folia-owned player event. */
@@ -1221,7 +1229,7 @@ public void markUserOnline(Player player) {
12211229
}
12221230

12231231
public void markUserOffline(UUID uuid) {
1224-
if (uuid != null) onlineUserSessions.remove(uuid);
1232+
if (uuid != null) onlineUserSessions.put(uuid, Boolean.FALSE);
12251233
}
12261234

12271235
/** Remove an online session from a Bukkit/Folia-owned player event. */
@@ -1233,17 +1241,17 @@ private void clearNonNeededCachedUsers(Set<UUID> onlineSnapshot) {
12331241
plugin.devDebug("Clearing cache for non online players (if any)");
12341242
int removed = 0;
12351243
for (UUID uuid : Set.copyOf(userDataCache.keySet())) {
1236-
if (onlineSnapshot.contains(uuid) || onlineUserSessions.contains(uuid)) continue;
1244+
if (onlineSnapshot.contains(uuid) || Boolean.TRUE.equals(onlineUserSessions.get(uuid))) continue;
12371245
UserDataCache expected = userDataCache.get(uuid);
12381246
if (expected == null) continue;
12391247
long expectedVersion = expected.getSharedSnapshotVersion();
12401248
java.util.concurrent.atomic.AtomicBoolean retired = new java.util.concurrent.atomic.AtomicBoolean();
12411249
withSharedSqlBackendExclusive(uuid, () -> {
1242-
if (onlineUserSessions.contains(uuid)) return;
1250+
if (Boolean.TRUE.equals(onlineUserSessions.get(uuid))) return;
12431251
UserDataCache current = userDataCache.get(uuid);
12441252
if (current != expected || current.getSharedSnapshotVersion() != expectedVersion) return;
12451253
current.beginRemoval();
1246-
if (onlineUserSessions.contains(uuid)) {
1254+
if (Boolean.TRUE.equals(onlineUserSessions.get(uuid))) {
12471255
current.cancelRemoval();
12481256
return;
12491257
}

‎AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/UserDataManagerCacheCleanupThreadingTest.java‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,40 @@ void joinAfterPlatformSnapshotPreventsWorkerEviction() throws Exception {
9494
assertTrue(manager.containsKey(uuid));
9595
}
9696
}
97+
98+
@Test
99+
void concurrentQuitCannotBeOverwrittenByPlatformSnapshot() throws Exception {
100+
AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class, RETURNS_DEEP_STUBS);
101+
BukkitScheduler scheduler = mock(BukkitScheduler.class);
102+
when(plugin.getBukkitScheduler()).thenReturn(scheduler);
103+
when(plugin.isEnabled()).thenReturn(true);
104+
when(plugin.getOptions().isOnlineMode()).thenReturn(true);
105+
UserDataManager manager = new UserDataManager(plugin);
106+
manager.getTimer().shutdownNow();
107+
java.util.concurrent.ScheduledExecutorService worker = mock(java.util.concurrent.ScheduledExecutorService.class);
108+
java.lang.reflect.Field timerField = UserDataManager.class.getDeclaredField("timer");
109+
timerField.setAccessible(true);
110+
timerField.set(manager, worker);
111+
UUID uuid = UUID.randomUUID();
112+
UserDataCache cache = new UserDataCache(manager, uuid);
113+
cache.updateCache(new java.util.HashMap<>(java.util.Map.of("Points", new DataValueInt(1))));
114+
manager.getUserDataCache().put(uuid, cache);
115+
org.bukkit.entity.Player player = mock(org.bukkit.entity.Player.class);
116+
when(player.getUniqueId()).thenReturn(uuid);
117+
Server server = mock(Server.class);
118+
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
119+
bukkit.when(Bukkit::getServer).thenReturn(server);
120+
bukkit.when(Bukkit::getOnlinePlayers).thenReturn(java.util.List.of(player));
121+
ArgumentCaptor<Runnable> platform = ArgumentCaptor.forClass(Runnable.class);
122+
ArgumentCaptor<Runnable> storage = ArgumentCaptor.forClass(Runnable.class);
123+
manager.markUserOnline(player);
124+
manager.clearNonNeededCachedUsers();
125+
verify(scheduler).runTask(eq(plugin), platform.capture());
126+
manager.markUserOffline(player);
127+
platform.getValue().run();
128+
verify(worker).execute(storage.capture());
129+
storage.getValue().run();
130+
assertTrue(!manager.containsKey(uuid));
131+
}
132+
}
97133
}

0 commit comments

Comments
 (0)