Browse Source

Optimizations and potentially fixing a ConcurrentModificationException involving the TransientEntityTracker
Fixes #4368

nossr50 4 years ago
parent
commit
fbecbc167a

+ 7 - 1
Changelog.txt

@@ -1,8 +1,14 @@
 Version 2.1.166
 Version 2.1.166
+    Fixed a small memory leak in the new COTW tracker
+    Potentially fixed a ConcurrentModificationException involving the TransientEntityTracker (report this error if you encounter it)
     Music discs removed from the default fishing_treasures.yml
     Music discs removed from the default fishing_treasures.yml
+    Optimized how mcMMO saves player data (should improve timings on servers with bad disk speeds and or bad connectivity to their SQL server instance)
 
 
     NOTES:
     NOTES:
-    No one likes fishing up music discs
+    No one likes fishing up music discs, if you want this change it is recommended you delete fishing_treasures.yml and let it regenerate
+    If any of you encounter a ConcurrentModificationException error that mentions TransientEntityTracker in its stack trace after this update let me know, I have another fix in mind for this if this update didn't fix it.
+    (You won't have this file if you haven't updated in a while, if so you don't need to do anything)
+
 Version 2.1.165
 Version 2.1.165
     Fixed a bug where Enchanted Books dropped by mcMMO (in Fishing) did not function correctly
     Fixed a bug where Enchanted Books dropped by mcMMO (in Fishing) did not function correctly
     The mcMMO system which tracks player placed blocks has had some major rewrites (thanks t00thpick1)
     The mcMMO system which tracks player placed blocks has had some major rewrites (thanks t00thpick1)

+ 3 - 2
src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java

@@ -131,10 +131,11 @@ public class PlayerProfile {
             {
             {
                 saveAttempts++;
                 saveAttempts++;
 
 
-                if(useSync)
+                //Back out of async saving if we detect a server shutdown, this is not always going to be caught
+                if(mcMMO.isServerShutdownExecuted() || useSync)
                     scheduleSyncSave(); //Execute sync saves immediately
                     scheduleSyncSave(); //Execute sync saves immediately
                 else
                 else
-                    scheduleAsyncSaveDelay();
+                    scheduleAsyncSave();
 
 
             } else {
             } else {
                 mcMMO.p.getLogger().severe("mcMMO has failed to save the profile for "
                 mcMMO.p.getLogger().severe("mcMMO has failed to save the profile for "

+ 5 - 6
src/main/java/com/gmail/nossr50/listeners/PlayerListener.java

@@ -526,16 +526,15 @@ public class PlayerListener implements Listener {
             return;
             return;
         }
         }
 
 
+        McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
+
         //Profile not loaded
         //Profile not loaded
-        if(UserManager.getPlayer(player) == null)
-        {
+        if(mcMMOPlayer == null) {
             return;
             return;
         }
         }
 
 
-        McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
-        //There's an issue with using Async saves on player quit
-        //Basically there are conditions in which an async task does not execute fast enough to save the data if the server shutdown shortly after this task was scheduled
-        mcMMOPlayer.logout(true);
+        //Use a sync save if the server is shutting down to avoid race conditions
+        mcMMOPlayer.logout(mcMMO.isServerShutdownExecuted());
     }
     }
 
 
     /**
     /**

+ 20 - 5
src/main/java/com/gmail/nossr50/mcMMO.java

@@ -88,6 +88,7 @@ public class mcMMO extends JavaPlugin {
     private static ChatManager chatManager;
     private static ChatManager chatManager;
     private static CommandManager commandManager; //ACF
     private static CommandManager commandManager; //ACF
     private static TransientEntityTracker transientEntityTracker;
     private static TransientEntityTracker transientEntityTracker;
+    private static boolean serverShutdownExecuted = false;
 
 
     /* Adventure */
     /* Adventure */
     private static BukkitAudiences audiences;
     private static BukkitAudiences audiences;
@@ -292,6 +293,7 @@ public class mcMMO extends JavaPlugin {
         commandManager = new CommandManager(this);
         commandManager = new CommandManager(this);
 
 
         transientEntityTracker = new TransientEntityTracker();
         transientEntityTracker = new TransientEntityTracker();
+        setServerShutdown(false); //Reset flag, used to make decisions about async saves
     }
     }
 
 
     public static PlayerLevelUtils getPlayerLevelUtils() {
     public static PlayerLevelUtils getPlayerLevelUtils() {
@@ -327,6 +329,10 @@ public class mcMMO extends JavaPlugin {
      */
      */
     @Override
     @Override
     public void onDisable() {
     public void onDisable() {
+        setServerShutdown(true);
+        //TODO: Write code to catch unfinished async save tasks, for now we just hope they finish in time, which they should in most cases
+        mcMMO.p.getLogger().info("Server shutdown has been executed, saving and cleaning up data...");
+
         try {
         try {
             UserManager.saveAll();      // Make sure to save player information if the server shuts down
             UserManager.saveAll();      // Make sure to save player information if the server shuts down
             UserManager.clearAll();
             UserManager.clearAll();
@@ -345,11 +351,6 @@ public class mcMMO extends JavaPlugin {
 
 
         catch (Exception e) { e.printStackTrace(); }
         catch (Exception e) { e.printStackTrace(); }
 
 
-        debug("Canceling all tasks...");
-        getServer().getScheduler().cancelTasks(this); // This removes our tasks
-        debug("Unregister all events...");
-        HandlerList.unregisterAll(this); // Cancel event registrations
-
         if (Config.getInstance().getBackupsEnabled()) {
         if (Config.getInstance().getBackupsEnabled()) {
             // Remove other tasks BEFORE starting the Backup, or we just cancel it straight away.
             // Remove other tasks BEFORE starting the Backup, or we just cancel it straight away.
             try {
             try {
@@ -369,6 +370,11 @@ public class mcMMO extends JavaPlugin {
             }
             }
         }
         }
 
 
+        debug("Canceling all tasks...");
+        getServer().getScheduler().cancelTasks(this); // This removes our tasks
+        debug("Unregister all events...");
+        HandlerList.unregisterAll(this); // Cancel event registrations
+
         databaseManager.onDisable();
         databaseManager.onDisable();
         debug("Was disabled."); // How informative!
         debug("Was disabled."); // How informative!
     }
     }
@@ -727,4 +733,13 @@ public class mcMMO extends JavaPlugin {
     public static TransientEntityTracker getTransientEntityTracker() {
     public static TransientEntityTracker getTransientEntityTracker() {
         return transientEntityTracker;
         return transientEntityTracker;
     }
     }
+
+    public static synchronized boolean isServerShutdownExecuted() {
+        return serverShutdownExecuted;
+    }
+
+    private static synchronized void setServerShutdown(boolean bool) {
+        serverShutdownExecuted = bool;
+    }
+
 }
 }

+ 1 - 1
src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java

@@ -52,7 +52,7 @@ public class TamingManager extends SkillManager {
         lastSummonTimeStamp = 0L;
         lastSummonTimeStamp = 0L;
 
 
         //Init per-player tracking of summoned entities
         //Init per-player tracking of summoned entities
-        mcMMO.getTransientEntityTracker().initPlayer(mmoPlayer.getPlayer().getUniqueId());
+        mcMMO.getTransientEntityTracker().initPlayer(mmoPlayer.getPlayer());
 
 
         //Hacky stuff used as a band-aid
         //Hacky stuff used as a band-aid
         initStaticCaches();
         initStaticCaches();

+ 108 - 34
src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java

@@ -19,6 +19,7 @@ import org.jetbrains.annotations.Nullable;
 import java.util.*;
 import java.util.*;
 
 
 public class TransientEntityTracker {
 public class TransientEntityTracker {
+    //These two are updated in step with each other
     private final @NotNull HashMap<UUID, HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>>> perPlayerTransientEntityMap;
     private final @NotNull HashMap<UUID, HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>>> perPlayerTransientEntityMap;
     private final @NotNull HashSet<LivingEntity> chunkLookupCache;
     private final @NotNull HashSet<LivingEntity> chunkLookupCache;
 
 
@@ -27,30 +28,74 @@ public class TransientEntityTracker {
         chunkLookupCache = new HashSet<>();
         chunkLookupCache = new HashSet<>();
     }
     }
 
 
-    public void initPlayer(@NotNull UUID playerUUID) {
-        if (!isPlayerRegistered(playerUUID)) {
-            registerPlayer(playerUUID);
+    public synchronized @NotNull HashSet<LivingEntity> getChunkLookupCache() {
+        return chunkLookupCache;
+    }
+
+    public synchronized @NotNull HashMap<UUID, HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>>> getPerPlayerTransientEntityMap() {
+        return perPlayerTransientEntityMap;
+    }
+
+    public synchronized void initPlayer(@NotNull Player player) {
+        if (!isPlayerRegistered(player.getUniqueId())) {
+            registerPlayer(player.getUniqueId());
         }
         }
     }
     }
 
 
-    public void cleanupPlayer(@NotNull UUID playerUUID) {
-        cleanupAllSummons(null, playerUUID);
+    /**
+     * Removes a player from the tracker
+     *
+     * @param playerUUID target player
+     */
+    public synchronized void cleanupPlayer(@NotNull UUID playerUUID) {
+        cleanPlayer(null, playerUUID);
+    }
+
+    /**
+     * Removes a player from the tracker
+     *
+     * @param player target player
+     */
+    public synchronized void cleanupPlayer(@NotNull Player player) {
+        cleanPlayer(player, player.getUniqueId());
     }
     }
 
 
-    public void cleanupPlayer(@NotNull Player player) {
-        //First remove all entities related to this player
+    /**
+     * Removes a player from the tracker
+     *
+     * @param player target player
+     * @param playerUUID target player UUID
+     */
+    private void cleanPlayer(@Nullable Player player, @NotNull UUID playerUUID) {
         cleanupAllSummons(player, player.getUniqueId());
         cleanupAllSummons(player, player.getUniqueId());
+        removePlayerFromMap(playerUUID);
+    }
+
+    private void removePlayerFromMap(@NotNull UUID playerUUID) {
+        getPerPlayerTransientEntityMap().remove(playerUUID);
     }
     }
 
 
-    private boolean isPlayerRegistered(@NotNull UUID playerUUID) {
-        return perPlayerTransientEntityMap.get(playerUUID) != null;
+    /**
+     * Checks if a player has already been registered
+     * Being registered constitutes having necessary values initialized in our per-player map
+     *
+     * @param playerUUID target player
+     * @return true if the player is registered
+     */
+    private synchronized boolean isPlayerRegistered(@NotNull UUID playerUUID) {
+        return getPerPlayerTransientEntityMap().get(playerUUID) != null;
     }
     }
 
 
-    private void registerPlayer(@NotNull UUID playerUUID) {
-        perPlayerTransientEntityMap.put(playerUUID, new HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>>());
+    /**
+     * Register a player to our tracker, which initializes the necessary values in our per-player map
+     *
+     * @param playerUUID player to register
+     */
+    private synchronized void registerPlayer(@NotNull UUID playerUUID) {
+        getPerPlayerTransientEntityMap().put(playerUUID, new HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>>());
 
 
         for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) {
         for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) {
-            perPlayerTransientEntityMap.get(playerUUID).put(callOfTheWildType, new HashSet<>());
+            getPerPlayerTransientEntityMap().get(playerUUID).put(callOfTheWildType, new HashSet<>());
         }
         }
     }
     }
 
 
@@ -60,16 +105,18 @@ public class TransientEntityTracker {
      * @param playerUUID the target uuid of the player
      * @param playerUUID the target uuid of the player
      * @return the tracked entities map for the player, null if the player isn't registered
      * @return the tracked entities map for the player, null if the player isn't registered
      */
      */
-    public @Nullable HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>> getPlayerTrackedEntityMap(@NotNull UUID playerUUID) {
-        return perPlayerTransientEntityMap.get(playerUUID);
+    public synchronized @Nullable HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>> getPlayerTrackedEntityMap(@NotNull UUID playerUUID) {
+        return getPerPlayerTransientEntityMap().get(playerUUID);
     }
     }
 
 
-    public void registerEntity(@NotNull UUID playerUUID, @NotNull TrackedTamingEntity trackedTamingEntity) {
-        if(!isPlayerRegistered(playerUUID)) {
-            mcMMO.p.getLogger().severe("Attempting to register entity to a player which hasn't been initialized!");
-            initPlayer(playerUUID);
-        }
-
+    /**
+     * Registers an entity to a player
+     * This includes registration to our per-player map and our chunk lookup cache
+     *
+     * @param playerUUID target player's UUID
+     * @param trackedTamingEntity target entity
+     */
+    public synchronized void registerEntity(@NotNull UUID playerUUID, @NotNull TrackedTamingEntity trackedTamingEntity) {
         //Add to map entry
         //Add to map entry
         getTrackedEntities(playerUUID, trackedTamingEntity.getCallOfTheWildType()).add(trackedTamingEntity);
         getTrackedEntities(playerUUID, trackedTamingEntity.getCallOfTheWildType()).add(trackedTamingEntity);
 
 
@@ -85,7 +132,7 @@ public class TransientEntityTracker {
      * @param callOfTheWildType target type
      * @param callOfTheWildType target type
      * @return the set of tracked entities for the player, null if the player isn't registered, the set can be empty
      * @return the set of tracked entities for the player, null if the player isn't registered, the set can be empty
      */
      */
-    private @Nullable HashSet<TrackedTamingEntity> getTrackedEntities(@NotNull UUID playerUUID, @NotNull CallOfTheWildType callOfTheWildType) {
+    private synchronized @Nullable HashSet<TrackedTamingEntity> getTrackedEntities(@NotNull UUID playerUUID, @NotNull CallOfTheWildType callOfTheWildType) {
         HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>> playerEntityMap = getPlayerTrackedEntityMap(playerUUID);
         HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>> playerEntityMap = getPlayerTrackedEntityMap(playerUUID);
 
 
         if(playerEntityMap == null)
         if(playerEntityMap == null)
@@ -94,21 +141,45 @@ public class TransientEntityTracker {
         return playerEntityMap.get(callOfTheWildType);
         return playerEntityMap.get(callOfTheWildType);
     }
     }
 
 
-    private void addToChunkLookupCache(@NotNull TrackedTamingEntity trackedTamingEntity) {
-        chunkLookupCache.add(trackedTamingEntity.getLivingEntity());
+    /**
+     * Adds an entity to our chunk lookup cache
+     *
+     * @param trackedTamingEntity target tracked taming entity
+     */
+    private synchronized void addToChunkLookupCache(@NotNull TrackedTamingEntity trackedTamingEntity) {
+        getChunkLookupCache().add(trackedTamingEntity.getLivingEntity());
     }
     }
 
 
-    public void unregisterEntity(@NotNull LivingEntity livingEntity) {
+    /**
+     * Removes an entity from our tracker
+     * This includes removal from our per-player map and our chunk lookup cache
+     *
+     * @param livingEntity target entity
+     */
+    private void unregisterEntity(@NotNull LivingEntity livingEntity) {
         chunkLookupCacheCleanup(livingEntity);
         chunkLookupCacheCleanup(livingEntity);
         perPlayerTransientMapCleanup(livingEntity);
         perPlayerTransientMapCleanup(livingEntity);
     }
     }
 
 
+    /**
+     * Removes an entity from our chunk lookup cache
+     *
+     * @param livingEntity target entity
+     */
     private void chunkLookupCacheCleanup(@NotNull LivingEntity livingEntity) {
     private void chunkLookupCacheCleanup(@NotNull LivingEntity livingEntity) {
-        chunkLookupCache.remove(livingEntity);
+        getChunkLookupCache().remove(livingEntity);
     }
     }
 
 
+    /**
+     * Clean a living entity from our tracker
+     * Iterates over all players and their registered entities
+     * Doesn't do any kind of failure checking, if it doesn't find any player with a registered entity nothing bad happens or is reported
+     * However it should never happen like that, so maybe we could consider adding some failure to execute checking in the future
+     *
+     * @param livingEntity
+     */
     private void perPlayerTransientMapCleanup(@NotNull LivingEntity livingEntity) {
     private void perPlayerTransientMapCleanup(@NotNull LivingEntity livingEntity) {
-        for(UUID uuid : perPlayerTransientEntityMap.keySet()) {
+        for(UUID uuid : getPerPlayerTransientEntityMap().keySet()) {
             for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) {
             for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) {
 
 
                 HashSet<TrackedTamingEntity> trackedEntities = getTrackedEntities(uuid, callOfTheWildType);
                 HashSet<TrackedTamingEntity> trackedEntities = getTrackedEntities(uuid, callOfTheWildType);
@@ -127,10 +198,16 @@ public class TransientEntityTracker {
         }
         }
     }
     }
 
 
-    public @NotNull List<LivingEntity> getAllTransientEntitiesInChunk(@NotNull Chunk chunk) {
+    /**
+     * Get all transient entities that exist in a specific chunk
+     *
+     * @param chunk the chunk to match
+     * @return a list of transient entities that are located in the provided chunk
+     */
+    public synchronized @NotNull List<LivingEntity> getAllTransientEntitiesInChunk(@NotNull Chunk chunk) {
         ArrayList<LivingEntity> matchingEntities = new ArrayList<>();
         ArrayList<LivingEntity> matchingEntities = new ArrayList<>();
 
 
-        for(LivingEntity livingEntity : chunkLookupCache) {
+        for(LivingEntity livingEntity : getChunkLookupCache()) {
             if(livingEntity.getLocation().getChunk().equals(chunk)) {
             if(livingEntity.getLocation().getChunk().equals(chunk)) {
                 matchingEntities.add(livingEntity);
                 matchingEntities.add(livingEntity);
             }
             }
@@ -139,17 +216,14 @@ public class TransientEntityTracker {
         return matchingEntities;
         return matchingEntities;
     }
     }
 
 
-    /*
-     * Gross code below
-     */
-
     /**
     /**
      * Get the amount of a summon currently active for a player
      * Get the amount of a summon currently active for a player
+     *
      * @param playerUUID target player
      * @param playerUUID target player
      * @param callOfTheWildType summon type
      * @param callOfTheWildType summon type
      * @return the amount of summons currently active for player of target type
      * @return the amount of summons currently active for player of target type
      */
      */
-    public int getAmountCurrentlySummoned(@NotNull UUID playerUUID, @NotNull CallOfTheWildType callOfTheWildType) {
+    public synchronized int getAmountCurrentlySummoned(@NotNull UUID playerUUID, @NotNull CallOfTheWildType callOfTheWildType) {
         HashSet<TrackedTamingEntity> trackedEntities = getTrackedEntities(playerUUID, callOfTheWildType);
         HashSet<TrackedTamingEntity> trackedEntities = getTrackedEntities(playerUUID, callOfTheWildType);
 
 
         if(trackedEntities == null)
         if(trackedEntities == null)
@@ -165,7 +239,7 @@ public class TransientEntityTracker {
      * @param livingEntity entity to remove
      * @param livingEntity entity to remove
      * @param player associated player
      * @param player associated player
      */
      */
-    public void removeSummon(@NotNull LivingEntity livingEntity, @Nullable Player player, boolean timeExpired) {
+    public synchronized void removeSummon(@NotNull LivingEntity livingEntity, @Nullable Player player, boolean timeExpired) {
         //Kill the summon & remove it
         //Kill the summon & remove it
         if(livingEntity.isValid()) {
         if(livingEntity.isValid()) {
             livingEntity.setHealth(0); //Should trigger entity death events
             livingEntity.setHealth(0); //Should trigger entity death events