Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ public class UserDataManager {
private final Set<UUID> sharedCachePopulations = ConcurrentHashMap.newKeySet();
private final Set<UUID> completedSharedCachePopulations = ConcurrentHashMap.newKeySet();
private final ConcurrentHashMap<UUID, SharedCachePopulationState> sharedCachePopulationStates = new ConcurrentHashMap<>();
/** Live storage identities, maintained from platform join/quit events. */
// Generation-stamped offline tombstones prevent an older queued platform
// snapshot from erasing a newer quit and letting a later stale snapshot
// resurrect that session.
private final ConcurrentHashMap<UUID, OnlineSessionState> onlineUserSessions = new ConcurrentHashMap<>();
private final AtomicLong onlineSessionGeneration = new AtomicLong();
private final Object[] onlineSessionLocks = createOnlineSessionLocks();
private record OnlineSessionState(boolean online, long generation) {}

private static Object[] createOnlineSessionLocks() {
Object[] locks = new Object[64];
java.util.Arrays.setAll(locks, ignored -> new Object());
return locks;
}

private Object onlineSessionLock(UUID uuid) {
return onlineSessionLocks[(uuid.hashCode() & Integer.MAX_VALUE) % onlineSessionLocks.length];
}

private static final class SharedCachePopulationState {
private long generation;
Expand Down Expand Up @@ -1162,23 +1180,164 @@ public void clearCacheBasic() {
if (plugin.getStorageType().equals(UserStorage.MYSQL)) plugin.getMysql().clearCacheBasic();
}

/**
* Capture Bukkit-owned online-player state on the platform scheduler, then do
* cache/storage eviction on the user-storage worker.
*/
public void clearNonNeededCachedUsers() {
Runnable capture = () -> {
java.util.HashSet<UUID> platformOnline = new java.util.HashSet<>();
if (Bukkit.getServer() != null) {
for (Player player : Bukkit.getOnlinePlayers()) {
UUID storageUuid = onlineStorageUuid(player);
if (storageUuid != null) platformOnline.add(storageUuid);
}
}
long snapshotGeneration = onlineSessionGeneration.get();
try {
timer.execute(() -> {
try { clearNonNeededCachedUsers(reconcileOnlineSnapshot(platformOnline, snapshotGeneration)); }
catch (RuntimeException | Error failure) {
reportDeferredStorageFailure(failure);
throw failure;
}
});
} catch (RejectedExecutionException rejected) {
if (plugin != null && plugin.isEnabled()) reportDeferredStorageFailure(rejected);
}
};
if (Bukkit.getServer() == null) capture.run();
else {
try { plugin.getBukkitScheduler().runTask(plugin, capture); }
catch (RuntimeException failure) {
if (plugin != null && plugin.isEnabled()) reportDeferredStorageFailure(failure);
}
}
}

private Set<UUID> reconcileOnlineSnapshot(Set<UUID> platformOnline, long snapshotGeneration) {
java.util.HashSet<UUID> online = new java.util.HashSet<>();
for (UUID uuid : platformOnline) {
synchronized (onlineSessionLock(uuid)) {
OnlineSessionState state = onlineUserSessions.compute(uuid,
(ignored, current) -> current == null
? new OnlineSessionState(true, snapshotGeneration) : current);
if (state.online()) online.add(uuid);
}
}
for (UUID uuid : Set.copyOf(onlineUserSessions.keySet())) {
synchronized (onlineSessionLock(uuid)) {
OnlineSessionState state = onlineUserSessions.get(uuid);
if (state != null && !state.online() && state.generation() <= snapshotGeneration
&& !platformOnline.contains(uuid)) {
onlineUserSessions.remove(uuid, state);
}
}
}
return online;
}

private UUID onlineStorageUuid(Player player) {
if (player == null) return null;
if (plugin.getOptions().isOnlineMode()) return player.getUniqueId();
try {
String resolved = UuidLookup.getInstance().getUUID(player.getName());
return resolved == null || resolved.isEmpty() ? null : UUID.fromString(resolved);
} catch (RuntimeException failure) {
plugin.debug(failure);
return null;
}
}

public void markUserOnline(UUID uuid) {
if (uuid != null) synchronized (onlineSessionLock(uuid)) {
onlineUserSessions.put(uuid, new OnlineSessionState(true, onlineSessionGeneration.incrementAndGet()));
}
}

/** Capture an online session from a Bukkit/Folia-owned player event. */
public void markUserOnline(Player player) {
markUserOnline(onlineStorageUuid(player));
}

public void markUserOffline(UUID uuid) {
if (uuid != null) synchronized (onlineSessionLock(uuid)) {
onlineUserSessions.put(uuid, new OnlineSessionState(false, onlineSessionGeneration.incrementAndGet()));
}
}

/** Remove an online session from a Bukkit/Folia-owned player event. */
public void markUserOffline(Player player) {
markUserOffline(onlineStorageUuid(player));
}

private void clearNonNeededCachedUsers(Set<UUID> onlineSnapshot) {
plugin.devDebug("Clearing cache for non online players (if any)");
ArrayList<UUID> onlineUUIDS = new ArrayList<>();
for (Player p : Bukkit.getOnlinePlayers()) onlineUUIDS.add(p.getUniqueId());
int removed = 0;
for (UUID uuid : userDataCache.keySet()) {
if (!onlineUUIDS.contains(uuid)) { removeCache(uuid, null); removed++; }
for (UUID uuid : Set.copyOf(userDataCache.keySet())) {
if (onlineSnapshot.contains(uuid) || isUserOnline(uuid)) continue;
UserDataCache expected = userDataCache.get(uuid);
if (expected == null) continue;
long expectedVersion = expected.getSharedSnapshotVersion();
java.util.concurrent.atomic.AtomicBoolean retired = new java.util.concurrent.atomic.AtomicBoolean();
java.util.concurrent.atomic.AtomicBoolean invalidatedForJoin = new java.util.concurrent.atomic.AtomicBoolean();
withSharedSqlBackendExclusive(uuid, () -> {
if (isUserOnline(uuid)) return;
UserDataCache current = userDataCache.get(uuid);
if (current != expected || current.getSharedSnapshotVersion() != expectedVersion) return;
current.beginRemoval();
if (isUserOnline(uuid)) {
current.cancelRemoval();
return;
}
try {
current.clearCache();
} catch (RuntimeException | Error failure) {
current.cancelRemoval();
throw failure;
}
synchronized (onlineSessionLock(uuid)) {
if (isUserOnline(uuid)) {
if (sharedSqlRoute != null) {
try { current.retireAfterSharedFlush(); }
catch (RuntimeException | Error failure) {
current.cancelRemoval();
throw failure;
}
}
boolean cacheRemoved = retireSharedCache(uuid, current);
retired.set(cacheRemoved);
invalidatedForJoin.set(cacheRemoved);
return;
}
if (sharedSqlRoute != null) {
try { current.retireAfterSharedFlush(); }
catch (RuntimeException | Error failure) {
current.cancelRemoval();
throw failure;
}
}
boolean cacheRemoved = retireSharedCache(uuid, current);
retired.set(cacheRemoved);
}
});
Consumer<UUID> listener = sharedCacheRemovalListener;
if (retired.get() && listener != null) listener.accept(uuid);
if (retired.get() && !invalidatedForJoin.get()) removed++;
}
if (removed > 0) plugin.devDebug("Removed " + removed + " cached users who are no longer online");
}

public boolean containsKey(UUID fromString) { return userDataCache.containsKey(fromString); }
/** Return an already-published cache snapshot without creating or populating one. */
private boolean isUserOnline(UUID uuid) {
OnlineSessionState state = onlineUserSessions.get(uuid);
return state != null && state.online();
}

public UserDataCache getPublishedCache(UUID uuid) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the public containsKey method

Removing UserDataManager.containsKey(UUID) breaks the library's public compatibility surface: existing tests still invoke it, and already-compiled downstream plugins that use it will fail with NoSuchMethodError when loaded against this JAR. Preserve the method, optionally deprecating it and delegating to the cache map, rather than deleting it as part of the cleanup refactor.

AGENTS.md reference: AGENTS.md:L27-L27

Useful? React with 👍 / 👎.

UserDataCache cache = userDataCache.get(uuid);
return cache != null && cache.hasPublishedStorageSnapshot() ? cache : null;
}
public boolean containsKey(UUID uuid) { return userDataCache.containsKey(uuid); }
public UserDataCache getCache(UUID uuid) {
if (hasSharedSqlBackend() && isPlatformOwnedThread()) {
UserDataCache cache = userDataCache.get(uuid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,20 @@ public void onJoin(AdvancedCoreLoginEvent event) {
}
}

@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerLogin(final org.bukkit.event.player.PlayerJoinEvent event) {
if (plugin == null || !plugin.isEnabled() || !plugin.isLoadUserData()) {
return;
}
if (plugin == null || !plugin.isEnabled()) {
return;
}
plugin.getUserManager().getDataManager().markUserOnline(event.getPlayer());
if (!plugin.isLoadUserData()) return;

if (!plugin.getOptions().isHideLoginMessage()) {
plugin.getLogger()
.info("Login: " + event.getPlayer().getName() + " (" + event.getPlayer().getUniqueId() + ")");
} else {
plugin.debug("Login: " + event.getPlayer().getName() + " (" + event.getPlayer().getUniqueId() + ")");
}

Player joiningPlayer = event.getPlayer();
synchronized (loginSessionLock(joiningPlayer.getUniqueId())) {
pendingLoginSessions.put(joiningPlayer.getUniqueId(), joiningPlayer);
Expand Down Expand Up @@ -170,6 +171,7 @@ public void onPlayerQuit(PlayerQuitEvent event) {

Player player = event.getPlayer();
plugin.debug("Logout: " + player.getName() + " (" + player.getUniqueId() + ")");
plugin.getUserManager().getDataManager().markUserOffline(player);

synchronized (loginSessionLock(player.getUniqueId())) {
pendingLoginSessions.remove(player.getUniqueId(), player);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@
import com.bencodez.advancedcore.api.permissions.PermissionHandler;

class PlayerJoinEventSessionTest {
@Test
void joinTracksOnlineStateWhenUserDataLoadingIsDisabled() {
AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class, RETURNS_DEEP_STUBS);
when(plugin.isEnabled()).thenReturn(true);
when(plugin.isLoadUserData()).thenReturn(false);
Player player = mock(Player.class);
org.bukkit.event.player.PlayerJoinEvent join = mock(org.bukkit.event.player.PlayerJoinEvent.class);
when(join.getPlayer()).thenReturn(player);

new PlayerJoinEvent(plugin).onPlayerLogin(join);

verify(plugin.getUserManager().getDataManager()).markUserOnline(player);
verify(plugin.getLoginTimer(), never()).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
}

@Test
void delayedLoginDoesNotRestorePermissionsAfterThePlayerQuit() {
AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class, RETURNS_DEEP_STUBS);
Expand Down
Loading
Loading