Skip to content

Commit 7b77b22

Browse files
authored
feat: add forced-host label for Velocity forced hosts support
Add a new 'forced-host' label/annotation that can be set on Pods and ReplicaSets to configure Velocity's forced hosts functionality. Players connecting via a specific hostname will be routed to the server with the matching forced-host label. See: https://docs.papermc.io/velocity/configuration/\#forced-hosts-section
1 parent e114f16 commit 7b77b22

8 files changed

Lines changed: 79 additions & 7 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,6 @@ buildNumber.properties
109109
# JDT-specific (Eclipse Java Development Tools)
110110
.classpath
111111

112-
# End of https://www.toptal.com/developers/gitignore/api/maven,intellij+all
112+
# End of https://www.toptal.com/developers/gitignore/api/maven,intellij+all
113+
114+
.vscode/

src/main/java/net/azisaba/kuvel/KuvelServiceHandler.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
import java.util.ArrayList;
1111
import java.util.HashMap;
1212
import java.util.List;
13+
import java.util.Map;
1314
import java.util.Map.Entry;
1415
import java.util.Optional;
16+
import java.util.concurrent.ConcurrentHashMap;
17+
import java.util.concurrent.CopyOnWriteArrayList;
1518
import java.util.concurrent.atomic.AtomicReference;
1619
import javax.annotation.Nullable;
1720

@@ -38,6 +41,7 @@ public class KuvelServiceHandler {
3841
private final UidAndServerNameMap replicaSetUidAndServerNameMap = new UidAndServerNameMap();
3942

4043
private final List<String> initialServerNames = new ArrayList<>();
44+
private final Map<String, List<String>> forcedHosts = new ConcurrentHashMap<>();
4145

4246
private final AtomicReference<ServerDiscovery> serverDiscovery = new AtomicReference<>();
4347
private final AtomicReference<LoadBalancerDiscovery> loadBalancerDiscovery =
@@ -59,6 +63,10 @@ public void registerLoadBalancer(LoadBalancer loadBalancer) {
5963
initialServerNames.add(serverName);
6064
}
6165

66+
if (loadBalancer.getForcedHost() != null) {
67+
addForcedHost(loadBalancer.getForcedHost(), serverName);
68+
}
69+
6270
plugin
6371
.getLogger()
6472
.info(
@@ -101,6 +109,7 @@ public void unregisterLoadBalancer(LoadBalancer loadBalancer) {
101109
replicaSetUidAndServerNameMap.unregister(loadBalancer.getReplicaSetUid());
102110

103111
initialServerNames.remove(serverName);
112+
removeForcedHost(serverName);
104113

105114
plugin
106115
.getLogger()
@@ -244,13 +253,26 @@ public boolean registerPod(Pod pod, String serverName) {
244253
}
245254
}
246255

256+
String labelKeyPrefix = plugin.getKuvelConfig().getLabelKeyPrefix();
247257
String initialServerStr =
248258
pod.getMetadata().getLabels().getOrDefault(
249-
LabelKeys.INITIAL_SERVER.getKey(plugin.getKuvelConfig().getLabelKeyPrefix()), "false");
259+
LabelKeys.INITIAL_SERVER.getKey(labelKeyPrefix), "false");
250260
if (Boolean.parseBoolean(initialServerStr)) {
251261
initialServerNames.add(serverName);
252262
}
253263

264+
String forcedHost =
265+
pod.getMetadata().getLabels().getOrDefault(
266+
LabelKeys.FORCED_HOST.getKey(labelKeyPrefix), null);
267+
if (forcedHost == null) {
268+
forcedHost =
269+
pod.getMetadata().getAnnotations().getOrDefault(
270+
LabelKeys.FORCED_HOST.getKey(labelKeyPrefix), null);
271+
}
272+
if (forcedHost != null) {
273+
addForcedHost(forcedHost, serverName);
274+
}
275+
254276
plugin
255277
.getLogger()
256278
.info("Registered server: " + serverName + " (" + pod.getMetadata().getUid() + ")");
@@ -315,6 +337,7 @@ public void unregisterPod(String podUid) {
315337
}
316338

317339
initialServerNames.remove(serverName);
340+
removeForcedHost(serverName);
318341

319342
plugin.getLogger().info("Unregistered server: " + serverName + " (" + podUid + ")");
320343
}
@@ -337,4 +360,17 @@ public void unregisterPod(Pod pod) {
337360
public boolean isPodRegistered(String podId) {
338361
return podUidAndServerNameMap.getServerNameFromUid(podId) != null;
339362
}
363+
364+
private void addForcedHost(String hostname, String serverName) {
365+
forcedHosts.computeIfAbsent(hostname, k -> new CopyOnWriteArrayList<>()).add(serverName);
366+
}
367+
368+
private void removeForcedHost(String serverName) {
369+
forcedHosts.values().forEach(list -> list.remove(serverName));
370+
forcedHosts.entrySet().removeIf(entry -> entry.getValue().isEmpty());
371+
}
372+
373+
public Map<String, List<String>> getForcedHosts() {
374+
return forcedHosts;
375+
}
340376
}

src/main/java/net/azisaba/kuvel/discovery/impl/redis/RedisLoadBalancerDiscovery.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@ private void registerOrIgnore(ReplicaSet replicaSet, boolean isFetchedFromRedis)
136136
.getLabels()
137137
.getOrDefault(LabelKeys.INITIAL_SERVER.getKey(labelKeyPrefix), "false")
138138
.equalsIgnoreCase("true");
139+
String forcedHost =
140+
metadata
141+
.getLabels()
142+
.getOrDefault(LabelKeys.FORCED_HOST.getKey(labelKeyPrefix), null);
143+
if (forcedHost == null) {
144+
forcedHost =
145+
metadata
146+
.getAnnotations()
147+
.getOrDefault(LabelKeys.FORCED_HOST.getKey(labelKeyPrefix), null);
148+
}
139149
if (isDisableLoadBalancer(metadata)) {
140150
return;
141151
}
@@ -169,7 +179,7 @@ private void registerOrIgnore(ReplicaSet replicaSet, boolean isFetchedFromRedis)
169179
kuvelServiceHandler.getReplicaSetUidAndServerNameMap().register(uid, serverName);
170180
jedis.hset(RedisKeys.LOAD_BALANCERS_PREFIX.getKey() + groupName, uid, serverName);
171181

172-
redisConnectionLeader.publishNewLoadBalancer(uid, serverName, initialServer);
182+
redisConnectionLeader.publishNewLoadBalancer(uid, serverName, initialServer, forcedHost);
173183

174184
RegisteredServer server =
175185
plugin
@@ -182,7 +192,8 @@ private void registerOrIgnore(ReplicaSet replicaSet, boolean isFetchedFromRedis)
182192
server,
183193
new RoundRobinLoadBalancingStrategy(),
184194
uid,
185-
initialServer));
195+
initialServer,
196+
forcedHost));
186197
}
187198
}
188199

src/main/java/net/azisaba/kuvel/listener/ChooseInitialServerListener.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent;
55
import com.velocitypowered.api.proxy.ProxyServer;
66
import com.velocitypowered.api.proxy.server.RegisteredServer;
7+
import java.net.InetSocketAddress;
78
import java.util.ArrayList;
89
import java.util.Collections;
910
import java.util.List;
@@ -19,6 +20,21 @@ public class ChooseInitialServerListener {
1920

2021
@Subscribe
2122
public void onInitialServerChoose(PlayerChooseInitialServerEvent event) {
23+
Optional<InetSocketAddress> virtualHost = event.getPlayer().getVirtualHost();
24+
if (virtualHost.isPresent()) {
25+
String hostname = virtualHost.get().getHostString();
26+
List<String> forcedServerNames = handler.getForcedHosts().get(hostname);
27+
if (forcedServerNames != null && !forcedServerNames.isEmpty()) {
28+
for (String serverName : forcedServerNames) {
29+
Optional<RegisteredServer> optionalServer = proxy.getServer(serverName);
30+
if (optionalServer.isPresent()) {
31+
event.setInitialServer(optionalServer.get());
32+
return;
33+
}
34+
}
35+
}
36+
}
37+
2238
if (handler.getInitialServerNames().isEmpty()) {
2339
return;
2440
}

src/main/java/net/azisaba/kuvel/loadbalancer/LoadBalancer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class LoadBalancer {
1919
private final String replicaSetUid;
2020

2121
private final boolean isInitialServer;
22+
private final String forcedHost;
2223
private final List<String> endpointServers = new ArrayList<>();
2324

2425
public void addEndpoint(String serverName) {

src/main/java/net/azisaba/kuvel/redis/RedisConnectionLeader.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ public void leaveLeader() {
8080
}
8181

8282
public void publishNewLoadBalancer(
83-
String replicaSetUid, String serverName, boolean initialServer) {
83+
String replicaSetUid, String serverName, boolean initialServer, String forcedHost) {
8484
try (Jedis jedis = jedisPool.getResource()) {
8585
jedis.publish(
8686
RedisKeys.LOAD_BALANCER_ADDED_NOTIFY_PREFIX.getKey() + groupName,
87-
replicaSetUid + ":" + serverName + ":" + initialServer);
87+
replicaSetUid + ":" + serverName + ":" + initialServer + ":" + (forcedHost != null ? forcedHost : ""));
8888
}
8989
}
9090

src/main/java/net/azisaba/kuvel/redis/RedisSubscriber.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public void onPMessage(String pattern, String channel, String message) {
4545
String replicaSetUid = message.split(":")[0];
4646
String serverName = message.split(":")[1];
4747
boolean initialServer = Boolean.parseBoolean(message.split(":")[2]);
48+
String forcedHost = message.split(":").length > 3 ? message.split(":")[3] : null;
49+
if (forcedHost != null && forcedHost.isEmpty()) {
50+
forcedHost = null;
51+
}
4852

4953
RegisteredServer server =
5054
plugin
@@ -56,7 +60,8 @@ public void onPMessage(String pattern, String channel, String message) {
5660
server,
5761
new RoundRobinLoadBalancingStrategy(),
5862
replicaSetUid,
59-
initialServer);
63+
initialServer,
64+
forcedHost);
6065
kuvelServiceHandler.registerLoadBalancer(loadBalancer);
6166
} else if (channel.startsWith(RedisKeys.POD_DELETED_NOTIFY_PREFIX.getKey())) {
6267
kuvelServiceHandler.unregisterPod(message);

src/main/java/net/azisaba/kuvel/util/LabelKeys.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public enum LabelKeys {
99
INITIAL_SERVER("initial-server"),
1010
DISABLE_NAME_SUFFIX("disable-name-suffix"),
1111
DISABLE_LOAD_BALANCER("disable-load-balancer"),
12+
FORCED_HOST("forced-host"),
1213
;
1314

1415
private final String key;

0 commit comments

Comments
 (0)