diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index 155b310a7..cd6a0a5b9 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -262,6 +262,7 @@ broadcast-received-log-entries: true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index 3e0633fff..4d20818a8 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -260,6 +260,7 @@ broadcast-received-log-entries: false # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost diff --git a/common/build.gradle b/common/build.gradle index 5958da14a..d27b3139c 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -74,7 +74,7 @@ dependencies { transitive = false } compileOnly 'com.zaxxer:HikariCP:4.0.3' - compileOnly 'redis.clients:jedis:3.5.2' + compileOnly 'redis.clients:jedis:4.4.3' compileOnly 'io.nats:jnats:2.16.4' compileOnly 'com.rabbitmq:amqp-client:5.12.0' compileOnly 'org.mongodb:mongodb-driver-legacy:4.5.0' diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java index 221e515dd..dcdea307c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java @@ -73,6 +73,7 @@ import static me.lucko.luckperms.common.config.generic.key.ConfigKeyFactory.lowe import static me.lucko.luckperms.common.config.generic.key.ConfigKeyFactory.mapKey; import static me.lucko.luckperms.common.config.generic.key.ConfigKeyFactory.notReloadable; import static me.lucko.luckperms.common.config.generic.key.ConfigKeyFactory.stringKey; +import static me.lucko.luckperms.common.config.generic.key.ConfigKeyFactory.stringListKey; /** * All of the {@link ConfigKey}s used by LuckPerms. @@ -639,6 +640,11 @@ public final class ConfigKeys { */ public static final ConfigKey REDIS_ADDRESS = notReloadable(stringKey("redis.address", null)); + /** + * The addresses of the redis servers (only for redis clusters) + */ + public static final ConfigKey> REDIS_ADDRESSES = notReloadable(stringListKey("redis.addresses", ImmutableList.of())); + /** * The username to connect with, or an empty string if it should use default */ diff --git a/common/src/main/java/me/lucko/luckperms/common/config/generic/key/ConfigKeyFactory.java b/common/src/main/java/me/lucko/luckperms/common/config/generic/key/ConfigKeyFactory.java index cd7da5a0c..add003435 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/generic/key/ConfigKeyFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/generic/key/ConfigKeyFactory.java @@ -28,6 +28,7 @@ package me.lucko.luckperms.common.config.generic.key; import com.google.common.collect.ImmutableMap; import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -36,6 +37,7 @@ public interface ConfigKeyFactory { ConfigKeyFactory BOOLEAN = ConfigurationAdapter::getBoolean; ConfigKeyFactory STRING = ConfigurationAdapter::getString; + ConfigKeyFactory> STRING_LIST = ConfigurationAdapter::getStringList; ConfigKeyFactory LOWERCASE_STRING = (adapter, path, def) -> adapter.getString(path, def).toLowerCase(Locale.ROOT); ConfigKeyFactory> STRING_MAP = (config, path, def) -> ImmutableMap.copyOf(config.getStringMap(path, ImmutableMap.of())); @@ -56,6 +58,10 @@ public interface ConfigKeyFactory { return key(new Bound<>(STRING, path, def)); } + static SimpleConfigKey> stringListKey(String path, List def) { + return key(new Bound<>(STRING_LIST, path, def)); + } + static SimpleConfigKey lowercaseStringKey(String path, String def) { return key(new Bound<>(LOWERCASE_STRING, path, def)); } diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java index 3ec7fa6b4..bc4a66c23 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java @@ -239,8 +239,8 @@ public enum Dependency { JEDIS( "redis.clients", "jedis", - "3.5.2", - "jX3340YaYjHFQN2sA+GCo33LB4FuIYKgQUPUv2MK/Xo=", + "4.4.3", + "wwwoCDPCywcfoNwpvwP95kXYusXSTtXhuVrB31sxE0k=", Relocation.of("jedis", "redis{}clients{}jedis"), Relocation.of("commonspool2", "org{}apache{}commons{}pool2") ), diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/MessagingFactory.java b/common/src/main/java/me/lucko/luckperms/common/messaging/MessagingFactory.java index c7a6271e5..7a2800779 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/MessagingFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/MessagingFactory.java @@ -43,6 +43,8 @@ import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; public class MessagingFactory

{ @@ -194,6 +196,7 @@ public class MessagingFactory

{ LuckPermsConfiguration config = getPlugin().getConfiguration(); String address = config.get(ConfigKeys.REDIS_ADDRESS); + List addresses = config.get(ConfigKeys.REDIS_ADDRESSES); String username = config.get(ConfigKeys.REDIS_USERNAME); String password = config.get(ConfigKeys.REDIS_PASSWORD); if (password.isEmpty()) { @@ -204,7 +207,18 @@ public class MessagingFactory

{ } boolean ssl = config.get(ConfigKeys.REDIS_SSL); - redis.init(address, username, password, ssl); + if (!addresses.isEmpty()) { + // redis cluster + addresses = new ArrayList<>(addresses); + if (address != null) { + addresses.add(address); + } + redis.init(addresses, username, password, ssl); + } else { + // redis pool + redis.init(address, username, password, ssl); + } + return redis; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java index 8fec0f7d7..e9e5ff69d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java @@ -30,11 +30,21 @@ import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.message.OutgoingMessage; import org.checkerframework.checker.nullness.qual.NonNull; -import redis.clients.jedis.Jedis; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisPooled; import redis.clients.jedis.JedisPubSub; import redis.clients.jedis.Protocol; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.exceptions.JedisClusterOperationException; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; /** * An implementation of {@link Messenger} using Redis. @@ -45,7 +55,7 @@ public class RedisMessenger implements Messenger { private final LuckPermsPlugin plugin; private final IncomingMessageConsumer consumer; - private /* final */ JedisPool jedisPool; + private /* final */ UnifiedJedis jedis; private /* final */ Subscription sub; private boolean closing = false; @@ -54,57 +64,74 @@ public class RedisMessenger implements Messenger { this.consumer = consumer; } + public void init(List addresses, String username, String password, boolean ssl) { + Set hosts = addresses.stream().map(RedisMessenger::parseAddress).collect(Collectors.toSet()); + this.init(new JedisCluster(hosts, jedisConfig(username, password, ssl))); + } + public void init(String address, String username, String password, boolean ssl) { + this.init(new JedisPooled(parseAddress(address), jedisConfig(username, password, ssl))); + } + + private void init(UnifiedJedis jedis) { + this.jedis = jedis; + this.sub = new Subscription(this); + this.plugin.getBootstrap().getScheduler().executeAsync(this.sub); + } + + private static JedisClientConfig jedisConfig(String username, String password, boolean ssl) { + return DefaultJedisClientConfig.builder() + .user(username) + .password(password) + .ssl(ssl) + .timeoutMillis(Protocol.DEFAULT_TIMEOUT) + .build(); + } + + private static HostAndPort parseAddress(String address) { String[] addressSplit = address.split(":"); String host = addressSplit[0]; int port = addressSplit.length > 1 ? Integer.parseInt(addressSplit[1]) : Protocol.DEFAULT_PORT; - - if (username == null) { - this.jedisPool = new JedisPool(new JedisPoolConfig(), host, port, Protocol.DEFAULT_TIMEOUT, password, ssl); - } else { - this.jedisPool = new JedisPool(new JedisPoolConfig(), host, port, Protocol.DEFAULT_TIMEOUT, username, password, ssl); - } - - this.sub = new Subscription(); - this.plugin.getBootstrap().getScheduler().executeAsync(this.sub); + return new HostAndPort(host, port); } @Override public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { - try (Jedis jedis = this.jedisPool.getResource()) { - jedis.publish(CHANNEL, outgoingMessage.asEncodedString()); - } catch (Exception e) { - e.printStackTrace(); - } + this.jedis.publish(CHANNEL, outgoingMessage.asEncodedString()); } @Override public void close() { this.closing = true; this.sub.unsubscribe(); - this.jedisPool.destroy(); + this.jedis.close(); } - private class Subscription extends JedisPubSub implements Runnable { + private static class Subscription extends JedisPubSub implements Runnable { + private final RedisMessenger messenger; + + private Subscription(RedisMessenger messenger) { + this.messenger = messenger; + } @Override public void run() { boolean first = true; - while (!RedisMessenger.this.closing && !Thread.interrupted() && !RedisMessenger.this.jedisPool.isClosed()) { - try (Jedis jedis = RedisMessenger.this.jedisPool.getResource()) { + while (!this.messenger.closing && !Thread.interrupted() && this.isRedisAlive()) { + try { if (first) { first = false; } else { - RedisMessenger.this.plugin.getLogger().info("Redis pubsub connection re-established"); + this.messenger.plugin.getLogger().info("Redis pubsub connection re-established"); } - jedis.subscribe(this, CHANNEL); // blocking call + this.messenger.jedis.subscribe(this, CHANNEL); // blocking call } catch (Exception e) { - if (RedisMessenger.this.closing) { + if (this.messenger.closing) { return; } - RedisMessenger.this.plugin.getLogger().warn("Redis pubsub connection dropped, trying to re-open the connection", e); + this.messenger.plugin.getLogger().warn("Redis pubsub connection dropped, trying to re-open the connection", e); try { unsubscribe(); } catch (Exception ignored) { @@ -126,8 +153,19 @@ public class RedisMessenger implements Messenger { if (!channel.equals(CHANNEL)) { return; } - RedisMessenger.this.consumer.consumeIncomingMessageAsString(msg); + this.messenger.consumer.consumeIncomingMessageAsString(msg); + } + + private boolean isRedisAlive() { + UnifiedJedis jedis = this.messenger.jedis; + + if (jedis instanceof JedisPooled) { + return !((JedisPooled) jedis).getPool().isClosed(); + } else if (jedis instanceof JedisCluster) { + return !((JedisCluster) jedis).getClusterNodes().isEmpty(); + } else { + throw new RuntimeException("Unknown jedis type: " + jedis.getClass().getName()); + } } } - } diff --git a/fabric/src/main/resources/luckperms.conf b/fabric/src/main/resources/luckperms.conf index 6045f1ebc..1b74e9d07 100644 --- a/fabric/src/main/resources/luckperms.conf +++ b/fabric/src/main/resources/luckperms.conf @@ -265,6 +265,7 @@ broadcast-received-log-entries = true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis { enabled = false address = "localhost" diff --git a/forge/src/main/resources/luckperms.conf b/forge/src/main/resources/luckperms.conf index 0093326c8..63e9e1876 100644 --- a/forge/src/main/resources/luckperms.conf +++ b/forge/src/main/resources/luckperms.conf @@ -263,6 +263,7 @@ broadcast-received-log-entries = true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis { enabled = false address = "localhost" diff --git a/nukkit/src/main/resources/config.yml b/nukkit/src/main/resources/config.yml index d77169a93..4eff4cb3f 100644 --- a/nukkit/src/main/resources/config.yml +++ b/nukkit/src/main/resources/config.yml @@ -257,6 +257,7 @@ broadcast-received-log-entries: true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost diff --git a/sponge/src/main/resources/luckperms.conf b/sponge/src/main/resources/luckperms.conf index 029c69892..1493b117f 100644 --- a/sponge/src/main/resources/luckperms.conf +++ b/sponge/src/main/resources/luckperms.conf @@ -265,6 +265,7 @@ broadcast-received-log-entries = true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis { enabled = false address = "localhost" diff --git a/standalone/build.gradle b/standalone/build.gradle index c44f0c862..4db3fbfa6 100644 --- a/standalone/build.gradle +++ b/standalone/build.gradle @@ -29,7 +29,7 @@ dependencies { testImplementation 'org.awaitility:awaitility:4.2.0' testImplementation 'com.zaxxer:HikariCP:4.0.3' - testImplementation 'redis.clients:jedis:3.5.2' + testImplementation 'redis.clients:jedis:4.4.3' testImplementation 'io.nats:jnats:2.16.4' testImplementation 'com.rabbitmq:amqp-client:5.12.0' testImplementation 'org.postgresql:postgresql:42.6.0' diff --git a/standalone/src/main/resources/config.yml b/standalone/src/main/resources/config.yml index d9fe05ecb..fe72e1d72 100644 --- a/standalone/src/main/resources/config.yml +++ b/standalone/src/main/resources/config.yml @@ -247,6 +247,7 @@ broadcast-received-log-entries: true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost diff --git a/velocity/src/main/resources/config.yml b/velocity/src/main/resources/config.yml index dcd8ac6fd..6c8b0608d 100644 --- a/velocity/src/main/resources/config.yml +++ b/velocity/src/main/resources/config.yml @@ -251,6 +251,7 @@ broadcast-received-log-entries: false # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost