Preserve server status when batch probes miss the deadline

This commit is contained in:
2026-10-05 16:17:29 -04:00
parent ad63b3957d
commit ae703617af
7 changed files with 105 additions and 22 deletions
+8 -7
View File
@@ -32,7 +32,7 @@ import urllib.parse
import urllib.request
import uuid
VERSION = "1.4.12"
VERSION = "1.4.13"
LABEL = "io.electric-cherry"
MAX_FILE = 32 * 1024 * 1024
MANIFEST_MAX_ENTRIES = 10000
@@ -1368,7 +1368,7 @@ class Agent:
def probe(identity):
instance_id, generation = identity
target = {"instanceId": instance_id, "generation": generation}
reply = {"ok": False, "reason": "timeout", "text": ""}
reply = {"ok": False, "reason": "deadline", "text": ""}
# A batch does not hold every instance lock while one server waits.
# Bound concurrency across overlapping batches as well as within one.
lock = self.instance_lock(target)
@@ -1382,7 +1382,7 @@ class Agent:
if current.get("generation", generation) != generation or current.get("removed"):
reply = {"ok": False, "reason": "superseded", "text": ""}
elif time.monotonic() < deadline:
reply = self.rcon(instance_id, generation, "status", deadline=deadline)
reply = self.rcon(instance_id, generation, "status", deadline=deadline, status_attempts=1)
except (AgentError, OSError, ValueError, KeyError):
reply = {"ok": False, "reason": "unreachable", "text": ""}
finally:
@@ -1395,7 +1395,7 @@ class Agent:
results = list(executor.map(probe, identities))
return {"ok": all(row["reply"].get("ok") for row in results), "results": results}
def rcon(self, instance_id, generation, command, deadline=None):
def rcon(self, instance_id, generation, command, deadline=None, status_attempts=3):
if not isinstance(command, str) or not command or len(command) > 2048 or any(ch in command for ch in ("\x00", "\r", "\n")):
raise AgentError("Invalid RCON command")
saved = self.spec(instance_id, generation)
@@ -1407,11 +1407,12 @@ class Agent:
family = socket.AF_INET6 if ":" in host else socket.AF_INET
# Retry only the read-only occupancy query. A missing UDP reply must
# never cause commands with effects to be replayed.
for attempt in range(3 if command == "status" else 1):
attempts = status_attempts if command == "status" else 1
for attempt in range(attempts):
if deadline is not None and time.monotonic() >= deadline:
return {"ok": False, "reason": "timeout", "players": None, "text": ""}
return {"ok": False, "reason": "deadline" if attempt == 0 else "timeout", "players": None, "text": ""}
result = self.rcon_attempt(family, host, int(saved["port"]), password, command, **({"deadline": deadline} if deadline is not None else {}))
if result["ok"] or result.get("reason") not in ("timeout", "unreachable") or attempt == 2 or command != "status":
if result["ok"] or result.get("reason") not in ("timeout", "unreachable") or attempt == attempts - 1:
return result
time.sleep(min(0.25 * 2 ** attempt, max(0, deadline - time.monotonic())) if deadline is not None else 0.25 * 2 ** attempt)
+2 -2
View File
@@ -1,8 +1,8 @@
{
"version": "1.4.12",
"version": "1.4.13",
"protocolVersion": 1,
"platform": "debian-amd64",
"sha256": "1e1bf2f04c02f0c8d0db8e4dbf39fadddc39c8bd04db5b8db753b6fe9e04c315",
"sha256": "2f3449058cd4b3ab9e3ff8f541fa3c65ad2d94aabe20694cdec3a6e59c8a4fd6",
"updaterProtocolVersion": 1,
"updaterSha256": "066bb896726600ece9ce72697ce523c9a707cc9a621505931ba0892dec59d3c8"
}