Docker-Warteschlange: Update-Check per RCON statt nur per Logzeile erkennen

Die Zeile 'Starting CS2 Dedicated Server' rutscht nach dem Serverstart
schnell aus den letzten Logzeilen. Der nächste Match-Container wartete
dadurch bis zur Zeitgrenze (20 min). Jetzt gilt ein Container als fertig,
sobald er per RCON antwortet; die Logzeile bleibt als zweite Erkennung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SPP UserandClaude Opus 5.5 committed 2026-10-25 22:16:57 +01:00
1 parent 7c74c6c4eb
commit 6ee2de5a46
2 files changed
+44 -6

No files matched your search

+20 -5
View File
@@ -62,18 +62,24 @@ export class DockerServerAdapter {
release(); release();
throw err; throw err;
} }
this.waitForUpdateCheck(result.handle.container).finally(release); this.waitForUpdateCheck(result.handle).finally(release);
return result; return result;
} }
/** Wartet, bis der Container SteamCMD abgeschlossen hat, beendet ist oder die Startzeit abläuft. */ /**
async waitForUpdateCheck(container, { pollMs = 3000 } = {}) { * Wartet, bis der Container SteamCMD abgeschlossen hat, beendet ist oder die Startzeit abläuft.
* Fertig ist er, sobald der Spielserver per RCON antwortet (RCON gibt es erst nach SteamCMD) oder
* die Startzeile im Log steht. Nur auf das Log zu schauen reicht nicht: Der Server schreibt danach
* so viel, dass die Zeile schnell aus den letzten Zeilen herausfällt.
*/
async waitForUpdateCheck(handle, { pollMs = 3000 } = {}) {
const deadline = Date.now() + this.options.startTimeoutMinutes * 60_000; const deadline = Date.now() + this.options.startTimeoutMinutes * 60_000;
while (Date.now() < deadline) { while (Date.now() < deadline) {
try { try {
const state = await this.docker(['inspect', '-f', '{{.State.Status}}', container], 15_000); const state = await this.docker(['inspect', '-f', '{{.State.Status}}', handle.container], 15_000);
if (state !== 'running' && state !== 'created') return; if (state !== 'running' && state !== 'created') return;
const logs = await this.docker(['logs', '--tail', '200', container], 15_000); if (await this.isReachable(handle)) return;
const logs = await this.docker(['logs', '--tail', '2000', handle.container], 15_000);
if (logs.includes(UPDATE_DONE_MARKER)) return; if (logs.includes(UPDATE_DONE_MARKER)) return;
} catch { } catch {
return; // Container weg – nicht ewig blockieren return; // Container weg – nicht ewig blockieren
@@ -82,6 +88,15 @@ export class DockerServerAdapter {
} }
} }
async isReachable(handle) {
try {
await rconStatus(this.target(handle));
return true;
} catch {
return false;
}
}
async launch({ matchId, mode, secrets }) { async launch({ matchId, mode, secrets }) {
const port = this.allocatePort(); const port = this.allocatePort();
const tvPort = port + 1; const tvPort = port + 1;
+24 -1
View File
@@ -28,6 +28,7 @@ const envOf = (args) => Object.fromEntries(args.flatMap((a, i) => (args[i - 1] =
test('Ohne GSLT startet der Container im LAN-Modus, mit GSLT mit Token', async () => { test('Ohne GSLT startet der Container im LAN-Modus, mit GSLT mit Token', async () => {
const lan = fakeDocker(); const lan = fakeDocker();
const a = new DockerServerAdapter(options, { getUsedPorts: () => new Set(), runDocker: lan.run }); const a = new DockerServerAdapter(options, { getUsedPorts: () => new Set(), runDocker: lan.run });
a.isReachable = async () => false;
const r = await a.start({ matchId: 'abc_DEF-1', mode: 'wingman', secrets }); const r = await a.start({ matchId: 'abc_DEF-1', mode: 'wingman', secrets });
lan.finishUpdate(r.handle.container); lan.finishUpdate(r.handle.container);
const runArgs = lan.calls.find((c) => c[0] === 'run'); const runArgs = lan.calls.find((c) => c[0] === 'run');
@@ -43,6 +44,7 @@ test('Ohne GSLT startet der Container im LAN-Modus, mit GSLT mit Token', async (
const tok = fakeDocker(); const tok = fakeDocker();
const b = new DockerServerAdapter({ ...options, gslt: 'TOKEN1' }, { getUsedPorts: () => new Set(), runDocker: tok.run }); const b = new DockerServerAdapter({ ...options, gslt: 'TOKEN1' }, { getUsedPorts: () => new Set(), runDocker: tok.run });
b.isReachable = async () => false;
const r2 = await b.start({ matchId: 'x', mode: 'competitive', secrets }); const r2 = await b.start({ matchId: 'x', mode: 'competitive', secrets });
tok.finishUpdate(r2.handle.container); tok.finishUpdate(r2.handle.container);
const env2 = envOf(tok.calls.find((c) => c[0] === 'run')); const env2 = envOf(tok.calls.find((c) => c[0] === 'run'));
@@ -56,7 +58,8 @@ test('Zweiter Container startet erst, wenn der erste seinen Update-Check abgesch
const a = new DockerServerAdapter(options, { getUsedPorts: () => used, runDocker: d.run }); const a = new DockerServerAdapter(options, { getUsedPorts: () => used, runDocker: d.run });
// Wartezeit zwischen Prüfungen für den Test verkürzen // Wartezeit zwischen Prüfungen für den Test verkürzen
const original = a.waitForUpdateCheck.bind(a); const original = a.waitForUpdateCheck.bind(a);
a.waitForUpdateCheck = (name) => original(name, { pollMs: 10 }); a.waitForUpdateCheck = (handle) => original(handle, { pollMs: 10 });
a.isReachable = async () => false;
const first = await a.start({ matchId: 'm1', mode: 'competitive', secrets }); const first = await a.start({ matchId: 'm1', mode: 'competitive', secrets });
used.add(first.port).add(first.tvPort); used.add(first.port).add(first.tvPort);
@@ -73,3 +76,23 @@ test('Zweiter Container startet erst, wenn der erste seinen Update-Check abgesch
assert.equal(r2.port, 27102, 'zweiter Container bekommt die nächsten Ports'); assert.equal(r2.port, 27102, 'zweiter Container bekommt die nächsten Ports');
d.finishUpdate(r2.handle.container); d.finishUpdate(r2.handle.container);
}); });
test('Warteschlange gibt frei, sobald der Server per RCON antwortet – auch ohne Startzeile im Log', async () => {
const d = fakeDocker();
const used = new Set();
const a = new DockerServerAdapter(options, { getUsedPorts: () => used, runDocker: d.run });
const original = a.waitForUpdateCheck.bind(a);
a.waitForUpdateCheck = (handle) => original(handle, { pollMs: 10 });
const reachable = new Set();
a.isReachable = async (handle) => reachable.has(handle.container);
const first = await a.start({ matchId: 'r1', mode: 'wingman', secrets });
used.add(first.port).add(first.tvPort);
const second = a.start({ matchId: 'r2', mode: 'wingman', secrets });
await new Promise((r) => setTimeout(r, 50));
assert.equal(d.calls.filter((c) => c[0] === 'run').length, 1, 'wartet noch');
reachable.add(first.handle.container); // Log ohne Startzeile, aber RCON antwortet
const r2 = await second;
assert.equal(d.calls.filter((c) => c[0] === 'run').length, 2);
reachable.add(r2.handle.container);
});