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.
This commit is contained in:
riking 2013-07-03 21:52:19 -07:00
parent 8fe18be79b
commit f1f9ffc10b
3 changed files with 113 additions and 2 deletions

View File

@ -15,12 +15,14 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerFishEvent; import org.bukkit.event.player.PlayerFishEvent;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.event.player.PlayerPickupItemEvent;
import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerRespawnEvent;
@ -264,6 +266,34 @@ public class PlayerListener implements Listener {
BleedTimerTask.bleedOut(player); // Bleed it out 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. * Monitor PlayerJoin events.
* *

View File

@ -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<PlayerProfile> {
private final String playerName;
public PlayerProfileLoader(String player) {
this.playerName = player;
}
@Override
public PlayerProfile call() {
return mcMMO.getDatabaseManager().loadPlayerProfile(playerName, true);
}
}

View File

@ -1,22 +1,55 @@
package com.gmail.nossr50.util.player; package com.gmail.nossr50.util.player;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; 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.OfflinePlayer;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.datatypes.player.McMMOPlayer; 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 { public final class UserManager {
private final static Map<String, McMMOPlayer> players = new HashMap<String, McMMOPlayer>(); private final static Map<String, McMMOPlayer> players = new HashMap<String, McMMOPlayer>();
private final static Map<String, Future<PlayerProfile>> loadTasks = new HashMap<String, Future<PlayerProfile>>();
private final static ExecutorService loadExecutor = Executors.newCachedThreadPool();
private UserManager() {}; 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<PlayerProfile> 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 * @param player The player to create a user record for
* @return the player's {@link McMMOPlayer} object * @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 mcMMOPlayer.setPlayer(player); // The player object is different on each reconnection and must be updated
} }
else { else {
Future<PlayerProfile> 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); mcMMOPlayer = new McMMOPlayer(player);
// (start post-processing that must be copied above)
players.put(playerName, mcMMOPlayer); players.put(playerName, mcMMOPlayer);
} }
@ -43,12 +91,14 @@ public final class UserManager {
*/ */
public static void remove(String playerName) { public static void remove(String playerName) {
players.remove(playerName); players.remove(playerName);
discardPrefetch(playerName);
} }
/** /**
* Clear all users. * Clear all users.
*/ */
public static void clearAll() { public static void clearAll() {
discardAllPrefetch();
players.clear(); players.clear();
} }
@ -56,11 +106,23 @@ public final class UserManager {
* Save all users. * Save all users.
*/ */
public static void saveAll() { public static void saveAll() {
discardAllPrefetch();
for (McMMOPlayer mcMMOPlayer : players.values()) { for (McMMOPlayer mcMMOPlayer : players.values()) {
mcMMOPlayer.getProfile().save(); mcMMOPlayer.getProfile().save();
} }
} }
/**
* Discard / cancel all data prefetching.
*/
public static void discardAllPrefetch() {
Iterator<Future<PlayerProfile>> taskIter = loadTasks.values().iterator();
while (taskIter.hasNext()) {
taskIter.next().cancel(false);
taskIter.remove();
}
}
public static Map<String, McMMOPlayer> getPlayers() { public static Map<String, McMMOPlayer> getPlayers() {
return players; return players;
} }