- Neue Server meiden Hosts, auf denen gerade ein Container per SteamCMD lädt (Warteschlange im Prozess und laufende Starts laut Datenbank). Vorher warteten alle Starts hinter einem langen Download, obwohl andere Hosts frei waren. - Vor dem Einrichten des Matches wird eine laufende Messerrunde in die Aufwärmphase gesetzt, und Logzeilen zählen erst, wenn die Map eingerichtet ist. Vorher wurde das Ende der Messerrunde als Ende von Map 1 gewertet und die nächste Map automatisch geladen.
136 lines
5.9 KiB
JavaScript
136 lines
5.9 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { DockerHostPool } from '../src/servers/dockerAdapter.js';
|
|
|
|
const secrets = { rconPassword: 'Rcon123', serverPassword: 'Pw123', tvPassword: 'Tv123' };
|
|
|
|
const hostA = { name: 'a', publicHost: '10.0.0.1', portRangeStart: 27100, portRangeEnd: 27103 }; // 2 Paare
|
|
const hostB = { name: 'b', publicHost: '10.0.0.2', portRangeStart: 27100, portRangeEnd: 27139 }; // 20 Paare
|
|
|
|
function options(hosts) {
|
|
return {
|
|
bin: 'docker', image: 'joedwards32/cs2:latest', host: '10.0.0.9', rconHost: '10.0.0.9',
|
|
portRangeStart: 27015, portRangeEnd: 27115, gslt: '', volume: 'cs2-mapveto-gamefiles', extraArgs: '',
|
|
startTimeoutMinutes: 1, hosts,
|
|
};
|
|
}
|
|
|
|
/** Docker-Aufrufe merken, Container gelten sofort als fertig. */
|
|
function fakeDocker() {
|
|
const calls = [];
|
|
const run = async (args) => {
|
|
calls.push(args);
|
|
if (args[0] === 'inspect') return 'running';
|
|
if (args[0] === 'logs') return 'Starting CS2 Dedicated Server';
|
|
return '';
|
|
};
|
|
return { run, calls };
|
|
}
|
|
|
|
/** Nur die Konfiguration der Hosts, ohne auf echte Container zu warten. */
|
|
function pool(hosts, used = {}) {
|
|
const docker = fakeDocker();
|
|
const p = new DockerHostPool(options(hosts), {
|
|
getUsedPorts: (publicHost) => new Set(used[publicHost] ?? []),
|
|
runDocker: docker.run,
|
|
});
|
|
for (const a of p.adapters.values()) a.waitForUpdateCheck = async () => {};
|
|
return { pool: p, calls: docker.calls };
|
|
}
|
|
|
|
test('Ohne CS2_DOCKER_HOSTS gibt es genau einen Host, wie bisher', () => {
|
|
const { pool: p } = pool(null);
|
|
assert.equal(p.hosts.length, 1);
|
|
assert.equal(p.hosts[0].publicHost, '10.0.0.9');
|
|
});
|
|
|
|
test('Ein Match landet auf dem Host mit den meisten freien Ports', async () => {
|
|
const { pool: p } = pool([hostA, hostB], { '10.0.0.1': [27100] }); // A hat ein Paar belegt, B ist frei
|
|
const r = await p.start({ matchId: 'm1', mode: 'competitive', secrets });
|
|
assert.equal(r.handle.hostName, 'b');
|
|
assert.equal(r.host, '10.0.0.2');
|
|
});
|
|
|
|
test('Ein voller Host wird übersprungen', async () => {
|
|
const fullA = Array.from({ length: 4 }, (_, i) => 27100 + i); // alle Ports von A belegt
|
|
const { pool: p } = pool([hostA, hostB], { '10.0.0.1': fullA });
|
|
const r = await p.start({ matchId: 'm2', mode: 'wingman', secrets });
|
|
assert.equal(r.handle.hostName, 'b');
|
|
});
|
|
|
|
test('Sind alle Hosts voll, gibt es einen klaren Fehler', async () => {
|
|
const fullA = Array.from({ length: 4 }, (_, i) => 27100 + i);
|
|
const fullB = Array.from({ length: 40 }, (_, i) => 27100 + i);
|
|
const { pool: p } = pool([hostA, hostB], { '10.0.0.1': fullA, '10.0.0.2': fullB });
|
|
await assert.rejects(p.start({ matchId: 'm3', mode: 'wingman', secrets }), /Kein Docker-Host mit freien Ports/);
|
|
});
|
|
|
|
test('Status, Stopp und Befehle gehen an den Host des Matches', async () => {
|
|
const { pool: p, calls } = pool([hostA, hostB], { '10.0.0.1': [27100] });
|
|
const r = await p.start({ matchId: 'm4', mode: 'competitive', secrets });
|
|
calls.length = 0;
|
|
await p.stop(r.handle);
|
|
assert.deepEqual(calls.at(-1)[0], 'rm');
|
|
assert.equal(calls.at(-1).at(-1), r.handle.container);
|
|
// Ein Handle von einem Host, der weder per Name noch per Adresse bekannt ist, wird abgelehnt
|
|
await assert.rejects(p.status({ ...r.handle, hostName: 'gelöscht', rconHost: '10.9.9.9' }), /nicht \(mehr\) konfiguriert/);
|
|
});
|
|
|
|
test('Alte Handles ohne Host-Namen laufen auf dem ersten Host', () => {
|
|
const { pool: p } = pool([hostA, hostB]);
|
|
assert.equal(p.adapterFor({ container: 'x' }), p.adapters.get('a'));
|
|
});
|
|
|
|
test('Auslastung pro Host: Serverplätze insgesamt und belegt', () => {
|
|
const { pool: p } = pool([hostA, hostB], { '10.0.0.1': [27100] });
|
|
assert.deepEqual(p.hostStats(), [
|
|
{ name: 'a', publicHost: '10.0.0.1', total: 4, used: 1 },
|
|
{ name: 'b', publicHost: '10.0.0.2', total: 40, used: 0 },
|
|
]);
|
|
});
|
|
test('df-Ausgabe wird in Gesamt- und freien Speicher zerlegt', async () => {
|
|
const { parseDf } = await import('../src/servers/dockerAdapter.js');
|
|
assert.deepEqual(parseDf('/dev/sda1 1000000000000 400000000000 600000000000 40% /var/lib/docker'), {
|
|
totalBytes: 1000000000000,
|
|
freeBytes: 600000000000,
|
|
});
|
|
assert.equal(parseDf('kaputt'), null);
|
|
});
|
|
test('Alter Handle ohne passenden Host-Namen wird über die Adresse dem richtigen Host zugeordnet', async () => {
|
|
const docker = fakeDocker();
|
|
const p = new DockerHostPool(options([
|
|
{ ...hostA, rconHost: '10.0.0.1' },
|
|
{ ...hostB, rconHost: '10.0.0.2' },
|
|
]), { runDocker: docker.run });
|
|
await p.stop({ container: 'cs2mv-alt', hostName: 'default', rconHost: '10.0.0.2', port: 27100 });
|
|
assert.deepEqual(docker.calls.at(-1), ['rm', '-f', 'cs2mv-alt']);
|
|
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);
|
|
});
|