diff --git a/plugins/velocity/src/main/java/best/lolicon/felis/velocity/ServerRegistry.java b/plugins/velocity/src/main/java/best/lolicon/felis/velocity/ServerRegistry.java index 25fc0fb..03891fe 100644 --- a/plugins/velocity/src/main/java/best/lolicon/felis/velocity/ServerRegistry.java +++ b/plugins/velocity/src/main/java/best/lolicon/felis/velocity/ServerRegistry.java @@ -36,9 +36,18 @@ import java.util.concurrent.ConcurrentHashMap; * first successful refresh replaces that placeholder with the live ClusterIP. A * proxy that starts while felis-api is down refreshes once from the last list the * API answered with instead ({@link ServerListSource}). + * + *
Only a {@code direct} endpoint carries a backend address. A server that is not
+ * up reports {@code fallback}, with the NAME of its fallback server ("login") in the
+ * address field: dialled as a hostname that resolves nowhere, so registering it
+ * turned every wake-and-transfer into "couldn't connect you". A fallback report leaves
+ * the last direct registration in place — the ClusterIP belongs to the Service and
+ * outlives the pod — and {@link #observe(ServerView)} lets the waiting queue apply the
+ * address a fresh status poll reports before it transfers anyone.
*/
final class ServerRegistry {
private static final int DEFAULT_PORT = 25565;
+ private static final String ENDPOINT_DIRECT = "direct";
private final ProxyServer proxy;
private final Logger log;
@@ -84,10 +93,23 @@ final class ServerRegistry {
subdomainToName.keySet().retainAll(subdomains.keySet());
}
+ /**
+ * observe applies one server's freshly polled view between refreshes: the view
+ * routing reads, and its registration when the poll reports a direct endpoint. A
+ * name the last refresh did not list is left to the next one.
+ */
+ void observe(ServerView v) {
+ String name = v.name();
+ if (name == null || byName.replace(name, v) == null) {
+ return;
+ }
+ ensureRegistered(v);
+ }
+
private void ensureRegistered(ServerView v) {
String addr = v.endpointAddress();
- if (addr == null || addr.isEmpty()) {
- return; // no backend address yet (server never started) → nothing to register
+ if (!ENDPOINT_DIRECT.equalsIgnoreCase(v.endpointMode()) || addr == null || addr.isEmpty()) {
+ return; // not up (the address is a fallback server's name) or never started
}
InetSocketAddress target = parseAddress(addr);
Optional Status replies carry the endpoint the way the operator writes it: a server that
+ * is up reports {@code direct} and its {@link #address}; one that is not reports
+ * {@code fallback} with the fallback server's NAME, "login", in the address field.
*/
static final class Api implements AutoCloseable {
final Set The views are shaped the way the operator writes status: up is {@code direct}
+ * plus the Service address; down is {@code fallback} with the fallback server's NAME
+ * in the address field; a server never reconciled has neither.
*
* Run: {@code ./gradlew routingTest} in plugins/velocity (it needs the velocity-api
* classes the plugin compiles against).
@@ -28,32 +33,65 @@ public final class ServerRegistryTest {
ServerRegistry reg = new ServerRegistry(net.proxy, log.logger, "MC.Example.test");
reg.refresh(List.of(
- view("login", "login", true, "10.43.0.1:25565"),
- view("lobby", "lobby", true, "10.43.0.2:25565"),
- view("alpha", "alpha", false, "10.43.0.3:25566"),
- view("fresh", "fresh", false, null),
- view("", "blank", true, "10.43.0.9:25565")));
+ up("login", "login", "10.43.0.1:25565"),
+ up("lobby", "lobby", "10.43.0.2:25565"),
+ down("alpha", "alpha"),
+ unstarted("fresh", "fresh"),
+ up("", "blank", "10.43.0.9:25565")));
assertEq("first refresh registrations",
- List.of("-login", "+login 10.43.0.1:25565", "+lobby 10.43.0.2:25565", "+alpha 10.43.0.3:25566"),
+ List.of("-login", "+login 10.43.0.1:25565", "+lobby 10.43.0.2:25565"),
List.copyOf(net.registrations));
assertEq("login placeholder replaced", "10.43.0.1:25565", net.address("login"));
- assertEq("a server with no address is known", true, reg.isManaged("fresh"));
+ assertEq("a stopped server is known", true, reg.isManaged("alpha"));
+ assertEq("... but its fallback's name is not dialled", null, net.address("alpha"));
+ assertEq("a server with no endpoint is known", true, reg.isManaged("fresh"));
assertEq("... but not registered", null, net.address("fresh"));
assertEq("a nameless entry is skipped", false, reg.isManaged(""));
assertEq("managed count", 4, reg.all().size());
- assertEq("registration logged", 1, log.count("INFO", "registered backend alpha -> 10.43.0.3:25566"));
+ assertEq("registration logged", 1, log.count("INFO", "registered backend lobby -> 10.43.0.2:25565"));
net.registrations.clear();
reg.refresh(List.of(
- view("login", "login", true, "10.43.0.1:25565"),
- view("lobby", "lobby", true, "10.43.0.2:25565"),
- view("alpha", "alpha", true, "10.43.0.7:25566"),
- view("fresh", "fresh", true, "10.43.0.8:25565")));
- assertEq("second refresh: only the moved and the new",
- List.of("-alpha", "+alpha 10.43.0.7:25566", "+fresh 10.43.0.8:25565"),
+ up("login", "login", "10.43.0.1:25565"),
+ up("lobby", "lobby", "10.43.0.2:25565"),
+ up("alpha", "alpha", "10.43.0.3:25566"),
+ up("fresh", "fresh", "10.43.0.8:25565")));
+ assertEq("second refresh: only the servers that came up",
+ List.of("+alpha 10.43.0.3:25566", "+fresh 10.43.0.8:25565"),
List.copyOf(net.registrations));
assertEq("view follows the fetch", true, reg.view("alpha").ready());
+ // alpha goes down: its Service keeps the ClusterIP, so the registration stays.
+ net.registrations.clear();
+ reg.refresh(List.of(
+ up("login", "login", "10.43.0.1:25565"),
+ up("lobby", "lobby", "10.43.0.2:25565"),
+ down("alpha", "alpha"),
+ up("fresh", "fresh", "10.43.0.8:25565")));
+ assertEq("down: registrations untouched", List.of(), List.copyOf(net.registrations));
+ assertEq("down: last direct address kept", "10.43.0.3:25566", net.address("alpha"));
+ assertEq("down: the view says so", false, reg.view("alpha").ready());
+
+ // A polled view between refreshes: up at a new address re-registers at once.
+ reg.observe(up("alpha", "alpha", "10.43.0.7:25566"));
+ assertEq("observed: moved", List.of("-alpha", "+alpha 10.43.0.7:25566"), List.copyOf(net.registrations));
+ assertEq("observed: the view follows", true, reg.view("alpha").ready());
+ net.registrations.clear();
+ reg.observe(down("alpha", "alpha"));
+ assertEq("observed down: registration kept", "10.43.0.7:25566", net.address("alpha"));
+ reg.observe(up("stranger", "stranger", "10.43.0.99:25565"));
+ assertEq("observed a name no refresh listed: left to the next refresh", false, reg.isManaged("stranger"));
+ assertEq("... and not registered", null, net.address("stranger"));
+ assertEq("observing registered nothing", List.of(), List.copyOf(net.registrations));
+
+ net.registrations.clear();
+ reg.refresh(List.of(
+ up("login", "login", "10.43.0.1:25565"),
+ up("lobby", "lobby", "10.43.0.2:25565"),
+ up("alpha", "alpha", "10.43.0.7:25566"),
+ up("fresh", "fresh", "10.43.0.8:25565")));
+ assertEq("an unchanged refresh changes nothing", List.of(), List.copyOf(net.registrations));
+
assertEq("host routing", "alpha", name(reg.resolveByHost("alpha.mc.example.test")));
assertEq("host routing ignores case", "alpha", name(reg.resolveByHost("ALPHA.Mc.Example.Test")));
assertEq("the root itself routes nowhere", null, name(reg.resolveByHost("mc.example.test")));
@@ -64,9 +102,9 @@ public final class ServerRegistryTest {
net.registrations.clear();
reg.refresh(List.of(
- view("login", "login", true, "10.43.0.1:25565"),
- view("lobby", "lobby", true, "10.43.0.2:25565"),
- view("fresh", "fresh", true, "10.43.0.8:25565")));
+ up("login", "login", "10.43.0.1:25565"),
+ up("lobby", "lobby", "10.43.0.2:25565"),
+ up("fresh", "fresh", "10.43.0.8:25565")));
assertEq("a vanished server is dropped", List.of("-alpha"), List.copyOf(net.registrations));
assertEq("... from the views", false, reg.isManaged("alpha"));
assertEq("... and from host routing", null, name(reg.resolveByHost("alpha.mc.example.test")));
@@ -74,18 +112,18 @@ public final class ServerRegistryTest {
// A server whose subdomain changed answers on the new one only.
reg.refresh(List.of(
- view("login", "login", true, "10.43.0.1:25565"),
- view("lobby", "lobby", true, "10.43.0.2:25565"),
- view("fresh", "renamed", true, "10.43.0.8:25565")));
+ up("login", "login", "10.43.0.1:25565"),
+ up("lobby", "lobby", "10.43.0.2:25565"),
+ up("fresh", "renamed", "10.43.0.8:25565")));
assertEq("new subdomain", "fresh", name(reg.resolveByHost("renamed.mc.example.test")));
assertEq("old subdomain let go", null, name(reg.resolveByHost("fresh.mc.example.test")));
// A subdomain handed to another server routes to the new holder.
reg.refresh(List.of(
- view("login", "login", true, "10.43.0.1:25565"),
- view("lobby", "lobby", true, "10.43.0.2:25565"),
- view("other", "renamed", true, "10.43.0.5:25565"),
- view("fresh", "fresh", true, "10.43.0.8:25565")));
+ up("login", "login", "10.43.0.1:25565"),
+ up("lobby", "lobby", "10.43.0.2:25565"),
+ up("other", "renamed", "10.43.0.5:25565"),
+ up("fresh", "fresh", "10.43.0.8:25565")));
assertEq("subdomain moved", "other", name(reg.resolveByHost("renamed.mc.example.test")));
assertEq("subdomain taken back", "fresh", name(reg.resolveByHost("fresh.mc.example.test")));
@@ -104,9 +142,16 @@ public final class ServerRegistryTest {
System.out.println("ServerRegistryTest OK (" + checks + " checks)");
}
- private static ServerView view(String name, String sub, boolean ready, String addr) {
- return new ServerView(name, sub, ready ? "Running" : "Stopped", ready, "ownerOnly",
- "Running", "ClusterIP", addr, 0, 20);
+ private static ServerView up(String name, String sub, String addr) {
+ return new ServerView(name, sub, "Running", true, "ownerOnly", "Running", "direct", addr, 0, 20);
+ }
+
+ private static ServerView down(String name, String sub) {
+ return new ServerView(name, sub, "Stopped", false, "ownerOnly", "Stopped", "fallback", "login", 0, 0);
+ }
+
+ private static ServerView unstarted(String name, String sub) {
+ return new ServerView(name, sub, "", false, "ownerOnly", "Stopped", null, null, 0, 0);
}
private static String name(Optional