Offene Lobbys nach 3 Stunden samt Server löschen (LOBBY_MAX_HOURS)

Der Aufräum-Lauf löscht Lobbys, deren Veto nie gestartet wurde, nach LOBBY_MAX_HOURS (Standard 3, 0 = nie) seit dem Erstellen und stoppt ihren Server. Matches im Veto, im Spiel oder abgeschlossen bleiben unberührt.
This commit is contained in:
mregosar committed 2026-11-01 19:54:09 +01:00
1 parent 5503995791
commit bd022e043e
5 files changed
+49 -2

No files matched your search

+2
View File
@@ -105,6 +105,8 @@ export function loadConfig(overrides = {}) {
serverAdapter: str('SERVER_ADAPTER', 'demo'),
idleTimeoutMinutes: int('SERVER_IDLE_TIMEOUT_MINUTES', 30),
maxLifetimeHours: int('SERVER_MAX_LIFETIME_HOURS', 8),
// Offene Lobbys (Veto nie gestartet) werden nach so vielen Stunden gelöscht. 0 = nie
lobbyMaxHours: int('LOBBY_MAX_HOURS', 3),
statusPollSeconds: int('SERVER_STATUS_POLL_SECONDS', 5),
reaperIntervalSeconds: int('SERVER_REAPER_INTERVAL_SECONDS', 60),
// Frist pro Veto-Schritt in Sekunden, danach führt das Backend den Schritt automatisch aus. 0 = keine Frist
+17 -2
View File
@@ -132,12 +132,13 @@ export class ServerManager {
}
/** Löscht ein Match samt Server-Datensatz. Ein laufender Server wird vorher freigegeben. */
async deleteMatch(matchId) {
async deleteMatch(matchId, reason = 'Match vom Admin gelöscht') {
this.service.getMatch(matchId);
clearTimeout(this.autoAdvance.get(matchId));
this.autoAdvance.delete(matchId);
this.simulator.stop(matchId);
await this.stop(matchId, 'Match vom Admin gelöscht');
this.knifeAttempts.delete(matchId);
await this.stop(matchId, reason);
this.db.prepare('DELETE FROM matches WHERE id = ?').run(matchId);
this.service.emit('changed', matchId);
}
@@ -687,8 +688,22 @@ export class ServerManager {
// ---------- Automatisches Herunterfahren ----------
/** Lobbys, die länger als LOBBY_MAX_HOURS offen sind (Veto nie gestartet), samt Server löschen. */
async deleteStaleLobbies(now = Date.now()) {
const hours = this.config.lobbyMaxHours;
if (!hours) return 0;
const stale = this.db.prepare("SELECT id FROM matches WHERE status = 'lobby' AND created_at < ?").all(now - hours * 3_600_000);
for (const { id } of stale) {
if (this.busy.has(id)) continue;
this.logger.log?.(`[aufräumen] Lobby ${id} gelöscht (länger als ${hours} h offen)`);
await this.deleteMatch(id, `Lobby länger als ${hours} h offen`).catch((err) => this.logger.error(err));
}
return stale.length;
}
async reap(now = Date.now()) {
this.service.archiveClosed(now);
await this.deleteStaleLobbies(now);
const timeoutMs = this.config.idleTimeoutMinutes * 60_000;
const rows = this.db.prepare(`SELECT * FROM servers WHERE status IN (${ACTIVE.map(() => '?').join(',')})`).all(...ACTIVE);
for (const row of rows) {
+26
View File
@@ -537,6 +537,8 @@ test('Archiv: nur abgeschlossene Matches, eigene Liste, automatisch nach Frist',
// Automatisch: abgebrochenes Match, älter als 3 Tage
await call('POST', `/admin/matches/${old.id}/cancel`, { token });
assert.equal(app.service.archiveClosed(Date.now() + 2 * 86_400_000), 0, 'nach 2 Tagen noch nicht');
// Hier geht es nur ums Archiv: das Löschen alter Lobbys (eigener Test) bleibt aus
app.manager.config.lobbyMaxHours = 0;
await app.manager.reap(Date.now() + 4 * 86_400_000);
assert.ok(app.service.getMatch(old.id).archived_at > 0, 'nach 4 Tagen archiviert');
assert.ok(app.service.getMatch(done.id).archived_at > 0, 'zurückgeholtes Match wird nach Ablauf der Frist wieder archiviert');
@@ -805,3 +807,27 @@ test('Messerrunde: Einrichten wird bei Fehlern von selbst wiederholt, danach kan
await close();
}
});
test('Offene Lobbys werden nach LOBBY_MAX_HOURS samt Server gelöscht, laufende Matches nicht', async () => {
const { app, call, close } = await setup();
try {
const make = async () => (await call('POST', '/matches', { body: { teamA: 'Alpha', teamB: 'Bravo', mode: 'wingman', format: 'bo1', vetoStart: 'A' } })).body.id;
const old = await make();
const fresh = await make();
const { id: running, org } = await readyWingmanMatch(call, 'bo1');
assert.equal((await call('POST', `/matches/${running}/veto/start`, { token: org })).status, 200);
const ago = (h) => Date.now() - h * 3_600_000;
app.service.db.prepare('UPDATE matches SET created_at = ? WHERE id IN (?, ?)').run(ago(4), old, running);
app.service.db.prepare('UPDATE matches SET created_at = ? WHERE id = ?').run(ago(2), fresh);
await app.manager.reap();
const left = app.service.db.prepare('SELECT id FROM matches').all().map((r) => r.id);
assert.ok(!left.includes(old), 'alte Lobby gelöscht');
assert.ok(left.includes(fresh), 'Lobby unter 3 h bleibt');
assert.ok(left.includes(running), 'Match im Veto bleibt');
assert.equal(app.service.db.prepare('SELECT COUNT(*) AS n FROM servers WHERE match_id = ?').get(old).n, 0, 'Server-Datensatz weg');
assert.equal((await call('GET', `/matches/${old}`)).status, 404);
} finally {
await close();
}
});