diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/DataManager.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/DataManager.java index 7342000..c98a15b 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/DataManager.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/DataManager.java @@ -26,6 +26,8 @@ */ 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; @@ -34,6 +36,7 @@ import com.imaginarycode.minecraft.redisbungee.events.PlayerChangedServerNetwork import com.imaginarycode.minecraft.redisbungee.events.PlayerJoinedNetworkEvent; import com.imaginarycode.minecraft.redisbungee.events.PlayerLeftNetworkEvent; import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.util.InternalCache; import lombok.Getter; import lombok.RequiredArgsConstructor; import net.md_5.bungee.api.connection.ProxiedPlayer; @@ -42,12 +45,11 @@ import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; import redis.clients.jedis.Jedis; -import redis.clients.jedis.exceptions.JedisConnectionException; import java.net.InetAddress; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; /** @@ -58,149 +60,108 @@ import java.util.logging.Level; @RequiredArgsConstructor public class DataManager implements Listener { private final RedisBungee plugin; - private final ConcurrentMap serverCache = new ConcurrentHashMap<>(192, 0.65f, 4); - private final ConcurrentMap proxyCache = new ConcurrentHashMap<>(192, 0.65f, 4); - private final ConcurrentMap ipCache = new ConcurrentHashMap<>(192, 0.65f, 4); - private final ConcurrentMap lastOnlineCache = new ConcurrentHashMap<>(192, 0.65f, 4); + private final InternalCache serverCache = createCache(); + private final InternalCache proxyCache = createCache(); + private final InternalCache ipCache = createCache(); + private final InternalCache lastOnlineCache = createCache(); + + public static InternalCache createCache() { + return new InternalCache<>(); + } private final JsonParser parser = new JsonParser(); - public String getServer(UUID uuid) { + public String getServer(final UUID uuid) { ProxiedPlayer player = plugin.getProxy().getPlayer(uuid); if (player != null) return player.getServer() != null ? player.getServer().getInfo().getName() : null; - String server = serverCache.get(uuid); - - if (server != null) - return server; - - try (Jedis tmpRsc = plugin.getPool().getResource()) { - server = tmpRsc.hget("player:" + uuid, "server"); - - if (server == null) - return null; - - serverCache.put(uuid, server); - return server; - } catch (JedisConnectionException e) { - // Redis server has disappeared! - plugin.getLogger().log(Level.SEVERE, "Unable to get connection from pool - did your Redis server go away?", e); + try { + return serverCache.get(uuid, new Callable() { + @Override + public String call() throws Exception { + try (Jedis tmpRsc = plugin.getPool().getResource()) { + return tmpRsc.hget("player:" + uuid, "server"); + } + } + }); + } catch (ExecutionException e) { + plugin.getLogger().log(Level.SEVERE, "Unable to get server", e); throw new RuntimeException("Unable to get server for " + uuid, e); } } - public String getProxy(UUID uuid) { + public String getProxy(final UUID uuid) { ProxiedPlayer player = plugin.getProxy().getPlayer(uuid); if (player != null) return RedisBungee.getConfiguration().getServerId(); - String server = proxyCache.get(uuid); - - if (server != null) - return server; - - try (Jedis tmpRsc = plugin.getPool().getResource()) { - server = tmpRsc.hget("player:" + uuid, "proxy"); - - if (server == null) - return null; - - proxyCache.put(uuid, server); - return server; - } catch (JedisConnectionException e) { - // Redis server has disappeared! - plugin.getLogger().log(Level.SEVERE, "Unable to get connection from pool - did your Redis server go away?", e); - throw new RuntimeException("Unable to get server for " + uuid, e); + try { + return proxyCache.get(uuid, new Callable() { + @Override + public String call() throws Exception { + try (Jedis tmpRsc = plugin.getPool().getResource()) { + return tmpRsc.hget("player:" + uuid, "proxy"); + } + } + }); + } catch (ExecutionException e) { + plugin.getLogger().log(Level.SEVERE, "Unable to get proxy", e); + throw new RuntimeException("Unable to get proxy for " + uuid, e); } } - public InetAddress getIp(UUID uuid) { + public InetAddress getIp(final UUID uuid) { ProxiedPlayer player = plugin.getProxy().getPlayer(uuid); if (player != null) return player.getAddress().getAddress(); - InetAddress address = ipCache.get(uuid); - - if (address != null) - return address; - - try (Jedis tmpRsc = plugin.getPool().getResource()) { - String result = tmpRsc.hget("player:" + uuid, "ip"); - if (result != null) { - address = InetAddresses.forString(result); - ipCache.put(uuid, address); - return address; - } - return null; - } catch (JedisConnectionException e) { - // Redis server has disappeared! - plugin.getLogger().log(Level.SEVERE, "Unable to get connection from pool - did your Redis server go away?", e); - throw new RuntimeException("Unable to get server for " + uuid, e); + try { + return ipCache.get(uuid, new Callable() { + @Override + public InetAddress call() throws Exception { + try (Jedis tmpRsc = plugin.getPool().getResource()) { + String result = tmpRsc.hget("player:" + uuid, "ip"); + return result == null ? null : InetAddresses.forString(result); + } + } + }); + } catch (ExecutionException e) { + plugin.getLogger().log(Level.SEVERE, "Unable to get IP", e); + throw new RuntimeException("Unable to get IP for " + uuid, e); } } - public long getLastOnline(UUID uuid) { + public long getLastOnline(final UUID uuid) { ProxiedPlayer player = plugin.getProxy().getPlayer(uuid); if (player != null) return 0; - Long time = lastOnlineCache.get(uuid); - - if (time != null) - return time; - - try (Jedis tmpRsc = plugin.getPool().getResource()) { - String result = tmpRsc.hget("player:" + uuid, "online"); - if (result != null) - try { - time = Long.valueOf(result); - - if (time == null) - return -1; - - lastOnlineCache.put(uuid, time); - return time; - } catch (NumberFormatException e) { - plugin.getLogger().info("I found a funny number for when " + uuid + " was last online!"); - boolean found = false; - for (String proxyId : plugin.getServerIds()) { - if (proxyId.equals(RedisBungee.getConfiguration().getServerId())) continue; - if (tmpRsc.sismember("proxy:" + proxyId + ":usersOnline", uuid.toString())) { - found = true; - break; - } + try { + return lastOnlineCache.get(uuid, new Callable() { + @Override + public Long call() throws Exception { + try (Jedis tmpRsc = plugin.getPool().getResource()) { + String result = tmpRsc.hget("player:" + uuid, "online"); + return result == null ? -1 : Long.valueOf(result); } - - long value = 0; - - if (!found) { - value = System.currentTimeMillis(); - plugin.getLogger().info(uuid + " isn't online. Setting to current time."); - } else { - plugin.getLogger().info(uuid + " is online. Setting to 0. Please check your BungeeCord instances."); - plugin.getLogger().info("If they are working properly, and this error does not resolve in a few minutes, please let Tux know!"); - } - tmpRsc.hset("player:" + uuid, "online", Long.toString(value)); - return value; } - return (long) -1; - } catch (JedisConnectionException e) { - // Redis server has disappeared! - plugin.getLogger().log(Level.SEVERE, "Unable to get connection from pool - did your Redis server go away?", e); - throw new RuntimeException("Unable to get server for " + uuid, e); + }); + } catch (ExecutionException e) { + plugin.getLogger().log(Level.SEVERE, "Unable to get last time online", e); + throw new RuntimeException("Unable to get last time online for " + uuid, e); } } private void invalidate(UUID uuid) { - ipCache.remove(uuid); - lastOnlineCache.remove(uuid); - serverCache.remove(uuid); - proxyCache.remove(uuid); + ipCache.invalidate(uuid); + lastOnlineCache.invalidate(uuid); + serverCache.invalidate(uuid); + proxyCache.invalidate(uuid); } @EventHandler diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/IOUtil.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/IOUtil.java new file mode 100644 index 0000000..121958e --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/IOUtil.java @@ -0,0 +1,45 @@ +/** + * 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; + +import com.google.common.io.ByteStreams; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class IOUtil { + public static String readInputStreamAsString(InputStream is) { + String string; + try { + string = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new AssertionError(e); + } + return string; + } +} diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java index ede2d61..90794a3 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java @@ -30,7 +30,9 @@ import com.google.common.base.Functions; import com.google.common.collect.*; import com.google.common.io.ByteStreams; import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.util.LuaManager; import com.imaginarycode.minecraft.redisbungee.util.NameFetcher; import com.imaginarycode.minecraft.redisbungee.util.UUIDFetcher; import com.imaginarycode.minecraft.redisbungee.util.UUIDTranslator; @@ -53,6 +55,7 @@ import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.exceptions.JedisException; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; @@ -87,7 +90,8 @@ public final class RedisBungee extends Plugin { private AtomicInteger nagAboutServers = new AtomicInteger(); private ScheduledTask integrityCheck; private ScheduledTask heartbeatTask; - + private boolean usingLua; + private LuaManager.Script serverToPlayersScript; /** * Fetch the {@link RedisBungeeAPI} object created on plugin start. @@ -133,13 +137,26 @@ public final class RedisBungee extends Plugin { } final Multimap serversToPlayers() { - ImmutableMultimap.Builder multimapBuilder = ImmutableMultimap.builder(); - for (UUID p : getPlayers()) { - String name = dataManager.getServer(p); - if (name != null) - multimapBuilder.put(name, p); + if (usingLua) { + String string = (String) serverToPlayersScript.eval(ImmutableList.of(), getServerIds()); + Map> deserialized = gson.fromJson(string, new TypeToken>>() {}.getType()); + + ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + + for (Map.Entry> entry : deserialized.entrySet()) { + builder.putAll(entry.getKey(), entry.getValue()); + } + + return builder.build(); + } else { + ImmutableMultimap.Builder multimapBuilder = ImmutableMultimap.builder(); + for (UUID p : getPlayers()) { + String name = dataManager.getServer(p); + if (name != null) + multimapBuilder.put(name, p); + } + return multimapBuilder.build(); } - return multimapBuilder.build(); } final int getCount() { @@ -229,6 +246,22 @@ public final class RedisBungee extends Plugin { if (pool != null) { try (Jedis tmpRsc = pool.getResource()) { tmpRsc.hset("heartbeats", configuration.getServerId(), String.valueOf(System.currentTimeMillis())); + // This is more portable than INFO
+ String info = tmpRsc.info(); + for (String s : info.split("\r\n")) { + if (s.startsWith("redis_version:")) { + String version = s.split(":")[1]; + if (!(usingLua = RedisUtil.canUseLua(version))) { + getLogger().warning("Your version of Redis (" + version + ") is below 2.6. RedisBungee will disable optimizations using Lua."); + getLogger().warning("Support for versions of Redis below version 2.6 will be removed in the future."); + } else { + getLogger().info("Using Redis >= 2.6, enabling Lua optimizations."); + LuaManager manager = new LuaManager(this); + serverToPlayersScript = manager.createScript(IOUtil.readInputStreamAsString(getResourceAsStream("lua/server_to_players.lua"))); + } + break; + } + } } serverIds = getCurrentServerIds(); uuidTranslator = new UUIDTranslator(this); diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java index 6cbd0eb..76527ba 100644 --- a/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisUtil.java @@ -35,4 +35,18 @@ class RedisUtil { rsc.hdel("player:" + player, "ip"); rsc.hdel("player:" + player, "proxy"); } + + public static boolean canUseLua(String redisVersion) { + // Need to use >=2.6 to use Lua optimizations. + String[] args = redisVersion.split("\\."); + + if (args.length < 2) { + return false; + } + + int major = Integer.parseInt(args[0]); + int minor = Integer.parseInt(args[1]); + + return major >= 3 || (major == 2 && minor >= 6); + } } diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/util/InternalCache.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/InternalCache.java new file mode 100644 index 0000000..3a7efcd --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/InternalCache.java @@ -0,0 +1,64 @@ +/** + * 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.util; + +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. +public class InternalCache { + private final ConcurrentMap map = new ConcurrentHashMap<>(128, 0.75f, 4); + + public V get(K key, Callable loader) throws ExecutionException { + V value = map.get(key); + + if (value == null) { + try { + value = loader.call(); + } catch (Exception e) { + throw new ExecutionException(e); + } + + if (value == null) + return null; + + map.putIfAbsent(key, value); + } + + return value; + } + + public V put(K key, V value) { + return map.put(key, value); + } + + public void invalidate(K key) { + map.remove(key); + } +} diff --git a/src/main/java/com/imaginarycode/minecraft/redisbungee/util/LuaManager.java b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/LuaManager.java new file mode 100644 index 0000000..b261e8e --- /dev/null +++ b/src/main/java/com/imaginarycode/minecraft/redisbungee/util/LuaManager.java @@ -0,0 +1,70 @@ +/** + * 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.util; + +import com.imaginarycode.minecraft.redisbungee.RedisBungee; +import lombok.RequiredArgsConstructor; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisDataException; + +import java.util.List; + +@RequiredArgsConstructor +public class LuaManager { + private final RedisBungee plugin; + + public Script createScript(String script) { + try (Jedis jedis = plugin.getPool().getResource()) { + String hash = jedis.scriptLoad(script); + return new Script(script, hash); + } + } + + @RequiredArgsConstructor + public class Script { + private final String script; + private final String hashed; + + public Object eval(List keys, List args) { + Object data; + + try (Jedis jedis = plugin.getPool().getResource()) { + try { + data = jedis.evalsha(hashed, keys, args); + } catch (JedisDataException e) { + if (e.getMessage().startsWith("NOSCRIPT")) { + data = jedis.eval(script, keys, args); + } else { + throw e; + } + } + } + + return data; + } + } +} diff --git a/src/main/resources/lua/server_to_players.lua b/src/main/resources/lua/server_to_players.lua new file mode 100644 index 0000000..a3fdd97 --- /dev/null +++ b/src/main/resources/lua/server_to_players.lua @@ -0,0 +1,20 @@ +-- This script needs all active proxies available specified as args. +local serverToData = {} + +for _, proxy in ipairs(ARGV) do + local players = redis.call("SMEMBERS", "proxy:" .. proxy .. ":usersOnline") + for _, player in ipairs(players) do + local server = redis.call("HGET", "player:" .. player, "server") + if server then + if serverToData[server] then + local data = serverToData[server] + data[#data + 1] = player + else + serverToData[server] = {player} + end + end + end +end + +-- Redis can't map a Lua table back, so we have to send it as JSON. +return cjson.encode(serverToData) \ No newline at end of file