diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/session/BukkitSessionManager.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/session/BukkitSessionManager.java index 2e64fb738..f270d9d4d 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/session/BukkitSessionManager.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/session/BukkitSessionManager.java @@ -19,6 +19,7 @@ package com.sk89q.worldguard.bukkit.session; +import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.world.World; import com.sk89q.worldguard.LocalPlayer; import com.sk89q.worldguard.WorldGuard; @@ -32,6 +33,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldUnloadEvent; import java.util.function.Consumer; @@ -96,6 +98,16 @@ public void accept(Object ignored) { task.run(); } } + + // Force eviction of expired bypass/session entries. Without this the + // caches only evict lazily, so an idle entry can keep a player (and + // their world) reachable long after its logical lifetime. + cleanUpCaches(); + } + + @EventHandler + public void onWorldUnload(WorldUnloadEvent event) { + forgetWorld(BukkitAdapter.adapt(event.getWorld())); } @Override diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/session/AbstractSessionManager.java b/worldguard-core/src/main/java/com/sk89q/worldguard/session/AbstractSessionManager.java index 0f9a05882..2dfea9f25 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/session/AbstractSessionManager.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/session/AbstractSessionManager.java @@ -164,6 +164,26 @@ public Session getIfPresent(LocalPlayer player) { return getIfPresentInternal(new CacheKey(player)); } + @Override + public void forgetWorld(World world) { + checkNotNull(world, "world"); + bypassCache.asMap().keySet().removeIf(tuple -> tuple.getWorld().equals(world)); + } + + /** + * Force the caches to evict any entries that have already expired. + * + *
The bypass cache uses {@code expireAfterWrite} and the session cache + * uses {@code expireAfterAccess}, but Guava only evicts expired entries + * lazily during cache operations. When the caches go idle, an expired + * entry can keep a strong reference to a player or world alive long after + * its logical lifetime. Calling this periodically bounds that retention.
+ */ + protected void cleanUpCaches() { + bypassCache.cleanUp(); + sessions.cleanUp(); + } + private Session getIfPresentInternal(CacheKey cacheKey) { @Nullable Session session = sessions.getIfPresent(cacheKey); if (session != null) { diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/session/Session.java b/worldguard-core/src/main/java/com/sk89q/worldguard/session/Session.java index f52f00cc7..baa9fe84e 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/session/Session.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/session/Session.java @@ -143,6 +143,9 @@ public void uninitialize(LocalPlayer player) { for (Handler handler : handlers.values()) { handler.uninitialize(player, location, set); } + + lastValid = null; + lastRegionSet = null; } /** diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/session/SessionManager.java b/worldguard-core/src/main/java/com/sk89q/worldguard/session/SessionManager.java index fce2bf340..809bfaa84 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/session/SessionManager.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/session/SessionManager.java @@ -108,6 +108,18 @@ public interface SessionManager { */ @Nullable Session getIfPresent(LocalPlayer player); + /** + * Forget any cached bypass information that references the given world. + * + *This should be called when a world is unloaded so that the bypass + * cache does not retain a strong reference to the world (and the players + * that were last checked in it) until the cache entries happen to + * expire.
+ * + * @param world The world to forget + */ + void forgetWorld(World world); + /** * Get a player's session. A session will be created if there is no * existing session for the player.