diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/DataManager.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/DataManager.java index c98a15b..87a4d2e 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/DataManager.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/DataManager.java @@ -26,8 +26,6 @@ */ package com.imaginarycode.minecraft.redisbungee; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; import com.google.common.net.InetAddresses; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -50,6 +48,7 @@ import java.net.InetAddress; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; /** @@ -57,18 +56,34 @@ import java.util.logging.Level; * * @since 0.3.3 */ -@RequiredArgsConstructor public class DataManager implements Listener { private final RedisBungee plugin; + // TODO: Add cleanup for this. private final InternalCache serverCache = createCache(); - private final InternalCache proxyCache = createCache(); - private final InternalCache ipCache = createCache(); - private final InternalCache lastOnlineCache = createCache(); + private final InternalCache proxyCache = createCache(TimeUnit.MINUTES.toMillis(60)); + private final InternalCache ipCache = createCache(TimeUnit.MINUTES.toMillis(60)); + private final InternalCache lastOnlineCache = createCache(TimeUnit.MINUTES.toMillis(60)); + + public DataManager(RedisBungee plugin) { + this.plugin = plugin; + plugin.getProxy().getScheduler().schedule(plugin, new Runnable() { + @Override + public void run() { + proxyCache.cleanup(); + ipCache.cleanup(); + lastOnlineCache.cleanup(); + } + }, 1, 1, TimeUnit.MINUTES); + } public static InternalCache createCache() { return new InternalCache<>(); } + public static InternalCache createCache(long entryWriteExpiry) { + return new InternalCache<>(entryWriteExpiry); + } + private final JsonParser parser = new JsonParser(); public String getServer(final UUID uuid) { diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java index e7b75f8..7f3ebce 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java @@ -26,10 +26,12 @@ */ package com.imaginarycode.minecraft.redisbungee; +import com.google.common.annotations.VisibleForTesting; import redis.clients.jedis.Jedis; import redis.clients.jedis.Pipeline; -class RedisUtil { +@VisibleForTesting +public class RedisUtil { // Compatibility restraints prevent me from using using HDEL with multiple keys. public static void cleanUpPlayer(String player, Jedis rsc) { rsc.srem("proxy:" + RedisBungee.getApi().getServerId() + ":usersOnline", player); diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/util/InternalCache.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/InternalCache.java index 3a7efcd..f2a63cb 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/util/InternalCache.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/InternalCache.java @@ -26,39 +26,78 @@ */ package com.imaginarycode.minecraft.redisbungee.util; +import lombok.Data; + +import java.util.Iterator; +import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; -// I would use the Guava, but can't because I need a few more properties. +// I would use the Guava cache, but can't because I need a few more properties. public class InternalCache { - private final ConcurrentMap map = new ConcurrentHashMap<>(128, 0.75f, 4); + private final ConcurrentMap map = new ConcurrentHashMap<>(128, 0.75f, 4); + private final long entryWriteExpiry; + + public InternalCache() { + this.entryWriteExpiry = 0; + } + + public InternalCache(long entryWriteExpiry) { + this.entryWriteExpiry = entryWriteExpiry; + } public V get(K key, Callable loader) throws ExecutionException { - V value = map.get(key); + Holder value = map.get(key); + + if (value == null || (entryWriteExpiry > 0 && System.currentTimeMillis() > value.expiry)) { + V freshValue; - if (value == null) { try { - value = loader.call(); + freshValue = loader.call(); } catch (Exception e) { throw new ExecutionException(e); } - if (value == null) + if (freshValue == null) return null; - map.putIfAbsent(key, value); + map.putIfAbsent(key, value = new Holder(freshValue, System.currentTimeMillis() + entryWriteExpiry)); } - return value; + return value.value; } public V put(K key, V value) { - return map.put(key, value); + Holder holder = map.put(key, new Holder(value, System.currentTimeMillis() + entryWriteExpiry)); + + if (holder == null) + return null; + + return holder.value; } public void invalidate(K key) { map.remove(key); } + + // Run periodically to clean up the cache mappings. + public void cleanup() { + if (entryWriteExpiry <= 0) + return; + + long fixedReference = System.currentTimeMillis(); + for (Iterator> it = map.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if (entry.getValue().expiry > fixedReference) + it.remove(); + } + } + + @Data + private class Holder { + private final V value; + private final long expiry; + } } diff --git a/src/test/java/com/imaginarycode/minecraft/redisbungee/test/InternalCacheTest.java b/src/test/java/com/imaginarycode/minecraft/redisbungee/test/InternalCacheTest.java new file mode 100644 index 0000000..5cb57b1 --- /dev/null +++ b/src/test/java/com/imaginarycode/minecraft/redisbungee/test/InternalCacheTest.java @@ -0,0 +1,141 @@ +/** + * This is free and unencumbered software released into the public domain. + * + * Anyone is free to copy, modify, publish, use, compile, sell, or + * distribute this software, either in source code form or as a compiled + * binary, for any purpose, commercial or non-commercial, and by any + * means. + * + * In jurisdictions that recognize copyright laws, the author or authors + * of this software dedicate any and all copyright interest in the + * software to the public domain. We make this dedication for the benefit + * of the public at large and to the detriment of our heirs and + * successors. We intend this dedication to be an overt act of + * relinquishment in perpetuity of all present and future rights to this + * software under copyright law. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * For more information, please refer to + */ +package com.imaginarycode.minecraft.redisbungee.test; + +import com.imaginarycode.minecraft.redisbungee.util.InternalCache; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +public class InternalCacheTest { + @Test + public void testNonCached() { + InternalCache cache = new InternalCache<>(); + try { + Assert.assertEquals("hi", cache.get("hi", new Callable() { + @Override + public String call() throws Exception { + return "hi"; + } + })); + } catch (ExecutionException e) { + throw new AssertionError(e); + } + } + + @Test + public void testCached() { + InternalCache cache = new InternalCache<>(); + try { + Assert.assertEquals("hi", cache.get("hi", new Callable() { + @Override + public String call() throws Exception { + return "hi"; + } + })); + Assert.assertEquals("hi", cache.get("hi", new Callable() { + @Override + public String call() throws Exception { + Assert.fail("Cache is using loader!"); + return null; + } + })); + } catch (ExecutionException e) { + throw new AssertionError(e); + } + } + + @Test + public void testWriteExpiry() { + final Object one = new Object(); + InternalCache cache = new InternalCache<>(100); // not very long + try { + // Successive calls should always work. + Assert.assertEquals(one, cache.get("hi", new Callable() { + @Override + public Object call() throws Exception { + return one; + } + })); + Assert.assertEquals(one, cache.get("hi", new Callable() { + @Override + public Object call() throws Exception { + Assert.fail("Cache is using loader!"); + return null; + } + })); + + // But try again in a second and a bit: + try { + Thread.sleep(150); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + + final Object two = new Object(); + Assert.assertEquals(two, cache.get("hi", new Callable() { + @Override + public Object call() throws Exception { + return two; + } + })); + } catch (ExecutionException e) { + throw new AssertionError(e); + } + } + + @Test + public void testCleanup() { + InternalCache cache = new InternalCache<>(10); + final Object one = new Object(); + final Object two = new Object(); + try { + Assert.assertEquals(one, cache.get("hi", new Callable() { + @Override + public Object call() throws Exception { + return one; + } + })); + try { + Thread.sleep(20); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + cache.cleanup(); + Assert.assertEquals(two, cache.get("hi", new Callable() { + @Override + public Object call() throws Exception { + return two; + } + })); + } catch (ExecutionException e) { + throw new AssertionError(e); + } + } +} diff --git a/src/test/java/com/imaginarycode/minecraft/redisbungee/test/RedisUtilTest.java b/src/test/java/com/imaginarycode/minecraft/redisbungee/test/RedisUtilTest.java new file mode 100644 index 0000000..31784fb --- /dev/null +++ b/src/test/java/com/imaginarycode/minecraft/redisbungee/test/RedisUtilTest.java @@ -0,0 +1,43 @@ +/** + * This is free and unencumbered software released into the public domain. + * + * Anyone is free to copy, modify, publish, use, compile, sell, or + * distribute this software, either in source code form or as a compiled + * binary, for any purpose, commercial or non-commercial, and by any + * means. + * + * In jurisdictions that recognize copyright laws, the author or authors + * of this software dedicate any and all copyright interest in the + * software to the public domain. We make this dedication for the benefit + * of the public at large and to the detriment of our heirs and + * successors. We intend this dedication to be an overt act of + * relinquishment in perpetuity of all present and future rights to this + * software under copyright law. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * For more information, please refer to + */ +package com.imaginarycode.minecraft.redisbungee.test; + +import com.imaginarycode.minecraft.redisbungee.RedisUtil; +import org.junit.Assert; +import org.junit.Test; + +public class RedisUtilTest { + @Test + public void testRedisLuaCheck() { + Assert.assertTrue(RedisUtil.canUseLua("2.6.0")); + Assert.assertFalse(RedisUtil.canUseLua("2.2.12")); + Assert.assertFalse(RedisUtil.canUseLua("1.2.4")); + Assert.assertTrue(RedisUtil.canUseLua("2.8.4")); + Assert.assertTrue(RedisUtil.canUseLua("3.0.0")); + Assert.assertTrue(RedisUtil.canUseLua("3.2.1")); + } +}