From 5b65520bbcdec938fdb7d20bbec214f91782395a Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Mon, 28 Sep 2026 10:21:52 -0400 Subject: [PATCH] Clarify provisioning wait messages (fixes #54) --- src/lib/server/provisioning/restarts.ts | 10 ++++++---- src/lib/server/provisioning/worker.ts | 8 ++++---- src/lib/ui/status.ts | 2 +- src/routes/collections/launches/[id]/+page.svelte | 2 +- src/routes/provisioning/[id]/+page.svelte | 4 ++-- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/lib/server/provisioning/restarts.ts b/src/lib/server/provisioning/restarts.ts index fc204b5..515635b 100644 --- a/src/lib/server/provisioning/restarts.ts +++ b/src/lib/server/provisioning/restarts.ts @@ -4,6 +4,7 @@ import { komodoInspectHealthy, komodoRestart, komodoRuntimeAction } from '../int import { probeServerStatus } from '../fleet/status-evidence.js'; import { deploymentStage, finishDeployment } from './history.js'; import { applyStagedFilesForOperation } from '../fleet/staged-files.js'; +import type { RuntimeAction } from '../../provisioning.js'; // Komodo queues restart work. Don't mistake the still-running old container // for a completed restart when an API kick immediately follows the request. @@ -36,7 +37,7 @@ export function restartSettling(server: string): boolean { && Date.now() - Date.parse(slot.requested_at) < HEALTH_POLL_MS); } -export function rolloutWaitReason(server: string, operationId = ''): string | null { +export function rolloutWaitReason(server: string, operationId = '', action: RuntimeAction = 'restart'): string | null { if (db().prepare(`SELECT 1 FROM server_deployments WHERE server = ? AND id != ? AND server_deleted_at IS NULL AND ((action IN ('start','stop','rebuild') AND state IN ('running','awaiting-health')) OR (action = 'destroy' AND state IN ('waiting','running')))`).get(server, operationId)) { @@ -47,10 +48,11 @@ export function rolloutWaitReason(server: string, operationId = ''): string | nu WHERE d.id = ? AND d.server_deleted_at IS NULL AND d.action IN ('restart','rebuild') AND EXISTS ( SELECT 1 FROM provisioning_sources s WHERE s.operation_id = d.id AND s.kind = 'watchdog')`).get(operationId); if (slot?.healthy_at === null && !ownHandoff) { - return `Waiting for the previous restart of this server to recover${slot.error ? `: ${slot.error}` : ''}`; + return `Waiting for this server to recover from its previous operation${slot.error ? `: ${slot.error}` : ''}`; } const { n } = db().prepare('SELECT COUNT(*) AS n FROM restart_rollout WHERE healthy_at IS NULL').get() as { n: number }; - return n - (ownHandoff && slot?.healthy_at === null ? 1 : 0) >= fleetSettings().restartConcurrency ? 'Queued for a rolling restart; waiting for another server to recover' : null; + return n - (ownHandoff && slot?.healthy_at === null ? 1 : 0) >= fleetSettings().restartConcurrency + ? `Queued to ${action}; waiting for another server to recover` : null; } export type RolloutRestartResult = @@ -63,7 +65,7 @@ export async function restartInRollout(server: string, options: { operationId: s const operationId = options.operationId; const waitReason = db().transaction(() => { if (!db().prepare('SELECT 1 FROM server_deployments WHERE id = ? AND server_deleted_at IS NULL').get(operationId)) return 'Original server was deleted'; - const reason = rolloutWaitReason(server, operationId); + const reason = rolloutWaitReason(server, operationId, options.action); if (reason) return reason; db().prepare(`INSERT INTO restart_rollout (server, requested_at, operation_id) VALUES (?, ?, ?) ON CONFLICT(server) DO UPDATE SET requested_at = excluded.requested_at, diff --git a/src/lib/server/provisioning/worker.ts b/src/lib/server/provisioning/worker.ts index 712e165..5e8306b 100644 --- a/src/lib/server/provisioning/worker.ts +++ b/src/lib/server/provisioning/worker.ts @@ -57,7 +57,7 @@ function sourceEligibility(operation: Operation): 'ready' | 'held' | 'cancelled' return held ? 'held' : ready ? 'ready' : 'cancelled'; } -function blocker(server: string, id: string): string | null { +function blocker(server: string, id: string, action: RuntimeAction): string | null { const sources = sourcesFor(id); const rank = priority(sources); const others = db().prepare(`SELECT id, state, action, rowid FROM server_deployments WHERE server = ? AND id != ? AND server_deleted_at IS NULL @@ -76,7 +76,7 @@ function blocker(server: string, id: string): string | null { SELECT 1 FROM server_deployments d WHERE d.id = ds.provisioning_id AND d.state IN ('queued','waiting','running','awaiting-health'))) AND NOT EXISTS (SELECT 1 FROM watchdog_actions a WHERE a.operation_id = ? AND a.campaign_id = ds.deployment_id) LIMIT 1`).get(server, id)) return 'Waiting for campaign work on this server'; - return rolloutWaitReason(server, id); + return rolloutWaitReason(server, id, action); } interface Operation { id: string; server: string; action: RuntimeAction; state: string; stage: string } @@ -128,7 +128,7 @@ async function processOperation(operation: Operation): Promise { } else deploymentStage(id, 'Waiting for container to stop' + (!health.ok ? ': ' + health.reason : '')); return; } - const reason = blocker(server, id); + const reason = blocker(server, id, action); if (reason) { deploymentStage(id, reason); return; } const health = await komodoInspectHealthy(server); if (action !== 'start' && !isStopped(health.state)) { @@ -140,7 +140,7 @@ async function processOperation(operation: Operation): Promise { } } // Requests can arrive during the probes. Recheck before claiming the action. - const latestReason = blocker(server, id); + const latestReason = blocker(server, id, action); if (latestReason) { deploymentStage(id, latestReason); return; } if (sourceEligibility(operation) !== 'ready') return; if (!db().prepare("SELECT 1 FROM server_deployments WHERE id = ? AND server_deleted_at IS NULL AND state = 'waiting'").get(id)) return; diff --git a/src/lib/ui/status.ts b/src/lib/ui/status.ts index 138d938..719fcee 100644 --- a/src/lib/ui/status.ts +++ b/src/lib/ui/status.ts @@ -5,7 +5,7 @@ const states: Record = { queued: ['Queued', 'neutral'], pending: ['Queued for restart', 'neutral'], running: ['In progress', 'info'], active: ['In progress', 'info'], deploying: ['Installing files', 'info'], restarting: ['Restarting', 'info'], observing: ['Collecting test evidence', 'info'], 'rolling-back': ['Rolling back', 'info'], - 'awaiting-empty': ['Waiting for empty lobby', 'neutral'], waiting: ['Waiting for empty lobby', 'neutral'], + 'awaiting-empty': ['Waiting for empty lobby', 'neutral'], waiting: ['Waiting', 'neutral'], 'awaiting-health': ['Verifying health', 'info'], 'awaiting-match': ['Waiting for a match', 'neutral'], ready: ['Ready', 'online'], 'ready-promote': ['Ready to promote', 'online'], done: ['Completed', 'online'], passed: ['Test passed', 'online'], tested: ['Test passed', 'online'], diff --git a/src/routes/collections/launches/[id]/+page.svelte b/src/routes/collections/launches/[id]/+page.svelte index 53b9b28..e405028 100644 --- a/src/routes/collections/launches/[id]/+page.svelte +++ b/src/routes/collections/launches/[id]/+page.svelte @@ -20,7 +20,7 @@ const currentServers = $derived(detail?.servers.filter(launching) ?? []); const otherLaunches = $derived(detail?.queue.active.filter(active => !detail?.servers.some(server => server.id === active.id)) ?? []); const firstQueued = $derived(detail?.servers.find(server => !server.server_deleted_at && server.state === 'queued')); - const labels: Record = { queued: 'Queued', running: 'Launching', 'awaiting-health': 'Checking readiness', done: 'Ready', warning: 'Needs review', failed: 'Failed', interrupted: 'Interrupted', waiting: 'Waiting for empty server', cancelled: 'Cancelled' }; + const labels: Record = { queued: 'Queued', running: 'Launching', 'awaiting-health': 'Checking readiness', done: 'Ready', warning: 'Needs review', failed: 'Failed', interrupted: 'Interrupted', waiting: 'Waiting', cancelled: 'Cancelled' }; const waitingForPort = (server: CollectionServer) => server.state === 'queued' && (server.stage.startsWith('Waiting for removal') || server.stage.startsWith('Waiting for port')); function stateLabel(server: CollectionServer) { if (waitingForPort(server)) return 'Waiting for removal'; diff --git a/src/routes/provisioning/[id]/+page.svelte b/src/routes/provisioning/[id]/+page.svelte index 2e751fa..ed3c22b 100644 --- a/src/routes/provisioning/[id]/+page.svelte +++ b/src/routes/provisioning/[id]/+page.svelte @@ -36,7 +36,7 @@ }); const createSteps = ['Prepare server files', 'Create Komodo deployment', 'Start container', 'Verify Komodo health', 'Verify RCON health', 'Verify selected map']; const createStages = ['Preparing server files', 'Creating Komodo deployment', 'Starting container', 'Waiting for Komodo health', 'Waiting for RCON health', 'Waiting for selected map']; - const destroySteps = ['Wait for empty server', 'Stop and remove container', 'Delete Komodo deployment', 'Remove server files and registry']; + const destroySteps = ['Wait for eligibility', 'Stop and remove container', 'Delete Komodo deployment', 'Remove server files and registry']; const destroyStages = ['Waiting to verify', 'Stopping and removing container', 'Deleting Komodo deployment', 'Removing server files and registry']; let steps = $derived(deployment?.action === 'destroy' ? ['Request destruction', ...destroySteps] : deployment?.action === 'rebuild' ? ['Request rebuild', ...destroySteps, 'Queue fresh instance', ...createSteps] @@ -76,7 +76,7 @@ ? 'The server is still registered. Inspect its status, then resume health verification without creating or restarting a container.' : 'Inspect the error and any remaining container or files before launching again. The activity below records how far this operation reached.'; if (deployment.state === 'queued') return 'Your request is saved in the launch queue. A startup slot opens after an earlier server passes its container, RCON, and map checks.'; - if (deployment.action === 'rebuild' && deployment.state === 'waiting') return 'Waiting until the instance is empty and a creation slot is available. The old instance and all of its files will be deleted before a fresh instance is created.'; + if (deployment.action === 'rebuild' && deployment.state === 'waiting') return 'The rebuild waits for other work to finish, an empty server, and available recovery and startup capacity. The old instance and all of its files will be deleted before a fresh instance is created.'; if (deployment.state === 'waiting') return 'The server stays online while players finish. Removal begins only after a live check confirms it is empty.'; if (deployment.stage.startsWith('Waiting for selected map')) return 'Container and RCON checks passed. Waiting for RCON to report the selected engine map. Inspect the server configuration if this persists.'; if (deployment.stage.startsWith('Waiting for RCON')) return 'The container is healthy. Waiting for the game to answer RCON; check its startup logs, password, and network if this persists.';