From f1f9ffc10b75bf3cb4ce50a215434c4a8dda5948 Mon Sep 17 00:00:00 2001 From: riking Date: Wed, 3 Jul 2013 21:52:19 -0700 Subject: [PATCH] Prefetch profile information from the database. Note that this implementation may open us to a denial-of-service attack by a banned user - AsyncPlayerPreLoginEvent is called before the ban lists are checked. Look into the consequences of recieving an InterruptedException in the middle of loadPlayerProfile on the integrity of the database. If possible, we would prefer to interrupt the profile fetching task when the player stops logging in, to mitigate any possible denial-of-service attacks. --- .../nossr50/listeners/PlayerListener.java | 30 +++++++++ .../runnables/player/PlayerProfileLoader.java | 19 ++++++ .../nossr50/util/player/UserManager.java | 66 ++++++++++++++++++- 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gmail/nossr50/runnables/player/PlayerProfileLoader.java diff --git a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java index f3229fb0c..bbc30ea9a 100644 --- a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java @@ -15,12 +15,14 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerFishEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; @@ -264,6 +266,34 @@ public class PlayerListener implements Listener { BleedTimerTask.bleedOut(player); // Bleed it out } + /** + * Start user data prefetch. + */ + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onFirstLogin(AsyncPlayerPreLoginEvent event) { + UserManager.prefetchUserData(event.getName()); + } + + /** + * Cancel user data prefetch if another plugin kicks them. + */ + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = false) + public void onPreLoginComplete(AsyncPlayerPreLoginEvent event) { + if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { + UserManager.discardPrefetch(event.getName()); + } + } + + /** + * Cancel user data prefetch if they're banned or a plugin kicks them. + */ + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = false) + public void onLoginComplete(PlayerLoginEvent event) { + if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { + UserManager.discardPrefetch(event.getPlayer().getName()); + } + } + /** * Monitor PlayerJoin events. * diff --git a/src/main/java/com/gmail/nossr50/runnables/player/PlayerProfileLoader.java b/src/main/java/com/gmail/nossr50/runnables/player/PlayerProfileLoader.java new file mode 100644 index 000000000..fb6465c3e --- /dev/null +++ b/src/main/java/com/gmail/nossr50/runnables/player/PlayerProfileLoader.java @@ -0,0 +1,19 @@ +package com.gmail.nossr50.runnables.player; + +import java.util.concurrent.Callable; + +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.datatypes.player.PlayerProfile; + +public class PlayerProfileLoader implements Callable { + private final String playerName; + + public PlayerProfileLoader(String player) { + this.playerName = player; + } + + @Override + public PlayerProfile call() { + return mcMMO.getDatabaseManager().loadPlayerProfile(playerName, true); + } +} diff --git a/src/main/java/com/gmail/nossr50/util/player/UserManager.java b/src/main/java/com/gmail/nossr50/util/player/UserManager.java index b547dfa9d..202ab84cf 100644 --- a/src/main/java/com/gmail/nossr50/util/player/UserManager.java +++ b/src/main/java/com/gmail/nossr50/util/player/UserManager.java @@ -1,22 +1,55 @@ package com.gmail.nossr50.util.player; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.datatypes.player.PlayerProfile; +import com.gmail.nossr50.runnables.player.PlayerProfileLoader; public final class UserManager { - private final static Map players = new HashMap(); + private final static Map players = new HashMap(); + private final static Map> loadTasks = new HashMap>(); + private final static ExecutorService loadExecutor = Executors.newCachedThreadPool(); private UserManager() {}; /** - * Add a new user. + * Asynchronously pre-fetch information about the player. This is intended + * to expedite the PlayerJoinEvent. + * + * @param playerName The player name + */ + public static void prefetchUserData(String playerName) { + loadTasks.put(playerName, loadExecutor.submit(new PlayerProfileLoader(playerName))); + } + + /** + * Discard the information from the prefetch - for example, due to the + * user being banned. + * + * @param playerName The player name + */ + public static void discardPrefetch(String playerName) { + Future oldTask = loadTasks.remove(playerName); + if (oldTask != null) { + oldTask.cancel(false); + } + } + + /** + * Add a new user. If the prefetched player information is available, it + * will be used. * * @param player The player to create a user record for * @return the player's {@link McMMOPlayer} object @@ -29,7 +62,22 @@ public final class UserManager { mcMMOPlayer.setPlayer(player); // The player object is different on each reconnection and must be updated } else { + Future task = loadTasks.remove(playerName); + if (task != null && !task.isCancelled()) { + try { + mcMMOPlayer = new McMMOPlayer(player, task.get()); + // TODO copy any additional post-processing here + players.put(playerName, mcMMOPlayer); + return mcMMOPlayer; + } + catch (ExecutionException e) { + } + catch (InterruptedException e) { + } + } + // Did not return - load on main thread mcMMOPlayer = new McMMOPlayer(player); + // (start post-processing that must be copied above) players.put(playerName, mcMMOPlayer); } @@ -43,12 +91,14 @@ public final class UserManager { */ public static void remove(String playerName) { players.remove(playerName); + discardPrefetch(playerName); } /** * Clear all users. */ public static void clearAll() { + discardAllPrefetch(); players.clear(); } @@ -56,11 +106,23 @@ public final class UserManager { * Save all users. */ public static void saveAll() { + discardAllPrefetch(); for (McMMOPlayer mcMMOPlayer : players.values()) { mcMMOPlayer.getProfile().save(); } } + /** + * Discard / cancel all data prefetching. + */ + public static void discardAllPrefetch() { + Iterator> taskIter = loadTasks.values().iterator(); + while (taskIter.hasNext()) { + taskIter.next().cancel(false); + taskIter.remove(); + } + } + public static Map getPlayers() { return players; }