#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
@@ -48,14 +48,13 @@ public class LimboServiceHelperTest {
// given
Location newLocation = mock(Location.class);
LimboPlayer newLimbo = new LimboPlayer(newLocation, false, "grp-new", true, 0.3f, 0.0f);
Location oldLocation = mock(Location.class);
LimboPlayer oldLimbo = new LimboPlayer(oldLocation, false, "", false, 0.1f, 0.1f);
LimboPlayer oldLimbo = new LimboPlayer(null, false, "", false, 0.1f, 0.1f);
// when
LimboPlayer result = limboServiceHelper.merge(newLimbo, oldLimbo);
// then
assertThat(result.getLocation(), equalTo(oldLocation));
assertThat(result.getLocation(), equalTo(newLocation));
assertThat(result.isOperator(), equalTo(false));
assertThat(result.getGroup(), equalTo("grp-new"));
assertThat(result.isCanFly(), equalTo(true));
@@ -4,6 +4,7 @@ import ch.jalu.injector.testing.DelayedInjectionRunner;
import ch.jalu.injector.testing.InjectDelayed;
import fr.xephi.authme.ReflectionTestUtils;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
@@ -57,6 +58,9 @@ public class LimboServiceTest {
@Mock
private LimboPlayerTaskManager taskManager;
@Mock
private LimboPersistence limboPersistence;
@BeforeClass
public static void initLogger() {
TestHelper.setupLogger();
@@ -0,0 +1,175 @@
package fr.xephi.authme.data.limbo.persistence;
import ch.jalu.injector.testing.BeforeInjecting;
import ch.jalu.injector.testing.DelayedInjectionRunner;
import ch.jalu.injector.testing.InjectDelayed;
import fr.xephi.authme.ReflectionTestUtils;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.data.limbo.LimboPlayer;
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 org.hamcrest.Matcher;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import java.util.logging.Logger;
import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.hamcrest.MockitoHamcrest.argThat;
/**
* Test for {@link LimboPersistence}.
*/
@RunWith(DelayedInjectionRunner.class)
public class LimboPersistenceTest {
@InjectDelayed
private LimboPersistence limboPersistence;
@Mock
private Factory<LimboPersistenceHandler> handlerFactory;
@Mock
private Settings settings;
@BeforeClass
public static void setUpLogger() {
TestHelper.setupLogger();
}
@BeforeInjecting
@SuppressWarnings("unchecked")
public void setUpMocks() {
given(settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE)).willReturn(LimboPersistenceType.DISABLED);
given(handlerFactory.newInstance(any(Class.class)))
.willAnswer(invocation -> mock(invocation.getArgument(0)));
}
@Test
public void shouldInitializeProperly() {
// given / when / then
assertThat(getHandler(), instanceOf(NoOpPersistenceHandler.class));
}
@Test
public void shouldDelegateToHandler() {
// given
Player player = mock(Player.class);
LimboPersistenceHandler handler = getHandler();
LimboPlayer limbo = mock(LimboPlayer.class);
given(handler.getLimboPlayer(player)).willReturn(limbo);
// when
LimboPlayer result = limboPersistence.getLimboPlayer(player);
limboPersistence.saveLimboPlayer(player, mock(LimboPlayer.class));
limboPersistence.removeLimboPlayer(mock(Player.class));
// then
assertThat(result, equalTo(limbo));
verify(handler).getLimboPlayer(player);
verify(handler).saveLimboPlayer(eq(player), argThat(notNullAndDifferentFrom(limbo)));
verify(handler).removeLimboPlayer(argThat(notNullAndDifferentFrom(player)));
}
@Test
public void shouldReloadProperly() {
// given
given(settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE))
.willReturn(LimboPersistenceType.INDIVIDUAL_FILES);
// when
limboPersistence.reload(settings);
// then
assertThat(getHandler(), instanceOf(LimboPersistenceType.INDIVIDUAL_FILES.getImplementationClass()));
}
@Test
public void shouldNotReinitializeHandlerForSameType() {
// given
LimboPersistenceHandler currentHandler = getHandler();
Mockito.reset(handlerFactory);
given(currentHandler.getType()).willCallRealMethod();
// when
limboPersistence.reload(settings);
// then
verifyZeroInteractions(handlerFactory);
assertThat(currentHandler, sameInstance(getHandler()));
}
@Test
public void shouldHandleExceptionWhenGettingLimbo() {
// given
Player player = mock(Player.class);
Logger logger = TestHelper.setupLogger();
LimboPersistenceHandler handler = getHandler();
doThrow(IllegalAccessException.class).when(handler).getLimboPlayer(player);
// when
LimboPlayer result = limboPersistence.getLimboPlayer(player);
// then
assertThat(result, nullValue());
verify(logger).warning(argThat(containsString("[IllegalAccessException]")));
}
@Test
public void shouldHandleExceptionWhenSavingLimbo() {
// given
Player player = mock(Player.class);
LimboPlayer limbo = mock(LimboPlayer.class);
Logger logger = TestHelper.setupLogger();
LimboPersistenceHandler handler = getHandler();
doThrow(IllegalStateException.class).when(handler).saveLimboPlayer(player, limbo);
// when
limboPersistence.saveLimboPlayer(player, limbo);
// then
verify(logger).warning(argThat(containsString("[IllegalStateException]")));
}
@Test
public void shouldHandleExceptionWhenRemovingLimbo() {
// given
Player player = mock(Player.class);
Logger logger = TestHelper.setupLogger();
LimboPersistenceHandler handler = getHandler();
doThrow(UnsupportedOperationException.class).when(handler).removeLimboPlayer(player);
// when
limboPersistence.removeLimboPlayer(player);
// then
verify(logger).warning(argThat(containsString("[UnsupportedOperationException]")));
}
private LimboPersistenceHandler getHandler() {
return ReflectionTestUtils.getFieldValue(LimboPersistence.class, limboPersistence, "handler");
}
private static <T> Matcher<T> notNullAndDifferentFrom(T o) {
return both(not(sameInstance(o))).and(not(nullValue()));
}
}
@@ -0,0 +1,127 @@
package fr.xephi.authme.data.limbo.persistence;
import ch.jalu.injector.testing.BeforeInjecting;
import ch.jalu.injector.testing.DelayedInjectionRunner;
import ch.jalu.injector.testing.InjectDelayed;
import fr.xephi.authme.TestHelper;
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 org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.UUID;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Test for {@link SeparateFilePersistenceHandler}.
*/
@RunWith(DelayedInjectionRunner.class)
public class SeparateFilePersistenceHandlerTest {
private static final UUID SAMPLE_UUID = UUID.nameUUIDFromBytes("PersistenceTest".getBytes());
private static final String SOURCE_FOLDER = TestHelper.PROJECT_ROOT + "data/backup/";
@InjectDelayed
private SeparateFilePersistenceHandler handler;
@Mock
private BukkitService bukkitService;
@DataFolder
private File dataFolder;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@BeforeInjecting
public void copyTestFiles() throws IOException {
dataFolder = temporaryFolder.newFolder();
File playerFolder = new File(dataFolder, FileUtils.makePath("playerdata", SAMPLE_UUID.toString()));
if (!playerFolder.mkdirs()) {
throw new IllegalStateException("Cannot create '" + playerFolder.getAbsolutePath() + "'");
}
Files.copy(TestHelper.getJarPath(FileUtils.makePath(SOURCE_FOLDER, "sample-folder", "data.json")),
new File(playerFolder, "data.json").toPath());
}
@Test
public void shouldReadDataFromFile() {
// given
Player player = mock(Player.class);
given(player.getUniqueId()).willReturn(SAMPLE_UUID);
World world = mock(World.class);
given(bukkitService.getWorld("nether")).willReturn(world);
// when
LimboPlayer data = handler.getLimboPlayer(player);
// then
assertThat(data, not(nullValue()));
assertThat(data.isOperator(), equalTo(true));
assertThat(data.isCanFly(), equalTo(true));
assertThat(data.getWalkSpeed(), equalTo(0.2f));
assertThat(data.getFlySpeed(), equalTo(0.1f));
assertThat(data.getGroup(), equalTo("players"));
Location location = data.getLocation();
assertThat(location.getX(), equalTo(-113.219));
assertThat(location.getY(), equalTo(72.0));
assertThat(location.getZ(), equalTo(130.637));
assertThat(location.getWorld(), equalTo(world));
assertThat(location.getPitch(), equalTo(24.15f));
assertThat(location.getYaw(), equalTo(-292.484f));
}
@Test
public void shouldReturnNullForUnavailablePlayer() {
// given
Player player = mock(Player.class);
given(player.getUniqueId()).willReturn(UUID.nameUUIDFromBytes("other-player".getBytes()));
// when
LimboPlayer data = handler.getLimboPlayer(player);
// then
assertThat(data, nullValue());
}
@Test
public void shouldSavePlayerData() {
// given
Player player = mock(Player.class);
UUID uuid = UUID.nameUUIDFromBytes("New player".getBytes());
given(player.getUniqueId()).willReturn(uuid);
World world = mock(World.class);
given(world.getName()).willReturn("player-world");
Location location = new Location(world, 0.2, 102.25, -89.28, 3.02f, 90.13f);
String group = "primary-grp";
LimboPlayer limbo = new LimboPlayer(location, true, group, true, 1.2f, 0.8f);
// when
handler.saveLimboPlayer(player, limbo);
// then
File playerFile = new File(dataFolder, FileUtils.makePath("playerdata", uuid.toString(), "data.json"));
assertThat(playerFile.exists(), equalTo(true));
// TODO ljacqu 20160711: Check contents of file
}
}
@@ -22,7 +22,7 @@ public class AuthMeSettingsRetrieverTest {
// an error margin of 10: this prevents us from having to adjust the test every time the config is changed.
// If this test fails, replace the first argument in closeTo() with the new number of properties
assertThat((double) configurationData.getProperties().size(),
closeTo(150, 10));
closeTo(160, 10));
}
@Test