Cancel active provisioning jobs when rolling back betas
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { listHosts } from '../hosts/index.js';
|
||||
import { fleetSettings } from '../settings.js';
|
||||
import { probePlayerEvidence, probeServerStatus } from '../fleet/status-evidence.js';
|
||||
import { assertProvisioningNotCancelled, provisioningCancelled } from './cancellation.js';
|
||||
import { beginDeployment, deploymentStage, finishDeployment, recordDeploymentEvent, REMOVING_SERVER, resumeCleanup, resumeVerification, claimRemoval } from './history.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { LaunchPreview } from '$lib/deployment-plan.js';
|
||||
@@ -76,7 +77,7 @@ export function nextLobbyNumber(baseName: string, additionalNames: string[] = []
|
||||
const rows = db()
|
||||
.prepare(`SELECT name FROM servers WHERE name LIKE ? UNION
|
||||
SELECT j.server AS name FROM server_creation_jobs j JOIN server_deployments d ON d.id = j.deployment_id
|
||||
WHERE d.server_deleted_at IS NULL AND (d.state IN ('queued', 'running', 'awaiting-health') OR (d.action = 'rebuild' AND (d.state = 'waiting' OR (d.state IN ('failed','interrupted') AND d.stage = 'Removing server files and registry'))))`)
|
||||
WHERE d.server_deleted_at IS NULL AND (d.state IN ('queued', 'running', 'awaiting-health') OR (d.state = 'cancelled' AND d.submitted_at IS NOT NULL) OR (d.action = 'rebuild' AND (d.state = 'waiting' OR (d.state IN ('failed','interrupted') AND d.stage = 'Removing server files and registry'))))`)
|
||||
.all(`${baseName}_%`) as Array<{ name: string }>;
|
||||
const taken = new Set<number>();
|
||||
const prefix = `${baseName}_`;
|
||||
@@ -100,7 +101,7 @@ export function nextAvailablePort(additionalPorts: number[] = [], releasingServe
|
||||
const rows = db()
|
||||
.prepare(`SELECT name, game_port FROM servers WHERE game_port IS NOT NULL UNION
|
||||
SELECT j.server AS name, j.port AS game_port FROM server_creation_jobs j JOIN server_deployments d ON d.id = j.deployment_id
|
||||
WHERE d.server_deleted_at IS NULL AND (d.state IN ('queued', 'running', 'awaiting-health') OR (d.action = 'rebuild' AND (d.state = 'waiting' OR (d.state IN ('failed','interrupted') AND d.stage = 'Removing server files and registry'))))`)
|
||||
WHERE d.server_deleted_at IS NULL AND (d.state IN ('queued', 'running', 'awaiting-health') OR (d.state = 'cancelled' AND d.submitted_at IS NOT NULL) OR (d.action = 'rebuild' AND (d.state = 'waiting' OR (d.state IN ('failed','interrupted') AND d.stage = 'Removing server files and registry'))))`)
|
||||
.all() as Array<{ name: string; game_port: number }>;
|
||||
const taken = new Set([...rows.filter(row => !releasingServers.includes(row.name)).map((r) => r.game_port), ...additionalPorts]);
|
||||
for (let p = config.portRange.min; p <= config.portRange.max; p++) {
|
||||
@@ -159,9 +160,14 @@ function buildSpec(args: {
|
||||
}
|
||||
|
||||
let creating = false;
|
||||
async function observeCommand<T>(operation: string, command: () => Promise<T>): Promise<T> {
|
||||
try { return await command(); }
|
||||
catch (err) {
|
||||
async function observeCommand<T>(operation: string, command: () => Promise<T>, retainReceipt = false): Promise<T> {
|
||||
assertProvisioningNotCancelled(operation);
|
||||
try {
|
||||
const result = await command();
|
||||
if (!retainReceipt) assertProvisioningNotCancelled(operation);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (provisioningCancelled(operation)) throw err;
|
||||
recordProgress(operation, { command: { state: 'uncertain', message: 'No confirmed command result was received: ' + (err as Error).message } });
|
||||
throw err;
|
||||
}
|
||||
@@ -314,6 +320,7 @@ export async function rebuildServer(id: string): Promise<void> {
|
||||
try {
|
||||
const result = job.stage === REMOVING_SERVER
|
||||
? await cleanupDestroyedServer(job.server, id) : await destroyServerUnlocked(job.server, id);
|
||||
if (provisioningCancelled(id)) return;
|
||||
if (!result.ok || result.warnings.length) {
|
||||
finishDeployment(id, { ok: false, message: result.ok ? 'Old server files could not be completely removed.' : result.message, warnings: result.warnings });
|
||||
return;
|
||||
@@ -435,12 +442,16 @@ async function createServerUnlocked(input: CreateServerInput, prepared: Prepared
|
||||
return { ok: false, kind: 'fs', message: `failed to create ${hostDir}: ${(err as Error).message}` };
|
||||
}
|
||||
|
||||
assertProvisioningNotCancelled(operation);
|
||||
// Step 2: Komodo CreateDeployment.
|
||||
deploymentStage(operation, 'Creating Komodo deployment', { phase: 'create-deployment',
|
||||
checkpoint: { key: 'prepare', label: 'Server files prepared' },
|
||||
command: { state: 'attempted', message: 'Komodo deployment creation attempted.' } });
|
||||
const spec = buildSpec({ game, name, port, hostDir, plutoKey, recipe });
|
||||
const createRes = await observeCommand(operation, () => komodoCreateDeployment(spec));
|
||||
db().prepare('UPDATE server_deployments SET submitted_at=? WHERE id=?').run(new Date().toISOString(), operation);
|
||||
// A confirmed creation must retain its identity for inspection/removal even if cancelled in flight.
|
||||
const createRes = await observeCommand(operation, () => komodoCreateDeployment(spec), true);
|
||||
if (!createRes.ok && provisioningCancelled(operation)) assertProvisioningNotCancelled(operation);
|
||||
if (!createRes.ok) {
|
||||
recordProgress(operation, { command: { state: createRes.kind === 'unreachable' || (createRes.httpStatus !== undefined && createRes.httpStatus >= 500) ? 'uncertain' : 'rejected', message: createRes.message } });
|
||||
await rm(hostDir, { recursive: true, force: true }).catch(() => {});
|
||||
@@ -482,6 +493,7 @@ async function createServerUnlocked(input: CreateServerInput, prepared: Prepared
|
||||
return { ok: false, kind: 'db', message: `failed to insert server row: ${(err as Error).message}` };
|
||||
}
|
||||
|
||||
assertProvisioningNotCancelled(operation);
|
||||
// Step 4: apply Komodo tags so operators can filter the deployment list
|
||||
// by game/map. Best-effort — a tag failure doesn't roll back the server;
|
||||
// the deployment is fully functional without cosmetic tags. Fires in
|
||||
@@ -718,6 +730,7 @@ async function destroyServerUnlocked(name: string, operation: string): Promise<D
|
||||
const pollDeadline = Date.now() + 30_000;
|
||||
while (Date.now() < pollDeadline) {
|
||||
const health = await komodoInspectHealthy(name);
|
||||
assertProvisioningNotCancelled(operation);
|
||||
if (!health.ok) break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
}
|
||||
@@ -740,6 +753,7 @@ async function destroyServerUnlocked(name: string, operation: string): Promise<D
|
||||
}
|
||||
|
||||
async function cleanupDestroyedServer(name: string, operation: string): Promise<DestroyServerResult> {
|
||||
assertProvisioningNotCancelled(operation);
|
||||
const blocker = activeBetaRemovalBlocker(name);
|
||||
if (blocker) return { ok: false, kind: 'conflict', message: blocker, warnings: [] };
|
||||
const row = getServer(name);
|
||||
@@ -753,11 +767,13 @@ async function cleanupDestroyedServer(name: string, operation: string): Promise<
|
||||
const hostDir = row?.host_dir ?? serverDir(name);
|
||||
try {
|
||||
await rm(hostDir, { recursive: true, force: true });
|
||||
assertProvisioningNotCancelled(operation);
|
||||
recordProgress(operation, { checkpoint: { key: 'files-removed', label: 'Instance files removed', phase: 'cleanup' } });
|
||||
} catch (err) {
|
||||
warnings.push(`rm ${hostDir} failed: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
assertProvisioningNotCancelled(operation);
|
||||
// The registry trigger preserves history and marks its old server identity.
|
||||
// Live inventory, rollback snapshots, and restart slots still cascade away.
|
||||
try {
|
||||
@@ -802,6 +818,7 @@ async function provisionCreation(id: string): Promise<CreateServerResult> {
|
||||
}
|
||||
const result = await createServerUnlocked({ gameMode: job.game_mode, mapSlug: job.map_slug, createdBy: job.triggered_by },
|
||||
{ ok: true, name: job.server, port: job.port, mapEntry, plutoKey }, id);
|
||||
if (provisioningCancelled(id) && result.ok) return result;
|
||||
if (!result.ok) finishDeployment(id, result);
|
||||
else {
|
||||
db().transaction(() => {
|
||||
@@ -812,6 +829,7 @@ async function provisionCreation(id: string): Promise<CreateServerResult> {
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (provisioningCancelled(id)) throw err;
|
||||
const server = getServer(job.server);
|
||||
if (server?.game_port === job.port) {
|
||||
// A lost response may have happened after Komodo started the server.
|
||||
|
||||
Reference in New Issue
Block a user