#1125 Create infrastructure for Limbo persistence + restore 5.2 JSON storage

- Introduce configurable storage mechanism
  - LimboPersistence wraps a LimboPersistenceHandler, of which there are multiple implementations
  - Outside of the limbo.persistence package, classes only talk to LimboPersistence
  - Restore the way of persisting to JSON from 5.2 (SeparateFilePersistenceHandler)

- Add handling for stored limbo players
  - Merge any existing LimboPlayers together with the goal of only keeping one version of a LimboPlayer: there is no way for a player to be online without triggering the creation of a LimboPlayer first, so we can guarantee that the in-memory LimboPlayer is the most up-to-date, i.e. when restoring limbo data we don't have to check against the disk.
  - Create and delete LimboPlayers at the same time when LimboPlayers are added or removed from the in-memory map

- Catch all exceptions in LimboPersistence so a handler throwing an unexpected exception does not stop the limbo process (#1070)

- Extend debug command /authme debug limbo to show LimboPlayer information on disk, too
This commit is contained in:
ljacqu
2017-03-12 18:43:37 +01:00
parent 1678901e02
commit 8557621c02
16 changed files with 733 additions and 28 deletions
@@ -1,6 +1,7 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
import fr.xephi.authme.settings.Settings;
import org.bukkit.entity.Player;
@@ -28,7 +29,10 @@ public class LimboService {
private LimboPlayerTaskManager taskManager;
@Inject
private LimboServiceHelper limboServiceHelper;
private LimboServiceHelper helper;
@Inject
private LimboPersistence persistence;
LimboService() {
}
@@ -42,19 +46,25 @@ public class LimboService {
public void createLimboPlayer(Player player, boolean isRegistered) {
final String name = player.getName().toLowerCase();
LimboPlayer limboFromDisk = persistence.getLimboPlayer(player);
if (limboFromDisk != null) {
ConsoleLogger.debug("LimboPlayer for `{0}` already exists on disk", name);
}
LimboPlayer existingLimbo = entries.remove(name);
if (existingLimbo != null) {
existingLimbo.clearTasks();
ConsoleLogger.debug("LimboPlayer for `{0}` was already present", name);
ConsoleLogger.debug("LimboPlayer for `{0}` already present in memory", name);
}
LimboPlayer limboPlayer = limboServiceHelper.merge(
limboServiceHelper.createLimboPlayer(player, isRegistered), existingLimbo);
LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk);
limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered), limboPlayer);
taskManager.registerMessageTask(player, limboPlayer, isRegistered);
taskManager.registerTimeoutTask(player, limboPlayer);
limboServiceHelper.revokeLimboStates(player);
helper.revokeLimboStates(player);
entries.put(name, limboPlayer);
persistence.saveLimboPlayer(player, limboPlayer);
}
/**
@@ -98,6 +108,7 @@ public class LimboService {
settings.getProperty(RESTORE_WALK_SPEED).restoreWalkSpeed(player, limbo);
limbo.clearTasks();
ConsoleLogger.debug("Restored LimboPlayer stats for `{0}`", lowerName);
persistence.removeLimboPlayer(player);
}
}
@@ -69,8 +69,7 @@ class LimboServiceHelper {
* <ul>
* <li><code>isOperator, allowFlight</code>: true if either limbo has true</li>
* <li><code>flySpeed, walkSpeed</code>: maximum value of either limbo player</li>
* <li><code>group</code>: from old limbo if not empty, otherwise from new limbo</li>
* <li><code>location</code>: from old limbo</li>
* <li><code>group, location</code>: from old limbo if not empty/null, otherwise from new limbo</li>
* </ul>
*
* @param newLimbo the new limbo player
@@ -89,8 +88,9 @@ class LimboServiceHelper {
float flySpeed = Math.max(newLimbo.getFlySpeed(), oldLimbo.getFlySpeed());
float walkSpeed = Math.max(newLimbo.getWalkSpeed(), oldLimbo.getWalkSpeed());
String group = firstNotEmpty(newLimbo.getGroup(), oldLimbo.getGroup());
Location location = firstNotNull(oldLimbo.getLocation(), newLimbo.getLocation());
return new LimboPlayer(oldLimbo.getLocation(), isOperator, group, canFly, walkSpeed, flySpeed);
return new LimboPlayer(location, isOperator, group, canFly, walkSpeed, flySpeed);
}
private static String firstNotEmpty(String newGroup, String oldGroup) {
@@ -100,4 +100,8 @@ class LimboServiceHelper {
}
return oldGroup;
}
private static Location firstNotNull(Location first, Location second) {
return first == null ? second : first;
}
}
@@ -0,0 +1,82 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.initialization.factory.Factory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.LimboSettings;
import org.bukkit.entity.Player;
import javax.inject.Inject;
/**
* Handles the persistence of LimboPlayers.
*/
public class LimboPersistence implements SettingsDependent {
private final Factory<LimboPersistenceHandler> handlerFactory;
private LimboPersistenceHandler handler;
@Inject
LimboPersistence(Settings settings, Factory<LimboPersistenceHandler> handlerFactory) {
this.handlerFactory = handlerFactory;
reload(settings);
}
/**
* Retrieves the LimboPlayer for the given player if available.
*
* @param player the player to retrieve the LimboPlayer for
* @return the player's limbo player, or null if not available
*/
public LimboPlayer getLimboPlayer(Player player) {
try {
return handler.getLimboPlayer(player);
} catch (Exception e) {
ConsoleLogger.logException("Could not get LimboPlayer for '" + player.getName() + "'", e);
}
return null;
}
/**
* Saves the given LimboPlayer for the provided player.
*
* @param player the player to save the LimboPlayer for
* @param limbo the limbo player to save
*/
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
try {
handler.saveLimboPlayer(player, limbo);
} catch (Exception e) {
ConsoleLogger.logException("Could not save LimboPlayer for '" + player.getName() + "'", e);
}
}
/**
* Removes the LimboPlayer for the given player.
*
* @param player the player whose LimboPlayer should be removed
*/
public void removeLimboPlayer(Player player) {
try {
handler.removeLimboPlayer(player);
} catch (Exception e) {
ConsoleLogger.logException("Could not remove LimboPlayer for '" + player.getName() + "'", e);
}
}
@Override
public void reload(Settings settings) {
LimboPersistenceType persistenceType = settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE);
if (handler == null || handler.getType() != persistenceType) {
// If we're changing from an existing handler, output a quick hint that nothing is converted.
if (handler != null) {
ConsoleLogger.info("Limbo persistence type has changed! Note that the data is not converted.");
}
handler = handlerFactory.newInstance(persistenceType.getImplementationClass());
}
}
}
@@ -0,0 +1,39 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.entity.Player;
/**
* Handles I/O for storing LimboPlayer objects.
*/
interface LimboPersistenceHandler {
/**
* Returns the limbo player for the given player if it exists.
*
* @param player the player
* @return the stored limbo player, or null if not available
*/
LimboPlayer getLimboPlayer(Player player);
/**
* Saves the given limbo player for the given player to the disk.
*
* @param player the player to save the limbo player for
* @param limbo the limbo player to save
*/
void saveLimboPlayer(Player player, LimboPlayer limbo);
/**
* Removes the limbo player from the disk.
*
* @param player the player whose limbo player should be removed
*/
void removeLimboPlayer(Player player);
/**
* @return the type of the limbo persistence implementation
*/
LimboPersistenceType getType();
}
@@ -0,0 +1,29 @@
package fr.xephi.authme.data.limbo.persistence;
/**
* Types of persistence for LimboPlayer objects.
*/
public enum LimboPersistenceType {
INDIVIDUAL_FILES(SeparateFilePersistenceHandler.class),
DISABLED(NoOpPersistenceHandler.class);
private final Class<? extends LimboPersistenceHandler> implementationClass;
/**
* Constructor.
*
* @param implementationClass the implementation class
*/
LimboPersistenceType(Class<? extends LimboPersistenceHandler> implementationClass) {
this.implementationClass= implementationClass;
}
/**
* @return class implementing the persistence type
*/
public Class<? extends LimboPersistenceHandler> getImplementationClass() {
return implementationClass;
}
}
@@ -0,0 +1,30 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.entity.Player;
/**
* Limbo player persistence implementation that does nothing.
*/
class NoOpPersistenceHandler implements LimboPersistenceHandler {
@Override
public LimboPlayer getLimboPlayer(Player player) {
return null;
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
// noop
}
@Override
public void removeLimboPlayer(Player player) {
// noop
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.DISABLED;
}
}
@@ -0,0 +1,178 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.FileUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
/**
* Saves LimboPlayer objects as JSON into individual files.
*/
class SeparateFilePersistenceHandler implements LimboPersistenceHandler {
private final Gson gson;
private final File cacheDir;
private final BukkitService bukkitService;
@Inject
SeparateFilePersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService) {
this.bukkitService = bukkitService;
cacheDir = new File(dataFolder, "playerdata");
if (!cacheDir.exists() && !cacheDir.isDirectory() && !cacheDir.mkdir()) {
ConsoleLogger.warning("Failed to create userdata directory.");
}
gson = new GsonBuilder()
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer())
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer())
.setPrettyPrinting()
.create();
}
@Override
public LimboPlayer getLimboPlayer(Player player) {
String id = PlayerUtils.getUUIDorName(player);
File file = new File(cacheDir, id + File.separator + "data.json");
if (!file.exists()) {
return null;
}
try {
String str = Files.toString(file, StandardCharsets.UTF_8);
return gson.fromJson(str, LimboPlayer.class);
} catch (IOException e) {
ConsoleLogger.logException("Could not read player data on disk for '" + player.getName() + "'", e);
return null;
}
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limboPlayer) {
String id = PlayerUtils.getUUIDorName(player);
try {
File file = new File(cacheDir, id + File.separator + "data.json");
Files.createParentDirs(file);
Files.touch(file);
Files.write(gson.toJson(limboPlayer), file, StandardCharsets.UTF_8);
} catch (IOException e) {
ConsoleLogger.logException("Failed to write " + player.getName() + " data:", e);
}
}
/**
* Removes the LimboPlayer. This will delete the
* "playerdata/&lt;uuid or name&gt;/" folder from disk.
*
* @param player player to remove
*/
@Override
public void removeLimboPlayer(Player player) {
String id = PlayerUtils.getUUIDorName(player);
File file = new File(cacheDir, id);
if (file.exists()) {
FileUtils.purgeDirectory(file);
FileUtils.delete(file);
}
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.INDIVIDUAL_FILES;
}
private final class LimboPlayerDeserializer implements JsonDeserializer<LimboPlayer> {
@Override
public LimboPlayer deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext context) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
if (jsonObject == null) {
return null;
}
Location loc = null;
String group = "";
boolean operator = false;
boolean canFly = false;
float walkSpeed = LimboPlayer.DEFAULT_WALK_SPEED;
float flySpeed = LimboPlayer.DEFAULT_FLY_SPEED;
JsonElement e;
if ((e = jsonObject.getAsJsonObject("location")) != null) {
JsonObject obj = e.getAsJsonObject();
World world = bukkitService.getWorld(obj.get("world").getAsString());
if (world != null) {
double x = obj.get("x").getAsDouble();
double y = obj.get("y").getAsDouble();
double z = obj.get("z").getAsDouble();
float yaw = obj.get("yaw").getAsFloat();
float pitch = obj.get("pitch").getAsFloat();
loc = new Location(world, x, y, z, yaw, pitch);
}
}
if ((e = jsonObject.get("group")) != null) {
group = e.getAsString();
}
if ((e = jsonObject.get("operator")) != null) {
operator = e.getAsBoolean();
}
if ((e = jsonObject.get("can-fly")) != null) {
canFly = e.getAsBoolean();
}
if ((e = jsonObject.get("walk-speed")) != null) {
walkSpeed = e.getAsFloat();
}
if ((e = jsonObject.get("fly-speed")) != null) {
flySpeed = e.getAsFloat();
}
return new LimboPlayer(loc, operator, group, canFly, walkSpeed, flySpeed);
}
}
private static final class LimboPlayerSerializer implements JsonSerializer<LimboPlayer> {
@Override
public JsonElement serialize(LimboPlayer limboPlayer, Type type,
JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("group", limboPlayer.getGroup());
Location loc = limboPlayer.getLocation();
JsonObject obj2 = new JsonObject();
obj2.addProperty("world", loc.getWorld().getName());
obj2.addProperty("x", loc.getX());
obj2.addProperty("y", loc.getY());
obj2.addProperty("z", loc.getZ());
obj2.addProperty("yaw", loc.getYaw());
obj2.addProperty("pitch", loc.getPitch());
obj.add("location", obj2);
obj.addProperty("operator", limboPlayer.isOperator());
obj.addProperty("can-fly", limboPlayer.isCanFly());
obj.addProperty("walk-speed", limboPlayer.getWalkSpeed());
obj.addProperty("fly-speed", limboPlayer.getFlySpeed());
return obj;
}
}
}