diff --git a/bukkit-legacy/pom.xml b/bukkit-legacy/pom.xml index f2f03178..6bc6e8c9 100644 --- a/bukkit-legacy/pom.xml +++ b/bukkit-legacy/pom.xml @@ -75,6 +75,18 @@ com.google.gson me.lucko.luckperms.lib.gson + + redis.clients.jedis + me.lucko.luckperms.lib.jedis.jedis + + + redis.clients.util + me.lucko.luckperms.lib.jedis.util + + + org.apache.commons.pool2 + me.lucko.luckperms.lib.jedis.pool2 + diff --git a/bukkit/pom.xml b/bukkit/pom.xml index ca19cd05..14a8cbc4 100644 --- a/bukkit/pom.xml +++ b/bukkit/pom.xml @@ -70,6 +70,18 @@ org.h2 me.lucko.luckperms.lib.h2 + + redis.clients.jedis + me.lucko.luckperms.lib.jedis.jedis + + + redis.clients.util + me.lucko.luckperms.lib.jedis.util + + + org.apache.commons.pool2 + me.lucko.luckperms.lib.jedis.pool2 + diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java index 51243fbf..7a08e5bc 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java @@ -45,6 +45,7 @@ import me.lucko.luckperms.common.contexts.ServerCalculator; import me.lucko.luckperms.common.core.UuidCache; import me.lucko.luckperms.common.data.Importer; import me.lucko.luckperms.common.groups.GroupManager; +import me.lucko.luckperms.common.messaging.RedisMessaging; import me.lucko.luckperms.common.runnables.ExpireTemporaryTask; import me.lucko.luckperms.common.runnables.UpdateTask; import me.lucko.luckperms.common.storage.Datastore; @@ -77,6 +78,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { private GroupManager groupManager; private TrackManager trackManager; private Datastore datastore; + private RedisMessaging redisMessaging = null; private UuidCache uuidCache; private ApiProvider apiProvider; private Logger log; @@ -97,6 +99,42 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { getLog().info("Loading configuration..."); configuration = new BukkitConfig(this); + // setup the Bukkit defaults hook + defaultsProvider = new DefaultsProvider(); + // give all plugins a chance to load their defaults, then refresh. + getServer().getScheduler().runTaskLater(this, () -> defaultsProvider.refresh(), 1L); + + // register events + PluginManager pm = getServer().getPluginManager(); + pm.registerEvents(new BukkitListener(this), this); + + // initialise datastore + datastore = StorageFactory.getDatastore(this, "h2"); + + // initialise redis + if (getConfiguration().isRedisEnabled()) { + getLog().info("Loading redis..."); + redisMessaging = new RedisMessaging(this); + try { + redisMessaging.init(getConfiguration().getRedisAddress(), getConfiguration().getRedisPassword()); + getLog().info("Loaded redis successfully..."); + } catch (Exception e) { + getLog().info("Couldn't load redis..."); + e.printStackTrace(); + } + } + + // setup the update task buffer + final LPBukkitPlugin i = this; + updateTaskBuffer = new BufferedRequest(1000L, this::doAsync) { + @Override + protected Void perform() { + getServer().getScheduler().runTaskAsynchronously(i, new UpdateTask(i)); + return null; + } + }; + + // load locale localeManager = new LocaleManager(); File locale = new File(getDataFolder(), "lang.yml"); if (locale.exists()) { @@ -108,13 +146,6 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { } } - defaultsProvider = new DefaultsProvider(); - getServer().getScheduler().runTaskLater(this, () -> defaultsProvider.refresh(), 1L); - - // register events - PluginManager pm = getServer().getPluginManager(); - pm.registerEvents(new BukkitListener(this), this); - // register commands getLog().info("Registering commands..."); BukkitCommand commandManager = new BukkitCommand(this); @@ -123,8 +154,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { main.setTabCompleter(commandManager); main.setAliases(Arrays.asList("perms", "lp", "permissions", "p", "perm")); - datastore = StorageFactory.getDatastore(this, "h2"); - + // load internal managers getLog().info("Loading internal permission managers..."); uuidCache = new UuidCache(getConfiguration().isOnlineMode()); userManager = new UserManager(this); @@ -140,29 +170,11 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { contextManager.registerCalculator(worldCalculator); contextManager.registerCalculator(new ServerCalculator<>(getConfiguration().getServer())); + // handle server operators if (getConfiguration().isAutoOp()) { contextManager.registerListener(new AutoOPListener()); } - final LPBukkitPlugin i = this; - updateTaskBuffer = new BufferedRequest(1000L, this::doAsync) { - @Override - protected Void perform() { - getServer().getScheduler().runTaskAsynchronously(i, new UpdateTask(i)); - return null; - } - }; - - int mins = getConfiguration().getSyncTime(); - if (mins > 0) { - long ticks = mins * 60 * 20; - getServer().getScheduler().runTaskTimerAsynchronously(this, () -> updateTaskBuffer.request(), ticks, ticks); - } - - getServer().getScheduler().runTaskTimer(this, BukkitSenderFactory.get(this), 1L, 1L); - getServer().getScheduler().runTaskTimerAsynchronously(this, new ExpireTemporaryTask(this), 60L, 60L); - getServer().getScheduler().runTaskTimerAsynchronously(this, consecutiveExecutor, 20L, 20L); - // Provide vault support getLog().info("Attempting to hook into Vault..."); try { @@ -178,15 +190,29 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { e.printStackTrace(); } + // register with the LP API getLog().info("Registering API..."); apiProvider = new ApiProvider(this); ApiHandler.registerProvider(apiProvider); getServer().getServicesManager().register(LuckPermsApi.class, apiProvider, this, ServicePriority.Normal); - // Run update task to refresh any online users - getLog().info("Scheduling Update Task to refresh any online users."); - updateTaskBuffer.request(); + // schedule update tasks + int mins = getConfiguration().getSyncTime(); + if (mins > 0) { + long ticks = mins * 60 * 20; + getServer().getScheduler().runTaskTimerAsynchronously(this, () -> updateTaskBuffer.request(), 20L, ticks); + } else { + // Update online users + updateTaskBuffer.request(); + } + + // register tasks + getServer().getScheduler().runTaskTimer(this, BukkitSenderFactory.get(this), 1L, 1L); + getServer().getScheduler().runTaskTimerAsynchronously(this, new ExpireTemporaryTask(this), 60L, 60L); + getServer().getScheduler().runTaskTimerAsynchronously(this, consecutiveExecutor, 20L, 20L); + + // register permissions registerPermissions(getConfiguration().isCommandsAllowOp() ? PermissionDefault.OP : PermissionDefault.FALSE); if (!getConfiguration().isOpsEnabled()) { getServer().getOperators().forEach(o -> o.setOp(false)); @@ -202,6 +228,11 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { getLog().info("Closing datastore..."); datastore.shutdown(); + if (redisMessaging != null) { + getLog().info("Closing redis..."); + redisMessaging.shutdown(); + } + getLog().info("Unregistering API..."); ApiHandler.unregisterProvider(); getServer().getServicesManager().unregisterAll(this); diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index e5a144cb..7d2aedf5 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -155,6 +155,19 @@ data: # e.g. if you're using sqlite or flatfile, this can be set to -1 to save resources. sync-minutes: 3 +# Settings for Redis. +# +# If enabled and configured, LuckPerms will use the Redis PubSub system to inform other +# connected servers of changes. Use the command "/luckperms networksync" to push changes. +# Data is NOT stored on redis. It is only used as a messaging platform. +# +# If you decide to enable this feature, you should set "sync-minutes" to -1, as there is no need for LuckPerms +# to poll the database for changes. +redis: + enabled: false + address: localhost:6379 + password: '' + diff --git a/bungee/pom.xml b/bungee/pom.xml index fdd8d1f2..1ab60cfc 100644 --- a/bungee/pom.xml +++ b/bungee/pom.xml @@ -69,6 +69,18 @@ org.sqlite me.lucko.luckperms.lib.sqlite + + redis.clients.jedis + me.lucko.luckperms.lib.jedis.jedis + + + redis.clients.util + me.lucko.luckperms.lib.jedis.util + + + org.apache.commons.pool2 + me.lucko.luckperms.lib.jedis.pool2 + diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java index e818ec4f..c8be2556 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java @@ -42,6 +42,7 @@ import me.lucko.luckperms.common.contexts.ServerCalculator; import me.lucko.luckperms.common.core.UuidCache; import me.lucko.luckperms.common.data.Importer; import me.lucko.luckperms.common.groups.GroupManager; +import me.lucko.luckperms.common.messaging.RedisMessaging; import me.lucko.luckperms.common.runnables.ExpireTemporaryTask; import me.lucko.luckperms.common.runnables.UpdateTask; import me.lucko.luckperms.common.storage.Datastore; @@ -69,6 +70,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { private GroupManager groupManager; private TrackManager trackManager; private Datastore datastore; + private RedisMessaging redisMessaging = null; private UuidCache uuidCache; private ApiProvider apiProvider; private Logger log; @@ -86,6 +88,36 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { getLog().info("Loading configuration..."); configuration = new BungeeConfig(this); + // register events + getProxy().getPluginManager().registerListener(this, new BungeeListener(this)); + + // initialise datastore + datastore = StorageFactory.getDatastore(this, "h2"); + + // initialise redis + if (getConfiguration().isRedisEnabled()) { + getLog().info("Loading redis..."); + redisMessaging = new RedisMessaging(this); + try { + redisMessaging.init(getConfiguration().getRedisAddress(), getConfiguration().getRedisPassword()); + getLog().info("Loaded redis successfully..."); + } catch (Exception e) { + getLog().info("Couldn't load redis..."); + e.printStackTrace(); + } + } + + // setup the update task buffer + final LPBungeePlugin i = this; + updateTaskBuffer = new BufferedRequest(1000L, this::doAsync) { + @Override + protected Void perform() { + doAsync(new UpdateTask(i)); + return null; + } + }; + + // load locale localeManager = new LocaleManager(); File locale = new File(getDataFolder(), "lang.yml"); if (locale.exists()) { @@ -97,9 +129,6 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { } } - // register events - getProxy().getPluginManager().registerListener(this, new BungeeListener(this)); - // register commands getLog().info("Registering commands..."); CommandManager commandManager = new CommandManager(this); @@ -108,8 +137,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { // disable the default Bungee /perms command so it gets handled by the Bukkit plugin getProxy().getDisabledCommands().add("perms"); - datastore = StorageFactory.getDatastore(this, "h2"); - + // load internal managers getLog().info("Loading internal permission managers..."); uuidCache = new UuidCache(getConfiguration().isOnlineMode()); userManager = new UserManager(this); @@ -125,32 +153,24 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { contextManager.registerCalculator(serverCalculator); contextManager.registerCalculator(new ServerCalculator<>(getConfiguration().getServer())); - final LPBungeePlugin i = this; - updateTaskBuffer = new BufferedRequest(1000L, this::doAsync) { - @Override - protected Void perform() { - doAsync(new UpdateTask(i)); - return null; - } - }; - - int mins = getConfiguration().getSyncTime(); - if (mins > 0) { - getProxy().getScheduler().schedule(this, new UpdateTask(this), mins, mins, TimeUnit.MINUTES); - } - - // 20 times per second (once per "tick") - getProxy().getScheduler().schedule(this, BungeeSenderFactory.get(this), 50L, 50L, TimeUnit.MILLISECONDS); - getProxy().getScheduler().schedule(this, new ExpireTemporaryTask(this), 3L, 3L, TimeUnit.SECONDS); - getProxy().getScheduler().schedule(this, consecutiveExecutor, 1L, 1L, TimeUnit.SECONDS); - + // register with the LP API getLog().info("Registering API..."); apiProvider = new ApiProvider(this); ApiHandler.registerProvider(apiProvider); - // Run update task to refresh any online users - getLog().info("Scheduling Update Task to refresh any online users."); - updateTaskBuffer.request(); + // schedule update tasks + int mins = getConfiguration().getSyncTime(); + if (mins > 0) { + getProxy().getScheduler().schedule(this, new UpdateTask(this), mins, mins, TimeUnit.MINUTES); + } else { + // Update online users + updateTaskBuffer.request(); + } + + // register tasks + getProxy().getScheduler().schedule(this, BungeeSenderFactory.get(this), 50L, 50L, TimeUnit.MILLISECONDS); // 20 times per second (once per "tick") + getProxy().getScheduler().schedule(this, new ExpireTemporaryTask(this), 3L, 3L, TimeUnit.SECONDS); + getProxy().getScheduler().schedule(this, consecutiveExecutor, 1L, 1L, TimeUnit.SECONDS); getLog().info("Successfully loaded."); } @@ -160,6 +180,11 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { getLog().info("Closing datastore..."); datastore.shutdown(); + if (redisMessaging != null) { + getLog().info("Closing redis..."); + redisMessaging.shutdown(); + } + getLog().info("Unregistering API..."); ApiHandler.unregisterProvider(); } diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index 0a71488f..100e36d2 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -113,6 +113,19 @@ data: # e.g. if you're using sqlite or flatfile, this can be set to -1 to save resources. sync-minutes: 3 +# Settings for Redis. +# +# If enabled and configured, LuckPerms will use the Redis PubSub system to inform other +# connected servers of changes. Use the command "/luckpermsbungee networksync" to push changes. +# Data is NOT stored on redis. It is only used as a messaging platform. +# +# If you decide to enable this feature, you should set "sync-minutes" to -1, as there is no need for LuckPerms +# to poll the database for changes. +redis: + enabled: false + address: localhost:6379 + password: '' + diff --git a/common/pom.xml b/common/pom.xml index 8b52afd5..4a7b41ba 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -39,6 +39,13 @@ 2.5.1 compile + + + redis.clients + jedis + 2.8.1 + compile + org.xerial diff --git a/common/src/main/java/me/lucko/luckperms/common/LuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/LuckPermsPlugin.java index 22a5bdc7..1859e364 100644 --- a/common/src/main/java/me/lucko/luckperms/common/LuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/LuckPermsPlugin.java @@ -35,6 +35,7 @@ import me.lucko.luckperms.common.contexts.ContextManager; import me.lucko.luckperms.common.core.UuidCache; import me.lucko.luckperms.common.data.Importer; import me.lucko.luckperms.common.groups.GroupManager; +import me.lucko.luckperms.common.messaging.RedisMessaging; import me.lucko.luckperms.common.storage.Datastore; import me.lucko.luckperms.common.tracks.TrackManager; import me.lucko.luckperms.common.users.UserManager; @@ -60,6 +61,7 @@ public interface LuckPermsPlugin { TrackManager getTrackManager(); LPConfiguration getConfiguration(); Datastore getDatastore(); + RedisMessaging getRedisMessaging(); Logger getLog(); UuidCache getUuidCache(); ApiProvider getApiProvider(); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/CommandManager.java b/common/src/main/java/me/lucko/luckperms/common/commands/CommandManager.java index b87aa3a0..12a61df9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/CommandManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/CommandManager.java @@ -59,6 +59,7 @@ public class CommandManager { .add(new TrackMainCommand()) .add(new LogMainCommand()) .add(new SyncCommand()) + .add(new NetworkSyncCommand()) .add(new InfoCommand()) .add(new DebugCommand()) .add(new ImportCommand()) diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/NetworkSyncCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/NetworkSyncCommand.java new file mode 100644 index 00000000..580ea9c6 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/NetworkSyncCommand.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016 Lucko (Luck) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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 OR COPYRIGHT HOLDERS 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. + */ + +package me.lucko.luckperms.common.commands.misc; + +import me.lucko.luckperms.common.LuckPermsPlugin; +import me.lucko.luckperms.common.commands.CommandResult; +import me.lucko.luckperms.common.commands.Sender; +import me.lucko.luckperms.common.commands.SingleMainCommand; +import me.lucko.luckperms.common.constants.Message; +import me.lucko.luckperms.common.constants.Permission; + +import java.util.List; + +public class NetworkSyncCommand extends SingleMainCommand { + public NetworkSyncCommand() { + super("NetworkSync", "/%s networksync", 0, Permission.SYNC); + } + + @Override + protected CommandResult execute(LuckPermsPlugin plugin, Sender sender, List args, String label) { + Message.UPDATE_TASK_REQUEST.send(sender); + plugin.getUpdateTaskBuffer().request().getUnchecked(); + Message.UPDATE_TASK_COMPLETE_NETWORK.send(sender); + + if (plugin.getRedisMessaging() != null) { + plugin.getRedisMessaging().pushUpdate(); + Message.UPDATE_TASK_PUSH_SUCCESS.send(sender); + } else { + Message.UPDATE_TASK_PUSH_FAILURE.send(sender); + } + + return CommandResult.SUCCESS; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/config/AbstractConfiguration.java b/common/src/main/java/me/lucko/luckperms/common/config/AbstractConfiguration.java index e37434b7..e05f3eda 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/AbstractConfiguration.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/AbstractConfiguration.java @@ -74,6 +74,9 @@ public abstract class AbstractConfiguration implement private String storageMethod; private boolean splitStorage; private Map splitStorageOptions; + private boolean redisEnabled; + private String redisAddress; + private String redisPassword; public AbstractConfiguration(T plugin, String defaultServerName, boolean defaultIncludeGlobal, String defaultStorage) { this.plugin = plugin; @@ -142,6 +145,10 @@ public abstract class AbstractConfiguration implement .put("uuid", getString("split-storage.methods.uuid", defaultStorage)) .put("log", getString("split-storage.methods.log", defaultStorage)) .build(); + + redisEnabled = getBoolean("redis.enabled", false); + redisAddress = getString("redis.address", null); + redisPassword = getString("redis.password", ""); if (Patterns.NON_ALPHA_NUMERIC.matcher(getServer()).find()) { plugin.getLog().severe("Server name defined in config.yml contains invalid characters. Server names can " + diff --git a/common/src/main/java/me/lucko/luckperms/common/config/LPConfiguration.java b/common/src/main/java/me/lucko/luckperms/common/config/LPConfiguration.java index cae17528..ffcec226 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/LPConfiguration.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/LPConfiguration.java @@ -92,4 +92,10 @@ public interface LPConfiguration { Map getSplitStorageOptions(); + boolean isRedisEnabled(); + + String getRedisAddress(); + + String getRedisPassword(); + } diff --git a/common/src/main/java/me/lucko/luckperms/common/constants/Message.java b/common/src/main/java/me/lucko/luckperms/common/constants/Message.java index 3f4ed47f..61e1d42f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/constants/Message.java +++ b/common/src/main/java/me/lucko/luckperms/common/constants/Message.java @@ -110,6 +110,9 @@ public enum Message { UPDATE_TASK_REQUEST("&bUpdate task scheduled.", true), UPDATE_TASK_COMPLETE("&aUpdate task finished.", true), + UPDATE_TASK_COMPLETE_NETWORK("&aUpdate task finished. Now attempting to push to other servers.", true), + UPDATE_TASK_PUSH_SUCCESS("&aOther servers were notified successfully.", true), + UPDATE_TASK_PUSH_FAILURE("&cError whilst pushing changes to other servers. Is Redis enabled?", true), INFO( "{PREFIX}&2Running &bLuckPerms v{0}&2 by &bLuck&2." + "\n" + "{PREFIX}&f-> &3Platform: &f{1}" + "\n" + diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/RedisMessaging.java b/common/src/main/java/me/lucko/luckperms/common/messaging/RedisMessaging.java new file mode 100644 index 00000000..69ec818e --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/RedisMessaging.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016 Lucko (Luck) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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 OR COPYRIGHT HOLDERS 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. + */ + +package me.lucko.luckperms.common.messaging; + +import lombok.RequiredArgsConstructor; +import me.lucko.luckperms.common.LuckPermsPlugin; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisPubSub; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Uses Redis to push/receive changes to/from other servers + */ +@RequiredArgsConstructor +public class RedisMessaging { + private static final String CHANNEL = "luckperms"; + + private final LuckPermsPlugin plugin; + private JedisPool jedisPool; + private LPSub sub; + + public void init(String address, String password) { + String host = address.substring(0, address.indexOf(':')); + int port = Integer.parseInt(address.substring(address.indexOf(":") + 1)); + + if (password.equals("")) { + jedisPool = new JedisPool(new JedisPoolConfig(), host, port); + } else { + jedisPool = new JedisPool(new JedisPoolConfig(), host, port, 0, password); + } + + plugin.doAsync(() -> { + sub = new LPSub(plugin); + try (Jedis jedis = jedisPool.getResource()) { + jedis.subscribe(sub, CHANNEL); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + public void shutdown() { + sub.unsubscribe(); + jedisPool.destroy(); + } + + public void pushUpdate() { + plugin.doAsync(() -> { + UUID id = sub.generateId(); + plugin.getLog().info("[Redis Messaging] Sending redis ping with id: " + id.toString()); + try (Jedis jedis = jedisPool.getResource()) { + jedis.publish(CHANNEL, "update:" + id.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + @RequiredArgsConstructor + private static class LPSub extends JedisPubSub { + private final LuckPermsPlugin plugin; + private final Set receivedMsgs = Collections.synchronizedSet(new HashSet<>()); + + public UUID generateId() { + UUID uuid = UUID.randomUUID(); + receivedMsgs.add(uuid); + return uuid; + } + + @Override + public void onMessage(String channel, String msg) { + if (!channel.equals(CHANNEL)) { + return; + } + + if (!msg.startsWith("update:")) { + return; + } + + String requestId = msg.substring("update:".length()); + UUID uuid; + try { + uuid = UUID.fromString(requestId); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + if (!receivedMsgs.add(uuid)) { + return; + } + + plugin.getLog().info("[Redis Messaging] Received update ping with id: " + uuid.toString()); + plugin.getUpdateTaskBuffer().request(); + } + } + +} diff --git a/default-lang.yml b/default-lang.yml index f34b51c2..2feab4c8 100644 --- a/default-lang.yml +++ b/default-lang.yml @@ -71,6 +71,9 @@ track-empty: "The track cannot be used as it is empty or contains only one group update-task-request: "&bUpdate task scheduled." update-task-complete: "&aUpdate task finished." +update-task-complete-network: "&aUpdate task finished. Now attempting to push to other servers." +update-task-push-success: "&aOther servers were notified successfully." +update-task-push-failure: "&cError whilst pushing changes to other servers. Is Redis enabled?" info: > {PREFIX}&2Running &bLuckPerms v{0}&2 by &bLuck&2.\n {PREFIX}&f-> &3Platform: &f{1}\n diff --git a/sponge/pom.xml b/sponge/pom.xml index 6f13a848..2c083d87 100644 --- a/sponge/pom.xml +++ b/sponge/pom.xml @@ -68,6 +68,18 @@ com.mysql me.lucko.luckperms.lib.mysql + + redis.clients.jedis + me.lucko.luckperms.lib.jedis.jedis + + + redis.clients.util + me.lucko.luckperms.lib.jedis.util + + + org.apache.commons.pool2 + me.lucko.luckperms.lib.jedis.pool2 + diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java index 01d623a3..a708a9db 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java @@ -40,6 +40,7 @@ import me.lucko.luckperms.common.contexts.ServerCalculator; import me.lucko.luckperms.common.core.UuidCache; import me.lucko.luckperms.common.data.Importer; import me.lucko.luckperms.common.groups.GroupManager; +import me.lucko.luckperms.common.messaging.RedisMessaging; import me.lucko.luckperms.common.runnables.ExpireTemporaryTask; import me.lucko.luckperms.common.runnables.UpdateTask; import me.lucko.luckperms.common.storage.Datastore; @@ -107,6 +108,7 @@ public class LPSpongePlugin implements LuckPermsPlugin { private GroupManager groupManager; private TrackManager trackManager; private Datastore datastore; + private RedisMessaging redisMessaging = null; private UuidCache uuidCache; private ApiProvider apiProvider; private me.lucko.luckperms.api.Logger log; @@ -125,6 +127,37 @@ public class LPSpongePlugin implements LuckPermsPlugin { getLog().info("Loading configuration..."); configuration = new SpongeConfig(this); + // register events + Sponge.getEventManager().registerListeners(this, new SpongeListener(this)); + + // initialise datastore + datastore = StorageFactory.getDatastore(this, "h2"); + + // initialise redis + if (getConfiguration().isRedisEnabled()) { + getLog().info("Loading redis..."); + redisMessaging = new RedisMessaging(this); + try { + redisMessaging.init(getConfiguration().getRedisAddress(), getConfiguration().getRedisPassword()); + getLog().info("Loaded redis successfully..."); + } catch (Exception e) { + getLog().info("Couldn't load redis..."); + e.printStackTrace(); + redisMessaging = null; + } + } + + // setup the update task buffer + final LPSpongePlugin i = this; + updateTaskBuffer = new BufferedRequest(1000L, this::doAsync) { + @Override + protected Void perform() { + scheduler.createTaskBuilder().async().execute(new UpdateTask(i)).submit(i); + return null; + } + }; + + // load locale localeManager = new LocaleManager(); File locale = new File(getMainDir(), "lang.yml"); if (locale.exists()) { @@ -136,17 +169,13 @@ public class LPSpongePlugin implements LuckPermsPlugin { } } - // register events - Sponge.getEventManager().registerListeners(this, new SpongeListener(this)); - // register commands getLog().info("Registering commands..."); CommandManager cmdService = Sponge.getCommandManager(); SpongeCommand commandManager = new SpongeCommand(this); cmdService.register(this, commandManager, "luckperms", "perms", "lp", "permissions", "p", "perm"); - datastore = StorageFactory.getDatastore(this, "h2"); - + // load internal managers getLog().info("Loading internal permission managers..."); uuidCache = new UuidCache(getConfiguration().isOnlineMode()); userManager = new UserManager(this); @@ -160,23 +189,17 @@ public class LPSpongePlugin implements LuckPermsPlugin { contextManager.registerCalculator(new ServerCalculator<>(getConfiguration().getServer())); contextManager.registerCalculator(new WorldCalculator(this)); + // register the PermissionService with Sponge getLog().info("Registering PermissionService..."); Sponge.getServiceManager().setProvider(this, PermissionService.class, (service = new LuckPermsService(this))); + // register with the LP API getLog().info("Registering API..."); apiProvider = new ApiProvider(this); ApiHandler.registerProvider(apiProvider); Sponge.getServiceManager().setProvider(this, LuckPermsApi.class, apiProvider); - final LPSpongePlugin i = this; - updateTaskBuffer = new BufferedRequest(1000L, this::doAsync) { - @Override - protected Void perform() { - scheduler.createTaskBuilder().async().execute(new UpdateTask(i)).submit(i); - return null; - } - }; - + // schedule update tasks int mins = getConfiguration().getSyncTime(); if (mins > 0) { scheduler.createTaskBuilder().async().interval(mins, TimeUnit.MINUTES).execute(new UpdateTask(this)) @@ -186,6 +209,7 @@ public class LPSpongePlugin implements LuckPermsPlugin { updateTaskBuffer.request(); } + // register tasks scheduler.createTaskBuilder().intervalTicks(1L).execute(SpongeSenderFactory.get(this)).submit(this); scheduler.createTaskBuilder().async().intervalTicks(60L).execute(new ExpireTemporaryTask(this)).submit(this); scheduler.createTaskBuilder().async().intervalTicks(20L).execute(consecutiveExecutor).submit(this); @@ -198,6 +222,11 @@ public class LPSpongePlugin implements LuckPermsPlugin { getLog().info("Closing datastore..."); datastore.shutdown(); + if (redisMessaging != null) { + getLog().info("Closing redis..."); + redisMessaging.shutdown(); + } + getLog().info("Unregistering API..."); ApiHandler.unregisterProvider(); } diff --git a/sponge/src/main/resources/luckperms.conf b/sponge/src/main/resources/luckperms.conf index 4cb0eb96..6ff916b2 100644 --- a/sponge/src/main/resources/luckperms.conf +++ b/sponge/src/main/resources/luckperms.conf @@ -114,6 +114,20 @@ data { sync-minutes=3 } +# Settings for Redis. +# +# If enabled and configured, LuckPerms will use the Redis PubSub system to inform other +# connected servers of changes. Use the command "/luckperms networksync" to push changes. +# Data is NOT stored on redis. It is only used as a messaging platform. +# +# If you decide to enable this feature, you should set "sync-minutes" to -1, as there is no need for LuckPerms +# to poll the database for changes. +redis { + enabled=false + address="localhost:6379" + password="" +} +