9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-19 14:59:21 +00:00

feat: add more Redis configuration options (#564)

This commit is contained in:
Sushant Pangeni
2025-08-05 19:30:23 +05:45
committed by GitHub
parent a1956c6822
commit 807bffe9aa
2 changed files with 100 additions and 58 deletions

View File

@@ -174,7 +174,45 @@ public class Settings {
private int database = 0; private int database = 0;
private String user = ""; private String user = "";
private String password = ""; private String password = "";
@Comment("Use SSL/TLS for encrypted connections.")
private boolean useSsl = false; private boolean useSsl = false;
@Comment("Connection timeout in milliseconds.")
private int connectionTimeout = 2000;
@Comment("Socket (read/write) timeout in milliseconds.")
private int socketTimeout = 2000;
@Comment("Max number of connections in the pool.")
private int maxTotalConnections = 50;
@Comment("Max number of idle connections in the pool.")
private int maxIdleConnections = 8;
@Comment("Min number of idle connections in the pool.")
private int minIdleConnections = 2;
@Comment("Enable health checks when borrowing connections from the pool.")
private boolean testOnBorrow = true;
@Comment("Enable health checks when returning connections to the pool.")
private boolean testOnReturn = true;
@Comment("Enable periodic idle connection health checks.")
private boolean testWhileIdle = true;
@Comment("Min evictable idle time (ms) before a connection is eligible for eviction.")
private long minEvictableIdleTimeMillis = 60000;
@Comment("Time (ms) between eviction runs.")
private long timeBetweenEvictionRunsMillis = 30000;
@Comment("Number of retries for commands when connection fails.")
private int maxRetries = 3;
@Comment("Base backoff time in ms for retries (exponential backoff multiplier).")
private int retryBackoffMillis = 200;
} }
@Comment("Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!") @Comment("Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!")

View File

@@ -62,9 +62,11 @@ public class RedisManager extends JedisPubSub {
/** /**
* Initialize Redis connection pool * Initialize Redis connection pool
*/ */
@Blocking @Blocking
public void initialize() throws IllegalStateException { public void initialize() throws IllegalStateException {
final Settings.RedisSettings.RedisCredentials credentials = plugin.getSettings().getRedis().getCredentials(); final Settings.RedisSettings.RedisCredentials credentials = plugin.getSettings().getRedis().getCredentials();
final String user = credentials.getUser(); final String user = credentials.getUser();
final String password = credentials.getPassword(); final String password = credentials.getPassword();
final String host = credentials.getHost(); final String host = credentials.getHost();
@@ -72,44 +74,50 @@ public class RedisManager extends JedisPubSub {
final int database = credentials.getDatabase(); final int database = credentials.getDatabase();
final boolean useSSL = credentials.isUseSsl(); final boolean useSSL = credentials.isUseSsl();
// Create the jedis pool // Configure JedisPoolConfig
final JedisPoolConfig config = new JedisPoolConfig(); final JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(0); config.setMaxTotal(credentials.getMaxTotalConnections());
config.setTestOnBorrow(true); config.setMaxIdle(credentials.getMaxIdleConnections());
config.setTestOnReturn(true); config.setMinIdle(credentials.getMinIdleConnections());
config.setTestOnBorrow(credentials.isTestOnBorrow());
config.setTestOnReturn(credentials.isTestOnReturn());
config.setTestWhileIdle(credentials.isTestWhileIdle());
config.setMinEvictableIdleTimeMillis(credentials.getMinEvictableIdleTimeMillis());
config.setTimeBetweenEvictionRunsMillis(credentials.getTimeBetweenEvictionRunsMillis());
final Settings.RedisSettings.RedisSentinel sentinel = plugin.getSettings().getRedis().getSentinel(); final Settings.RedisSettings.RedisSentinel sentinel = plugin.getSettings().getRedis().getSentinel();
Set<String> redisSentinelNodes = new HashSet<>(sentinel.getNodes()); Set<String> redisSentinelNodes = new HashSet<>(sentinel.getNodes());
if (redisSentinelNodes.isEmpty()) { if (redisSentinelNodes.isEmpty()) {
// Standalone Redis setup
DefaultJedisClientConfig.Builder clientConfigBuilder = DefaultJedisClientConfig.builder() DefaultJedisClientConfig.Builder clientConfigBuilder = DefaultJedisClientConfig.builder()
.ssl(useSSL) .ssl(useSSL)
.database(database) .database(database)
.timeoutMillis(0); .timeoutMillis(credentials.getConnectionTimeout()) // connection and socket timeout combined
.user(user.isEmpty() ? null : user)
if (!user.isEmpty()) { .password(password.isEmpty() ? null : password);
clientConfigBuilder.user(user);
}
if (!password.isEmpty()) {
clientConfigBuilder.password(password);
}
this.jedisPool = new JedisPool(config, new HostAndPort(host, port), clientConfigBuilder.build()); this.jedisPool = new JedisPool(config, new HostAndPort(host, port), clientConfigBuilder.build());
} else { } else {
final String sentinelPassword = sentinel.getPassword(); final String sentinelPassword = sentinel.getPassword();
this.jedisPool = new JedisSentinelPool(sentinel.getMaster(), redisSentinelNodes, password.isEmpty() this.jedisPool = new JedisSentinelPool(
? null : password, sentinelPassword.isEmpty() ? null : sentinelPassword); sentinel.getMaster(),
redisSentinelNodes,
config,
credentials.getConnectionTimeout(),
credentials.getSocketTimeout(),
password.isEmpty() ? null : password,
sentinelPassword.isEmpty() ? null : sentinelPassword,
database);
} }
// Ping the server to check the connection try (var jedis = jedisPool.getResource()) {
try { jedis.ping();
jedisPool.getResource().ping();
} catch (JedisException e) { } catch (JedisException e) {
throw new IllegalStateException("Failed to establish connection with Redis. " throw new IllegalStateException("Failed to establish connection with Redis. " +
+ "Please check the supplied credentials in the config file", e); "Please check the supplied credentials in the config file", e);
} }
// Subscribe using a thread (rather than a task)
enabled = true; enabled = true;
new Thread(this::subscribe, "husksync:redis_subscriber").start(); new Thread(this::subscribe, "husksync:redis_subscriber").start();
} }
@@ -126,8 +134,7 @@ public class RedisManager extends JedisPubSub {
this, this,
Arrays.stream(RedisMessage.Type.values()) Arrays.stream(RedisMessage.Type.values())
.map(type -> type.getMessageChannel(clusterId)) .map(type -> type.getMessageChannel(clusterId))
.toArray(String[]::new) .toArray(String[]::new));
);
} catch (Throwable t) { } catch (Throwable t) {
// Thread was unlocked due error // Thread was unlocked due error
onThreadUnlock(t); onThreadUnlock(t);
@@ -175,20 +182,19 @@ public class RedisManager extends JedisPubSub {
user -> { user -> {
plugin.lockPlayer(user.getUuid()); plugin.lockPlayer(user.getUuid());
try { try {
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload()); final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin,
redisMessage.getPayload());
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED); user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred updating user data from Redis", e); plugin.log(Level.SEVERE, "An exception occurred updating user data from Redis", e);
user.completeSync(false, DataSnapshot.UpdateCause.UPDATED, plugin); user.completeSync(false, DataSnapshot.UpdateCause.UPDATED, plugin);
} }
} });
);
case REQUEST_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent( case REQUEST_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent(
user -> RedisMessage.create( user -> RedisMessage.create(
UUID.fromString(new String(redisMessage.getPayload(), StandardCharsets.UTF_8)), UUID.fromString(new String(redisMessage.getPayload(), StandardCharsets.UTF_8)),
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin) user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin))
).dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA) .dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA));
);
case CHECK_IN_PETITION -> { case CHECK_IN_PETITION -> {
if (!redisMessage.isTargetServer(plugin) if (!redisMessage.isTargetServer(plugin)
|| !plugin.getSettings().getSynchronization().isCheckinPetitions()) { || !plugin.getSettings().getSynchronization().isCheckinPetitions()) {
@@ -199,7 +205,8 @@ public class RedisManager extends JedisPubSub {
boolean online = plugin.getDisconnectingPlayers().contains(user.getUuid()) boolean online = plugin.getDisconnectingPlayers().contains(user.getUuid())
|| plugin.getOnlineUser(user.getUuid()).isEmpty(); || plugin.getOnlineUser(user.getUuid()).isEmpty();
if (!online && !plugin.isLocked(user.getUuid())) { if (!online && !plugin.isLocked(user.getUuid())) {
plugin.debug("[%s] Received check-in petition for online/unlocked user, ignoring".formatted(user.getName())); plugin.debug("[%s] Received check-in petition for online/unlocked user, ignoring"
.formatted(user.getName()));
return; return;
} }
plugin.getRedisManager().setUserCheckedOut(user, false); plugin.getRedisManager().setUserCheckedOut(user, false);
@@ -252,31 +259,30 @@ public class RedisManager extends JedisPubSub {
redisMessage.dispatch(plugin, RedisMessage.Type.CHECK_IN_PETITION); redisMessage.dispatch(plugin, RedisMessage.Type.CHECK_IN_PETITION);
} }
public CompletableFuture<Optional<DataSnapshot.Packed>> getOnlineUserData(@NotNull UUID requestId, @NotNull User user, public CompletableFuture<Optional<DataSnapshot.Packed>> getOnlineUserData(@NotNull UUID requestId,
@NotNull DataSnapshot.SaveCause saveCause) { @NotNull User user,
@NotNull DataSnapshot.SaveCause saveCause) {
return plugin.getOnlineUser(user.getUuid()) return plugin.getOnlineUser(user.getUuid())
.map(online -> CompletableFuture.completedFuture( .map(online -> CompletableFuture.completedFuture(
Optional.of(online.createSnapshot(saveCause))) Optional.of(online.createSnapshot(saveCause))))
)
.orElse(this.getNetworkedUserData(requestId, user)); .orElse(this.getNetworkedUserData(requestId, user));
} }
// Request a user's dat x-server // Request a user's dat x-server
private CompletableFuture<Optional<DataSnapshot.Packed>> getNetworkedUserData(@NotNull UUID requestId, @NotNull User user) { private CompletableFuture<Optional<DataSnapshot.Packed>> getNetworkedUserData(@NotNull UUID requestId,
@NotNull User user) {
final CompletableFuture<Optional<DataSnapshot.Packed>> future = new CompletableFuture<>(); final CompletableFuture<Optional<DataSnapshot.Packed>> future = new CompletableFuture<>();
pendingRequests.put(requestId, future); pendingRequests.put(requestId, future);
plugin.runAsync(() -> { plugin.runAsync(() -> {
final RedisMessage redisMessage = RedisMessage.create( final RedisMessage redisMessage = RedisMessage.create(
user.getUuid(), user.getUuid(),
requestId.toString().getBytes(StandardCharsets.UTF_8) requestId.toString().getBytes(StandardCharsets.UTF_8));
);
redisMessage.dispatch(plugin, RedisMessage.Type.REQUEST_USER_DATA); redisMessage.dispatch(plugin, RedisMessage.Type.REQUEST_USER_DATA);
}); });
return future return future
.orTimeout( .orTimeout(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds(), plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds(),
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS)
)
.exceptionally(throwable -> { .exceptionally(throwable -> {
pendingRequests.remove(requestId); pendingRequests.remove(requestId);
return Optional.empty(); return Optional.empty();
@@ -290,8 +296,7 @@ public class RedisManager extends JedisPubSub {
jedis.setex( jedis.setex(
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId), getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId),
RedisKeyType.TTL_1_YEAR, RedisKeyType.TTL_1_YEAR,
data.asBytes(plugin) data.asBytes(plugin));
);
plugin.debug(String.format("[%s] Set %s key on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT)); plugin.debug(String.format("[%s] Set %s key on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e); plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
@@ -302,8 +307,7 @@ public class RedisManager extends JedisPubSub {
public void clearUserData(@NotNull User user) { public void clearUserData(@NotNull User user) {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
jedis.del( jedis.del(
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId) getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId));
);
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT)); plugin.debug(String.format("[%s] Cleared %s on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e); plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
@@ -317,8 +321,7 @@ public class RedisManager extends JedisPubSub {
if (checkedOut) { if (checkedOut) {
jedis.set( jedis.set(
key.getBytes(StandardCharsets.UTF_8), key.getBytes(StandardCharsets.UTF_8),
plugin.getServerName().getBytes(StandardCharsets.UTF_8) plugin.getServerName().getBytes(StandardCharsets.UTF_8));
);
} else { } else {
if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) { if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) {
plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)", plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)",
@@ -382,8 +385,7 @@ public class RedisManager extends JedisPubSub {
jedis.setex( jedis.setex(
getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId), getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId),
RedisKeyType.TTL_10_SECONDS, RedisKeyType.TTL_10_SECONDS,
new byte[0] new byte[0]);
);
plugin.debug(String.format("[%s] Set %s key to Redis", plugin.debug(String.format("[%s] Set %s key to Redis",
user.getName(), RedisKeyType.SERVER_SWITCH)); user.getName(), RedisKeyType.SERVER_SWITCH));
} catch (Throwable e) { } catch (Throwable e) {
@@ -395,7 +397,8 @@ public class RedisManager extends JedisPubSub {
* Fetch a user's data from Redis and consume the key if found * Fetch a user's data from Redis and consume the key if found
* *
* @param user The user to fetch data for * @param user The user to fetch data for
* @return The user's data, if it's present on the database. Otherwise, an empty optional. * @return The user's data, if it's present on the database. Otherwise, an empty
* optional.
*/ */
@Blocking @Blocking
public Optional<DataSnapshot.Packed> getUserData(@NotNull User user) { public Optional<DataSnapshot.Packed> getUserData(@NotNull User user) {
@@ -476,13 +479,11 @@ public class RedisManager extends JedisPubSub {
jedis.setex( jedis.setex(
getMapIdKey(fromServer, fromId, toServer, clusterId), getMapIdKey(fromServer, fromId, toServer, clusterId),
RedisKeyType.TTL_1_YEAR, RedisKeyType.TTL_1_YEAR,
String.valueOf(toId).getBytes(StandardCharsets.UTF_8) String.valueOf(toId).getBytes(StandardCharsets.UTF_8));
);
jedis.setex( jedis.setex(
getReversedMapIdKey(toServer, toId, clusterId), getReversedMapIdKey(toServer, toId, clusterId),
RedisKeyType.TTL_1_YEAR, RedisKeyType.TTL_1_YEAR,
String.format("%s:%s", fromServer, fromId).getBytes(StandardCharsets.UTF_8) String.format("%s:%s", fromServer, fromId).getBytes(StandardCharsets.UTF_8));
);
plugin.debug(String.format("Bound map %s:%s -> %s:%s on Redis", fromServer, fromId, toServer, toId)); plugin.debug(String.format("Bound map %s:%s -> %s:%s on Redis", fromServer, fromId, toServer, toId));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred binding map ids on Redis", e); plugin.log(Level.SEVERE, "An exception occurred binding map ids on Redis", e);
@@ -534,8 +535,7 @@ public class RedisManager extends JedisPubSub {
jedis.setex( jedis.setex(
getMapDataKey(serverName, mapId, clusterId), getMapDataKey(serverName, mapId, clusterId),
RedisKeyType.TTL_1_YEAR, RedisKeyType.TTL_1_YEAR,
data data);
);
plugin.debug(String.format("Set map data %s:%s on Redis", serverName, mapId)); plugin.debug(String.format("Set map data %s:%s on Redis", serverName, mapId));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting map data on Redis", e); plugin.log(Level.SEVERE, "An exception occurred setting map data on Redis", e);
@@ -581,16 +581,20 @@ public class RedisManager extends JedisPubSub {
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid); return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid);
} }
private static byte[] getMapIdKey(@NotNull String fromServer, int fromId, @NotNull String toServer, @NotNull String clusterId) { private static byte[] getMapIdKey(@NotNull String fromServer, int fromId, @NotNull String toServer,
return String.format("%s:%s:%s:%s", RedisKeyType.MAP_ID.getKeyPrefix(clusterId), fromServer, fromId, toServer).getBytes(StandardCharsets.UTF_8); @NotNull String clusterId) {
return String.format("%s:%s:%s:%s", RedisKeyType.MAP_ID.getKeyPrefix(clusterId), fromServer, fromId, toServer)
.getBytes(StandardCharsets.UTF_8);
} }
private static byte[] getReversedMapIdKey(@NotNull String toServer, int toId, @NotNull String clusterId) { private static byte[] getReversedMapIdKey(@NotNull String toServer, int toId, @NotNull String clusterId) {
return String.format("%s:%s:%s", RedisKeyType.MAP_ID_REVERSED.getKeyPrefix(clusterId), toServer, toId).getBytes(StandardCharsets.UTF_8); return String.format("%s:%s:%s", RedisKeyType.MAP_ID_REVERSED.getKeyPrefix(clusterId), toServer, toId)
.getBytes(StandardCharsets.UTF_8);
} }
private static byte[] getMapDataKey(@NotNull String serverName, int mapId, @NotNull String clusterId) { private static byte[] getMapDataKey(@NotNull String serverName, int mapId, @NotNull String clusterId) {
return String.format("%s:%s:%s", RedisKeyType.MAP_DATA.getKeyPrefix(clusterId), serverName, mapId).getBytes(StandardCharsets.UTF_8); return String.format("%s:%s:%s", RedisKeyType.MAP_DATA.getKeyPrefix(clusterId), serverName, mapId)
.getBytes(StandardCharsets.UTF_8);
} }
} }