Skip to content

Commit fe461c6

Browse files
committed
Fence cache retirement against player joins
1 parent bf11544 commit fe461c6

2 files changed

Lines changed: 74 additions & 12 deletions

File tree

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

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ public class UserDataManager {
7979
// concurrent Folia snapshot from re-adding a player whose quit event won the
8080
// race; a later snapshot removes tombstones once Bukkit no longer lists them.
8181
private final ConcurrentHashMap<UUID, Boolean> onlineUserSessions = new ConcurrentHashMap<>();
82+
private final Object[] onlineSessionLocks = createOnlineSessionLocks();
83+
84+
private static Object[] createOnlineSessionLocks() {
85+
Object[] locks = new Object[64];
86+
java.util.Arrays.setAll(locks, ignored -> new Object());
87+
return locks;
88+
}
89+
90+
private Object onlineSessionLock(UUID uuid) {
91+
return onlineSessionLocks[(uuid.hashCode() & Integer.MAX_VALUE) % onlineSessionLocks.length];
92+
}
8293

8394
private static final class SharedCachePopulationState {
8495
private long generation;
@@ -1180,12 +1191,19 @@ public void clearNonNeededCachedUsers() {
11801191
}
11811192
java.util.HashSet<UUID> online = new java.util.HashSet<>();
11821193
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);
1194+
synchronized (onlineSessionLock(uuid)) {
1195+
Boolean state = onlineUserSessions.compute(uuid,
1196+
(ignored, current) -> Boolean.FALSE.equals(current) ? Boolean.FALSE : Boolean.TRUE);
1197+
if (Boolean.TRUE.equals(state)) online.add(uuid);
1198+
}
1199+
}
1200+
for (UUID uuid : Set.copyOf(onlineUserSessions.keySet())) {
1201+
synchronized (onlineSessionLock(uuid)) {
1202+
if (Boolean.FALSE.equals(onlineUserSessions.get(uuid)) && !platformOnline.contains(uuid)) {
1203+
onlineUserSessions.remove(uuid, Boolean.FALSE);
1204+
}
1205+
}
11861206
}
1187-
onlineUserSessions.entrySet().removeIf(entry -> Boolean.FALSE.equals(entry.getValue())
1188-
&& !platformOnline.contains(entry.getKey()));
11891207
try {
11901208
timer.execute(() -> {
11911209
try { clearNonNeededCachedUsers(online); }
@@ -1220,7 +1238,9 @@ private UUID onlineStorageUuid(Player player) {
12201238
}
12211239

12221240
public void markUserOnline(UUID uuid) {
1223-
if (uuid != null) onlineUserSessions.put(uuid, Boolean.TRUE);
1241+
if (uuid != null) synchronized (onlineSessionLock(uuid)) {
1242+
onlineUserSessions.put(uuid, Boolean.TRUE);
1243+
}
12241244
}
12251245

12261246
/** Capture an online session from a Bukkit/Folia-owned player event. */
@@ -1229,7 +1249,9 @@ public void markUserOnline(Player player) {
12291249
}
12301250

12311251
public void markUserOffline(UUID uuid) {
1232-
if (uuid != null) onlineUserSessions.put(uuid, Boolean.FALSE);
1252+
if (uuid != null) synchronized (onlineSessionLock(uuid)) {
1253+
onlineUserSessions.put(uuid, Boolean.FALSE);
1254+
}
12331255
}
12341256

12351257
/** Remove an online session from a Bukkit/Folia-owned player event. */
@@ -1257,16 +1279,22 @@ private void clearNonNeededCachedUsers(Set<UUID> onlineSnapshot) {
12571279
}
12581280
try {
12591281
current.clearCache();
1260-
if (sharedSqlRoute != null) current.retireAfterSharedFlush();
12611282
} catch (RuntimeException | Error failure) {
12621283
current.cancelRemoval();
12631284
throw failure;
12641285
}
1265-
boolean cacheRemoved = retireSharedCache(uuid, current);
1266-
Consumer<UUID> listener = sharedCacheRemovalListener;
1267-
if (cacheRemoved && listener != null) listener.accept(uuid);
1268-
retired.set(cacheRemoved);
1286+
synchronized (onlineSessionLock(uuid)) {
1287+
if (Boolean.TRUE.equals(onlineUserSessions.get(uuid))) {
1288+
current.cancelRemoval();
1289+
return;
1290+
}
1291+
if (sharedSqlRoute != null) current.retireAfterSharedFlush();
1292+
boolean cacheRemoved = retireSharedCache(uuid, current);
1293+
retired.set(cacheRemoved);
1294+
}
12691295
});
1296+
Consumer<UUID> listener = sharedCacheRemovalListener;
1297+
if (retired.get() && listener != null) listener.accept(uuid);
12701298
if (retired.get()) removed++;
12711299
}
12721300
if (removed > 0) plugin.devDebug("Removed " + removed + " cached users who are no longer online");

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import static org.mockito.Mockito.*;
77

88
import java.util.UUID;
9+
import java.util.concurrent.CountDownLatch;
10+
import java.util.concurrent.TimeUnit;
911

1012
import org.bukkit.Bukkit;
1113
import org.bukkit.Server;
@@ -130,4 +132,36 @@ void concurrentQuitCannotBeOverwrittenByPlatformSnapshot() throws Exception {
130132
assertTrue(!manager.containsKey(uuid));
131133
}
132134
}
135+
136+
@Test
137+
void joinDuringBlockedFlushPreventsRetirementWithoutBlockingJoin() throws Exception {
138+
AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class, RETURNS_DEEP_STUBS);
139+
when(plugin.isEnabled()).thenReturn(true);
140+
UserDataManager manager = new UserDataManager(plugin);
141+
UUID uuid = UUID.randomUUID();
142+
UserDataCache cache = mock(UserDataCache.class);
143+
when(cache.getSharedSnapshotVersion()).thenReturn(1L);
144+
CountDownLatch flushStarted = new CountDownLatch(1);
145+
CountDownLatch releaseFlush = new CountDownLatch(1);
146+
doAnswer(invocation -> {
147+
flushStarted.countDown();
148+
assertTrue(releaseFlush.await(5, TimeUnit.SECONDS));
149+
return null;
150+
}).when(cache).clearCache();
151+
manager.getUserDataCache().put(uuid, cache);
152+
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
153+
bukkit.when(Bukkit::getServer).thenReturn(null);
154+
manager.clearNonNeededCachedUsers();
155+
assertTrue(flushStarted.await(5, TimeUnit.SECONDS));
156+
long started = System.nanoTime();
157+
manager.markUserOnline(uuid);
158+
assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started) < 500);
159+
releaseFlush.countDown();
160+
manager.getTimer().shutdown();
161+
assertTrue(manager.getTimer().awaitTermination(5, TimeUnit.SECONDS));
162+
assertTrue(manager.containsKey(uuid));
163+
verify(cache).cancelRemoval();
164+
verify(cache, never()).retireAfterSharedFlush();
165+
} finally { manager.getTimer().shutdownNow(); }
166+
}
133167
}

0 commit comments

Comments
 (0)