--- a/net/minecraft/server/players/GameProfileCache.java +++ b/net/minecraft/server/players/GameProfileCache.java @@ -56,6 +_,10 @@ private final AtomicLong operationCount = new AtomicLong(); @Nullable private Executor executor; + // Paper start - Fix GameProfileCache concurrency + protected final java.util.concurrent.locks.ReentrantLock stateLock = new java.util.concurrent.locks.ReentrantLock(); + protected final java.util.concurrent.locks.ReentrantLock lookupLock = new java.util.concurrent.locks.ReentrantLock(); + // Paper end - Fix GameProfileCache concurrency public GameProfileCache(GameProfileRepository profileRepository, File file) { this.profileRepository = profileRepository; @@ -64,10 +_,12 @@ } private void safeAdd(GameProfileCache.GameProfileInfo profile) { + try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency GameProfile profile1 = profile.getProfile(); profile.setLastAccess(this.getNextOperation()); this.profilesByName.put(profile1.getName().toLowerCase(Locale.ROOT), profile); this.profilesByUUID.put(profile1.getId(), profile); + } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency } private static Optional lookupGameProfile(GameProfileRepository profileRepo, String name) { @@ -86,6 +_,8 @@ atomicReference.set(null); } }; + if (!org.apache.commons.lang3.StringUtils.isBlank(name) // Paper - Don't lookup a profile with a blank name + && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) // Paper - Add setting for proxy online mode status profileRepo.findProfilesByNames(new String[]{name}, profileLookupCallback); GameProfile gameProfile = atomicReference.get(); return gameProfile != null ? Optional.of(gameProfile) : createUnknownProfile(name); @@ -101,7 +_,7 @@ } private static boolean usesAuthentication() { - return usesAuthentication; + return io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode(); // Paper - Add setting for proxy online mode status } public void add(GameProfile gameProfile) { @@ -111,15 +_,29 @@ Date time = instance.getTime(); GameProfileCache.GameProfileInfo gameProfileInfo = new GameProfileCache.GameProfileInfo(gameProfile, time); this.safeAdd(gameProfileInfo); - this.save(); + if(!org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) this.save(true); // Spigot - skip saving if disabled // Paper - Perf: Async GameProfileCache saving } private long getNextOperation() { return this.operationCount.incrementAndGet(); } + // Paper start + public @Nullable GameProfile getProfileIfCached(String name) { + try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency + GameProfileCache.GameProfileInfo entry = this.profilesByName.get(name.toLowerCase(Locale.ROOT)); + if (entry == null) { + return null; + } + entry.setLastAccess(this.getNextOperation()); + return entry.getProfile(); + } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency + } + // Paper end + public Optional get(String name) { String string = name.toLowerCase(Locale.ROOT); + boolean stateLocked = true; try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency GameProfileCache.GameProfileInfo gameProfileInfo = this.profilesByName.get(string); boolean flag = false; if (gameProfileInfo != null && new Date().getTime() >= gameProfileInfo.expirationDate.getTime()) { @@ -133,19 +_,24 @@ if (gameProfileInfo != null) { gameProfileInfo.setLastAccess(this.getNextOperation()); optional = Optional.of(gameProfileInfo.getProfile()); + stateLocked = false; this.stateLock.unlock(); // Paper - Fix GameProfileCache concurrency } else { - optional = lookupGameProfile(this.profileRepository, string); + stateLocked = false; this.stateLock.unlock(); // Paper - Fix GameProfileCache concurrency + try { this.lookupLock.lock(); // Paper - Fix GameProfileCache concurrency + optional = lookupGameProfile(this.profileRepository, name); // CraftBukkit - use correct case for offline players + } finally { this.lookupLock.unlock(); } // Paper - Fix GameProfileCache concurrency if (optional.isPresent()) { this.add(optional.get()); flag = false; } } - if (flag) { - this.save(); + if (flag && !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) { // Spigot - skip saving if disabled + this.save(true); // Paper - Perf: Async GameProfileCache saving } return optional; + } finally { if (stateLocked) { this.stateLock.unlock(); } } // Paper - Fix GameProfileCache concurrency } public CompletableFuture> getAsync(String name) { @@ -157,7 +_,7 @@ return completableFuture; } else { CompletableFuture> completableFuture1 = CompletableFuture.>supplyAsync( - () -> this.get(name), Util.backgroundExecutor().forName("getProfile") + () -> this.get(name), Util.PROFILE_EXECUTOR // Paper - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread ) .whenCompleteAsync((gameProfile, exception) -> this.requests.remove(name), this.executor); this.requests.put(name, completableFuture1); @@ -167,6 +_,7 @@ } public Optional get(UUID uuid) { + try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency GameProfileCache.GameProfileInfo gameProfileInfo = this.profilesByUUID.get(uuid); if (gameProfileInfo == null) { return Optional.empty(); @@ -174,6 +_,7 @@ gameProfileInfo.setLastAccess(this.getNextOperation()); return Optional.of(gameProfileInfo.getProfile()); } + } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency } public void setExecutor(Executor exectutor) { @@ -206,6 +_,11 @@ return (List)var9; } catch (FileNotFoundException var7) { + // Spigot Start + } catch (com.google.gson.JsonSyntaxException | NullPointerException ex) { + GameProfileCache.LOGGER.warn( "Usercache.json is corrupted or has bad formatting. Deleting it to prevent further issues." ); + this.file.delete(); + // Spigot End } catch (JsonParseException | IOException var8) { LOGGER.warn("Failed to load profile cache {}", this.file, var8); } @@ -213,24 +_,45 @@ return list; } - public void save() { + public void save(boolean asyncSave) { // Paper - Perf: Async GameProfileCache saving JsonArray jsonArray = new JsonArray(); DateFormat dateFormat = createDateFormat(); - this.getTopMRUProfiles(1000).forEach(info -> jsonArray.add(writeGameProfile(info, dateFormat))); + this.listTopMRUProfiles(org.spigotmc.SpigotConfig.userCacheCap).forEach((info) -> jsonArray.add(writeGameProfile(info, dateFormat))); // Spigot // Paper - Fix GameProfileCache concurrency String string = this.gson.toJson((JsonElement)jsonArray); + Runnable save = () -> { // Paper - Perf: Async GameProfileCache saving try (Writer writer = Files.newWriter(this.file, StandardCharsets.UTF_8)) { writer.write(string); } catch (IOException var9) { } + // Paper start - Perf: Async GameProfileCache saving + }; + if (asyncSave) { + io.papermc.paper.util.MCUtil.scheduleAsyncTask(save); + } else { + save.run(); + } + // Paper end - Perf: Async GameProfileCache saving } private Stream getTopMRUProfiles(int limit) { - return ImmutableList.copyOf(this.profilesByUUID.values()) - .stream() - .sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()) - .limit(limit); - } + // Paper start - Fix GameProfileCache concurrency + return this.listTopMRUProfiles(limit).stream(); + } + + private List listTopMRUProfiles(int limit) { + try { + this.stateLock.lock(); + return this.profilesByUUID.values() + .stream() + .sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()) + .limit(limit) + .toList(); + } finally { + this.stateLock.unlock(); + } + } + // Paper end - Fix GameProfileCache concurrency private static JsonElement writeGameProfile(GameProfileCache.GameProfileInfo profileInfo, DateFormat dateFormat) { JsonObject jsonObject = new JsonObject();