33import java .util .Collection ;
44import java .util .Iterator ;
55import java .util .List ;
6+ import java .util .Map ;
67import java .util .Optional ;
78import java .util .OptionalInt ;
89import java .util .Set ;
910import java .util .UUID ;
11+ import java .util .concurrent .ConcurrentHashMap ;
1012import java .util .function .Consumer ;
1113
1214import javax .annotation .Nonnull ;
2830import io .github .bakedlibs .dough .common .ChatColors ;
2931import io .github .bakedlibs .dough .common .CommonPatterns ;
3032import io .github .bakedlibs .dough .config .Config ;
33+ import io .github .thebusybiscuit .slimefun4 .Threads ;
3134import io .github .thebusybiscuit .slimefun4 .api .events .AsyncProfileLoadEvent ;
3235import io .github .thebusybiscuit .slimefun4 .api .gps .Waypoint ;
3336import io .github .thebusybiscuit .slimefun4 .api .items .HashedArmorpiece ;
5558 */
5659public class PlayerProfile {
5760
61+ private static final Map <UUID , Boolean > loading = new ConcurrentHashMap <>();
62+
5863 private final UUID ownerId ;
5964 private final String name ;
6065
@@ -361,17 +366,37 @@ public static boolean fromUUID(@Nonnull UUID uuid, @Nonnull Consumer<PlayerProfi
361366 */
362367 public static boolean get (@ Nonnull OfflinePlayer p , @ Nonnull Consumer <PlayerProfile > callback ) {
363368 Validate .notNull (p , "Cannot get a PlayerProfile for: null!" );
364-
365369 UUID uuid = p .getUniqueId ();
370+
371+ Debug .log (TestCase .PLAYER_PROFILE_DATA , "Getting PlayerProfile for {}" , uuid );
372+
366373 PlayerProfile profile = Slimefun .getRegistry ().getPlayerProfiles ().get (uuid );
367374
368375 if (profile != null ) {
376+ Debug .log (TestCase .PLAYER_PROFILE_DATA , "PlayerProfile for {} was already loaded" , uuid );
369377 callback .accept (profile );
370378 return true ;
371379 }
372380
373- Bukkit .getScheduler ().runTaskAsynchronously (Slimefun .instance (), () -> {
381+ // If we're already loading, we don't want to spin up a whole new thread and load the profile again/more
382+ // This can very easily cause CPU, memory and thread exhaustion if the profile is large
383+ // See #4011, #4116
384+ if (loading .containsKey (uuid )) {
385+ Debug .log (TestCase .PLAYER_PROFILE_DATA , "Attempted to get PlayerProfile ({}) while loading" , uuid );
386+ Debug .log (TestCase .PLAYER_PROFILE_DATA , "Caller: {}" , Threads .getCaller ());
387+
388+ // We can't easily consume the callback so we will throw it away in this case
389+ // This will mean that if a user has attempted to do an action like open a block while
390+ // their profile is still loading. Instead of it opening after a second or whatever when the
391+ // profile is loaded, they will have to explicitly re-click the block/item/etc.
392+ // This isn't the best but I think it's totally reasonable.
393+ return false ;
394+ }
395+
396+ loading .put (uuid , true );
397+ Threads .newThread (Slimefun .instance (), "PlayerProfile#get(" + uuid + ")" , () -> {
374398 PlayerData data = Slimefun .getPlayerStorage ().loadPlayerData (p .getUniqueId ());
399+ loading .remove (uuid );
375400
376401 AsyncProfileLoadEvent event = new AsyncProfileLoadEvent (new PlayerProfile (p , data ));
377402 Bukkit .getPluginManager ().callEvent (event );
@@ -394,14 +419,28 @@ public static boolean get(@Nonnull OfflinePlayer p, @Nonnull Consumer<PlayerProf
394419 */
395420 public static boolean request (@ Nonnull OfflinePlayer p ) {
396421 Validate .notNull (p , "Cannot request a Profile for null" );
422+ Debug .log (TestCase .PLAYER_PROFILE_DATA , "Requesting PlayerProfile for {}" , p .getName ());
423+
424+ UUID uuid = p .getUniqueId ();
425+
426+ // If we're already loading, we don't want to spin up a whole new thread and load the profile again/more
427+ // This can very easily cause CPU, memory and thread exhaustion if the profile is large
428+ // See #4011, #4116
429+ if (loading .containsKey (uuid )) {
430+ Debug .log (TestCase .PLAYER_PROFILE_DATA , "Attempted to request PlayerProfile ({}) while loading" , uuid );
431+ Debug .log (TestCase .PLAYER_PROFILE_DATA , "Caller: {}" , Threads .getCaller ());
432+ return false ;
433+ }
397434
398- if (!Slimefun .getRegistry ().getPlayerProfiles ().containsKey (p .getUniqueId ())) {
435+ if (!Slimefun .getRegistry ().getPlayerProfiles ().containsKey (uuid )) {
436+ loading .put (uuid , true );
399437 // Should probably prevent multiple requests for the same profile in the future
400- Bukkit .getScheduler ().runTaskAsynchronously (Slimefun .instance (), () -> {
401- PlayerData data = Slimefun .getPlayerStorage ().loadPlayerData (p .getUniqueId ());
438+ Threads .newThread (Slimefun .instance (), "PlayerProfile#request(" + uuid + ")" , () -> {
439+ PlayerData data = Slimefun .getPlayerStorage ().loadPlayerData (uuid );
440+ loading .remove (uuid );
402441
403442 PlayerProfile pp = new PlayerProfile (p , data );
404- Slimefun .getRegistry ().getPlayerProfiles ().put (p . getUniqueId () , pp );
443+ Slimefun .getRegistry ().getPlayerProfiles ().put (uuid , pp );
405444 });
406445
407446 return false ;
0 commit comments