fix(felis): 启动失败的服可在面板重试或停止,玩家入服时直接告知启动失败

This commit is contained in:
Lemon-miaow committed 2026-09-26 22:08:29 +08:00
1 parent c1025274e1
commit 22d1e834ad
28 files changed
+775 -75

No files matched your search

@@ -20,12 +20,14 @@ import java.util.Optional;
* backend or the wake lever.
*
* <p>This is the read-only, phase-aware subset of the responsibility: the MOTD is
* synthesized from the server's lifecycle (online / starting / sleeping) and its
* cached player counts. Mirroring each backend's <em>own</em> MOTD string (by
* pinging ready servers in the background and caching the result) is a richer
* synthesized from the server's lifecycle (online / starting / start failed /
* sleeping) and its cached player counts. Mirroring each backend's <em>own</em> MOTD
* string (by pinging ready servers in the background and caching the result) is a richer
* variant deferred to a later slice; nothing here ever pings a sleeping backend.
*/
public final class MotdResponder {
private static final String PHASE_FAILED = "Failed";
private final ServerRegistry registry;
MotdResponder(ServerRegistry registry) {
@@ -55,21 +57,33 @@ public final class MotdResponder {
}
// The server-list ping carries no client locale, so the MOTD status uses the
// both-languages-in-one-line pattern the modded /link clients share.
private static String statusLine(ServerView v) {
// both-languages-in-one-line pattern the modded /link clients share. A start
// that failed still holds desiredState Running, so it is read first: joining a
// server whose retries are spent wakes nothing, and one between retries is
// waiting out a backoff, which a "starting…" line hid behind a queue that ran out.
static String statusLine(ServerView v) {
if (v.ready()) {
return "在线 / online";
}
if (v.startGaveUp()) {
return "启动失败,等服主处理 / failed to start — the owner has to restart it";
}
if (PHASE_FAILED.equals(v.phase())) {
return "启动超时,稍后自动重试 / start timed out — retrying shortly";
}
if ("Running".equals(v.desiredState())) {
return "启动中… / starting…";
}
return "休眠中,加入即唤醒 / sleeping — join to wake";
}
private static NamedTextColor statusColor(ServerView v) {
static NamedTextColor statusColor(ServerView v) {
if (v.ready()) {
return NamedTextColor.GREEN;
}
if (v.startGaveUp()) {
return NamedTextColor.RED;
}
if ("Running".equals(v.desiredState())) {
return NamedTextColor.YELLOW;
}
@@ -505,11 +505,7 @@ public final class WaitingRouter {
// stopped the server — it says so and returns false.
private boolean stillComing(Player player, boolean zh, Waiter w, ServerView status, long now) {
if (status.startGaveUp()) {
player.sendMessage(Component.text(
zh ? "「" + w.serverName + "」启动失败,自动重试也已用完。服主可以在面板查看日志后重新启动。"
: "« " + w.serverName + " » failed to start and its automatic retries are spent. "
+ "The owner can check its log in the panel and start it again.",
NamedTextColor.RED));
tellStartFailed(player, zh, w.serverName);
return false;
}
if (DESIRED_STOPPED.equalsIgnoreCase(status.desiredState())) {
@@ -655,6 +651,12 @@ public final class WaitingRouter {
NamedTextColor.YELLOW));
return;
}
if ("start_failed".equals(e.errorCode())) {
// The last start failed with its retries spent. The join does
// not buy another round of them, so there is nothing to wait for.
tellStartFailed(player, zh, serverName);
return;
}
logWakeFailure(player, serverName, zh, e);
return;
case 429:
@@ -688,6 +690,16 @@ public final class WaitingRouter {
waiting.put(id, new Waiter(serverName, clock.getAsLong(), fromMenu));
}
// The server's start failed and its automatic retries are spent: nothing more is
// coming until a person starts it again from the panel.
private static void tellStartFailed(Player player, boolean zh, String serverName) {
player.sendMessage(Component.text(
zh ? "「" + serverName + "」启动失败,自动重试也已用完。服主可以在面板查看日志后重新启动。"
: "« " + serverName + " » failed to start and its automatic retries are spent. "
+ "The owner can check its log in the panel and start it again.",
NamedTextColor.RED));
}
private void logWakeFailure(Player player, String serverName, boolean zh, LinkException e) {
log.warn("Felis: wake {} failed (status={}): {}", serverName, e.statusCode(), e.getMessage());
player.sendMessage(Component.text(
@@ -2,6 +2,8 @@ package best.lolicon.felis.velocity;
import best.lolicon.felis.link.ServerView;
import net.kyori.adventure.text.format.NamedTextColor;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Optional;
@@ -139,6 +141,24 @@ public final class ServerRegistryTest {
assertAddr("a non-numeric port is not a port", "backend:x", 25565,
ServerRegistry.parseAddress("backend:x"));
// The server-list MOTD reads a failed start before desiredState Running,
// which a failed start still holds.
assertEq("motd: up", "在线 / online", MotdResponder.statusLine(up("a", "a", "10.43.0.1:25565")));
assertEq("motd: starting", "启动中… / starting…", MotdResponder.statusLine(
new ServerView("a", "a", "Starting", false, "ownerOnly", "Running", "fallback", "login", 0, 0)));
ServerView backoff = new ServerView("a", "a", "Failed", false, "ownerOnly", "Running",
"fallback", "login", 0, 0, false, 1, false);
assertEq("motd: between retries", "启动超时,稍后自动重试 / start timed out — retrying shortly",
MotdResponder.statusLine(backoff));
assertEq("motd: between retries is yellow", NamedTextColor.YELLOW, MotdResponder.statusColor(backoff));
ServerView gaveUp = new ServerView("a", "a", "Failed", false, "ownerOnly", "Running",
"fallback", "login", 0, 0, false, 3, true);
assertEq("motd: retries spent", "启动失败,等服主处理 / failed to start — the owner has to restart it",
MotdResponder.statusLine(gaveUp));
assertEq("motd: retries spent is red", NamedTextColor.RED, MotdResponder.statusColor(gaveUp));
assertEq("motd: sleeping", "休眠中,加入即唤醒 / sleeping — join to wake",
MotdResponder.statusLine(down("a", "a")));
System.out.println("ServerRegistryTest OK (" + checks + " checks)");
}
@@ -219,6 +219,7 @@ public final class WaitingRouterTest {
String[][] cases = {
{"403 forbidden", "You're not allowed to start « gamma »", null},
{"409 maintenance_in_progress", "« gamma » is under maintenance", null},
{"409 start_failed", "« gamma » failed to start and its automatic retries are spent", null},
{"409 conflict", "Couldn't start « gamma » right now", "wake gamma failed (status=409)"},
{"503 at_capacity", "The cluster is at capacity right now", null},
{"503 unavailable", "Couldn't start « gamma » right now", "wake gamma failed (status=503)"},