refactor: make provisioning own instance lifecycle operations

This commit is contained in:
2026-09-27 14:45:00 -04:00
parent cfed584614
commit df265d1900
44 changed files with 1791 additions and 1609 deletions
+15 -51
View File
@@ -1,9 +1,7 @@
import { db } from '../db/index.js';
import { emit } from './bus.js';
import type { BetaStage } from './bus.js';
import { komodoConfigured, rconConfigured } from '../config.js';
import { rconGetPlayerCount } from '../integrations/rcon.js';
import { refreshRestartRollout, restartInRollout, rolloutHealth, rolloutWaitReason } from './restart-rollout.js';
import { requestRestart } from '../provisioning/index.js';
import { plannedBetaTick } from './beta-plan-runner.js';
interface BetaServerActiveRow {
@@ -20,10 +18,6 @@ function nowIso(): string {
return new Date().toISOString();
}
function integrationsOn(): boolean {
return komodoConfigured() && rconConfigured();
}
const setStage = () =>
db().prepare(
`UPDATE beta_servers
@@ -94,9 +88,7 @@ export function betaTick(): Promise<{ processed: number }> {
}
async function runBetaTick(): Promise<{ processed: number }> {
if (integrationsOn()) await refreshRestartRollout();
await plannedBetaTick();
if (!integrationsOn()) return { processed: 0 };
const rows = db()
.prepare(
@@ -131,49 +123,21 @@ async function runBetaTick(): Promise<{ processed: number }> {
async function advance(row: BetaServerActiveRow): Promise<void> {
switch (row.stage) {
case 'awaiting-empty':
case 'restarting': {
const waitReason = rolloutWaitReason(row.server);
if (waitReason) {
transition(row.beta_id, row.server, 'awaiting-empty', waitReason);
return;
}
const players = await rconGetPlayerCount(row.server);
if (players === null) return; // hold — RCON unreachable, try next tick
if (players > 0) return; // hold — still populated
transition(row.beta_id, row.server, 'restarting');
const r = await restartInRollout(row.server);
if (r.kind === 'queued') {
transition(row.beta_id, row.server, 'awaiting-empty', r.message);
return;
}
if (r.kind === 'failed') {
transition(row.beta_id, row.server, 'failed', r.message);
return;
}
const next = row.wait_for_health ? 'awaiting-health' : row.wait_for_match ? 'awaiting-match' : 'ready';
transition(row.beta_id, row.server, next);
return;
}
case 'restarting':
case 'awaiting-health': {
const h = rolloutHealth(row.server);
if (!h.ok) {
// Record the failure reason (including any snippet) so the
// beta detail UI can surface why we're stuck.
setStage().run(
'awaiting-health',
h.reason,
nowIso(),
'awaiting-health',
'awaiting-health',
nowIso(),
'awaiting-health',
'awaiting-health',
nowIso(),
'awaiting-health',
nowIso(),
row.beta_id,
row.server
);
const operation = requestRestart(row.server, { kind: 'beta', id: String(row.beta_id) });
if (['failed','interrupted','cancelled'].includes(operation.state)) {
transition(row.beta_id, row.server, 'failed', operation.error ?? operation.stage);
return;
}
if (!operation.submitted_at || operation.state === 'running') {
transition(row.beta_id, row.server, 'awaiting-empty', operation.stage);
return;
}
db().prepare('UPDATE beta_servers SET restarted_at = ? WHERE beta_id = ? AND server = ?')
.run(operation.submitted_at, row.beta_id, row.server);
if (row.wait_for_health && operation.state !== 'done') {
transition(row.beta_id, row.server, 'awaiting-health', operation.stage);
return;
}
const next = row.wait_for_match ? 'awaiting-match' : 'ready';