Merge fix/start-warteschlange in main
This commit is contained in:
commit
6a922f80de
6 files changed
+83
-8
No files matched your search
@@ -29,6 +29,7 @@ export function createApp(config, { adapter: adapterOverride, logger = console }
|
||||
let manager;
|
||||
const adapter = adapterOverride ?? createServerAdapter(config, {
|
||||
getUsedPorts: (host) => (host ? manager.usedPortsOn(host) : manager.usedPorts()),
|
||||
getStarting: (host, exclude) => manager.startingOn(host, exclude),
|
||||
});
|
||||
manager = new ServerManager({ db, service, adapter, config, logger });
|
||||
manager.init();
|
||||
|
||||
@@ -555,6 +555,9 @@ export class MatchService extends EventEmitter {
|
||||
if (!server || !safeEqual(String(key ?? ''), server.config_key)) {
|
||||
throw new HttpError(403, 'Unbekannter Log-Schlüssel', 'forbidden');
|
||||
}
|
||||
// Erst zählen, wenn die Map eingerichtet ist. Während des Einrichtens kommen noch Zeilen vom vorigen
|
||||
// Spiel auf dem Server (z. B. dem Ende der Messerrunde), die nicht zur neuen Map gehören.
|
||||
if (server.status !== 'configured') return;
|
||||
this.applyLiveLog(matchId, splitLogBody(body));
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,8 @@ export class DockerServerAdapter {
|
||||
this.runDocker = runDocker; // nur für Tests
|
||||
this.lanMode = !options.gslt;
|
||||
this.startQueue = Promise.resolve();
|
||||
// Starts, die gerade warten oder deren Container noch SteamCMD laufen lässt
|
||||
this.pendingStarts = 0;
|
||||
if (!options.host) throw new Error('SERVER_ADAPTER=docker benötigt CS2_PUBLIC_HOST');
|
||||
}
|
||||
|
||||
@@ -169,8 +171,13 @@ export class DockerServerAdapter {
|
||||
async start(args) {
|
||||
// In die Warteschlange: erst starten, wenn der vorige Container seinen Update-Check hinter sich hat.
|
||||
const previous = this.startQueue;
|
||||
let release;
|
||||
this.startQueue = new Promise((resolve) => { release = resolve; });
|
||||
let releaseQueue;
|
||||
this.startQueue = new Promise((resolve) => { releaseQueue = resolve; });
|
||||
this.pendingStarts += 1;
|
||||
const release = () => {
|
||||
this.pendingStarts -= 1;
|
||||
releaseQueue();
|
||||
};
|
||||
await previous;
|
||||
let result;
|
||||
try {
|
||||
@@ -368,10 +375,11 @@ export class DockerServerAdapter {
|
||||
* immer an denselben Host. Ohne CS2_DOCKER_HOSTS gibt es genau einen Host: den bisherigen.
|
||||
*/
|
||||
export class DockerHostPool {
|
||||
constructor(options, { getUsedPorts, runDocker } = {}) {
|
||||
constructor(options, { getUsedPorts, getStarting, runDocker } = {}) {
|
||||
this.name = 'docker';
|
||||
this.simulated = false;
|
||||
this.depsGetUsedPorts = getUsedPorts;
|
||||
this.depsGetStarting = getStarting;
|
||||
this.runDocker = runDocker;
|
||||
this.options = options;
|
||||
this.diskCache = new Map();
|
||||
@@ -594,19 +602,31 @@ export class DockerHostPool {
|
||||
return new Set(this.files.running ? this.files.results.filter((r) => r.state === 'running').map((r) => r.name) : []);
|
||||
}
|
||||
|
||||
pickHost() {
|
||||
/**
|
||||
* Wie viele Server auf einem Host gerade hochfahren: Starts in der Warteschlange dieses Prozesses, und
|
||||
* (nach einem Neustart der App) Server, die laut Datenbank noch starten. Ein Container lädt beim Start
|
||||
* per SteamCMD, nach einem großen CS2-Update dauert das lange; weitere Starts dort müssten so lange warten.
|
||||
*/
|
||||
startingOn(host, exclude) {
|
||||
const queued = this.adapters.get(host.name)?.pendingStarts ?? 0;
|
||||
const stored = this.depsGetStarting?.(host.publicHost, exclude) ?? 0;
|
||||
return Math.max(queued, stored);
|
||||
}
|
||||
|
||||
pickHost(exclude) {
|
||||
const updating = this.updatingHosts();
|
||||
const best = this.hosts
|
||||
.filter((h) => !updating.has(h.name))
|
||||
.map((h) => ({ host: h, free: this.freeSlots(h) }))
|
||||
.map((h) => ({ host: h, free: this.freeSlots(h), starting: this.startingOn(h, exclude) }))
|
||||
.filter((x) => x.free > 0)
|
||||
.sort((a, b) => b.free - a.free)[0];
|
||||
// Zuerst Hosts, auf denen nichts hochfährt, dann die mit den meisten freien Plätzen
|
||||
.sort((a, b) => a.starting - b.starting || b.free - a.free)[0];
|
||||
if (!best) throw new Error(updating.size ? 'Kein Docker-Host frei, auf den anderen werden gerade die Spieldateien aktualisiert' : 'Kein Docker-Host mit freien Ports');
|
||||
return best.host;
|
||||
}
|
||||
|
||||
async start(args) {
|
||||
return this.adapters.get(this.pickHost().name).start(args);
|
||||
return this.adapters.get(this.pickHost(args?.matchId).name).start(args);
|
||||
}
|
||||
|
||||
adapterFor(handle) {
|
||||
|
||||
@@ -258,6 +258,14 @@ export class ServerManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Server (Matches und Messer-Server), die auf einem Host gerade hochfahren. `exclude`: der, der gerade startet. */
|
||||
startingOn(host, exclude = null) {
|
||||
return this.db.prepare(`
|
||||
SELECT (SELECT COUNT(*) FROM servers WHERE host = @host AND status = 'starting' AND handle IS NOT NULL AND match_id IS NOT @exclude)
|
||||
+ (SELECT COUNT(*) FROM knife_servers WHERE host = @host AND status = 'starting' AND handle IS NOT NULL AND id IS NOT @exclude) AS n
|
||||
`).get({ host, exclude }).n;
|
||||
}
|
||||
|
||||
/** Ports, die auf einem bestimmten Docker-Host belegt sind. */
|
||||
usedPortsOn(host) {
|
||||
const used = new Set();
|
||||
@@ -500,7 +508,12 @@ export class ServerManager {
|
||||
this.simulator.stop(matchId);
|
||||
this.service.resetLiveScore(matchId, seriesMap.number);
|
||||
// Lief vorher die Messerrunde auf diesem Server, deren Einstellungen zurücknehmen
|
||||
if (row.knife_at) plan.afterMapChange = [...KNIFE_UNDO_COMMANDS, ...plan.afterMapChange];
|
||||
if (row.knife_at) {
|
||||
// Eine noch laufende Messerrunde zuerst anhalten: sonst endet sie (mp_maxrounds 1) mitten im Einrichten,
|
||||
// und ihr Ergebnis würde als Ende der ersten Map gezählt
|
||||
plan.beforeMapChange = ['mp_warmup_pausetimer 1', 'mp_warmup_start', ...plan.beforeMapChange];
|
||||
plan.afterMapChange = [...KNIFE_UNDO_COMMANDS, ...plan.afterMapChange];
|
||||
}
|
||||
try {
|
||||
await this.adapter.configure(JSON.parse(row.handle), { map: seriesMap.map, ...plan });
|
||||
} catch (err) {
|
||||
|
||||
@@ -753,7 +753,19 @@ test('Messerrunde auf dem eigenen Match-Server: Verbindungsdaten in der Lobby, d
|
||||
const capA = snap.teams.A.players.find((p) => p.isCaptain)?.nickname ?? 'a1';
|
||||
const capB = snap.teams.B.players.find((p) => p.isCaptain)?.nickname ?? 'b1';
|
||||
await playVeto(call, id, { A: tokens[capA], B: tokens[capB] });
|
||||
// Das Ende der Messerrunde kommt während des Einrichtens an: darf nicht als Ende von Map 1 zählen
|
||||
await waitFor(() => app.manager.row(id).status === 'configuring', { label: 'Map 1 wird eingerichtet' });
|
||||
const firstMap = app.service.seriesMaps(id)[0].map;
|
||||
const knifeEnd = ['Team "CT" triggered "SFUI_Notice_Target_Saved" (CT "1") (T "0")', 'World triggered "Round_End"',
|
||||
`Game Over: scrimcomp2v2 ${firstMap} score 1:0 after 2 min`].join('\n');
|
||||
const key = app.manager.row(id).config_key;
|
||||
assert.equal((await fetch(`http://127.0.0.1:${app.server.address().port}/api/logs/${id}/${key}`, { method: 'POST', body: knifeEnd })).status, 200);
|
||||
await waitFor(() => app.manager.row(id).status === 'configured', { label: 'Match eingerichtet' });
|
||||
const map1 = app.service.seriesMaps(id)[0];
|
||||
assert.notEqual(map1.status, 'done', 'Messerrunde zählt nicht als Map-Ende');
|
||||
assert.equal(map1.score_a, 0);
|
||||
assert.equal(app.service.getMatch(id).current_map_number, 1);
|
||||
assert.deepEqual(plans.at(-1).beforeMapChange.slice(0, 2), ['mp_warmup_pausetimer 1', 'mp_warmup_start'], 'laufende Messerrunde wird zuerst angehalten');
|
||||
const row = app.manager.row(id);
|
||||
assert.equal(row.handle, handle, 'derselbe Server');
|
||||
assert.equal(row.knife_at, null);
|
||||
|
||||
@@ -107,3 +107,29 @@ test('Alter Handle ohne passenden Host-Namen wird über die Adresse dem richtige
|
||||
assert.equal(p.adapterFor({ hostName: 'default', rconHost: '10.0.0.2' }).options.name, 'b');
|
||||
assert.throws(() => p.adapterFor({ hostName: 'weg', rconHost: '10.9.9.9' }), /nicht \(mehr\) konfiguriert/);
|
||||
});
|
||||
|
||||
test('Neue Server meiden Hosts, auf denen gerade ein Server hochfährt (SteamCMD)', async () => {
|
||||
const starting = { '10.0.0.2': 1 };
|
||||
const p = new DockerHostPool(options([hostA, hostB]), {
|
||||
getUsedPorts: () => new Set(),
|
||||
getStarting: (publicHost, exclude) => (exclude === 'self' ? 0 : starting[publicHost] ?? 0),
|
||||
runDocker: fakeDocker().run,
|
||||
});
|
||||
// b hat mehr Plätze, aber dort lädt gerade ein Container: a gewinnt
|
||||
assert.equal(p.pickHost().name, 'a');
|
||||
// Der hochfahrende Server selbst zählt nicht gegen seinen eigenen Neustart
|
||||
assert.equal(p.pickHost('self').name, 'b');
|
||||
// Warteschlange im Prozess zählt genauso
|
||||
starting['10.0.0.2'] = 0;
|
||||
p.adapters.get('b').pendingStarts = 1;
|
||||
assert.equal(p.pickHost().name, 'a');
|
||||
// Starts zählen hoch und nach dem Update-Check wieder herunter
|
||||
const a = p.adapters.get('a');
|
||||
let done;
|
||||
a.waitForUpdateCheck = () => new Promise((r) => { done = r; });
|
||||
await a.start({ matchId: 'm1', mode: 'competitive', secrets });
|
||||
assert.equal(a.pendingStarts, 1);
|
||||
done();
|
||||
await new Promise((r) => setImmediate(r));
|
||||
assert.equal(a.pendingStarts, 0);
|
||||
});
|
||||
Reference in new issue
Block a user