Require revocable authorization for automatic lobby removal
This commit is contained in:
@@ -148,13 +148,13 @@ export function canCancelRemoval(id: string): boolean {
|
||||
}
|
||||
|
||||
/** Competes atomically with the worker's claim, before any external removal. */
|
||||
export function cancelRemoval(id: string): boolean {
|
||||
export function cancelRemoval(id: string, pausePolicy = true): boolean {
|
||||
return db().transaction(() => {
|
||||
const finishedAt = new Date().toISOString();
|
||||
const result = db().prepare(`UPDATE server_deployments SET state = 'cancelled', stage = 'Removal cancelled', finished_at = ?, error = NULL
|
||||
WHERE id = ? AND server_deleted_at IS NULL AND action = 'destroy' AND state = 'waiting' AND stage != ?`).run(finishedAt, id, REMOVING_SERVER);
|
||||
if (!result.changes) return false;
|
||||
pauseScalingForOperation(id);
|
||||
if (pausePolicy) pauseScalingForOperation(id);
|
||||
recordProgress(id, { blocker: null, checks: [], event: { kind: 'outcome',
|
||||
message: 'Queued removal cancelled. The server and its files were kept.',
|
||||
details: { state: 'cancelled', finishedAt } } });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { scalingOwnershipBlocker } from '../autoscaling/ownership.js';
|
||||
import { scalingOwnershipBlocker, assertScalingOwnership } from '../autoscaling/ownership.js';
|
||||
import { mapIdentity } from '../autoscaling/snapshot.js';
|
||||
import { db } from '../db/index.js';
|
||||
import { activeBetaRemovalBlocker, getServer } from '../fleet/servers.js';
|
||||
import type { GameMode, ZombieGame } from '../fleet/map-aliases.js';
|
||||
@@ -68,9 +69,13 @@ function hasActiveCreation(name: string): boolean {
|
||||
return !!db().prepare("SELECT 1 FROM server_deployments WHERE server = ? AND server_deleted_at IS NULL AND action = 'create' AND state IN ('queued', 'running', 'awaiting-health')").get(name);
|
||||
}
|
||||
|
||||
export function destroyServer(name: string, triggeredBy = 'ui'): DestroyServerResult & { deploymentId?: string } {
|
||||
export function destroyServer(name: string, triggeredBy = 'ui', autoscalingDecisionId?: string): DestroyServerResult & { deploymentId?: string } {
|
||||
const owned = scalingOwnershipBlocker(name);
|
||||
if (owned) return { ok: false, kind: 'conflict', message: owned, warnings: [] };
|
||||
if (autoscalingDecisionId) {
|
||||
const identity = mapIdentity(getServer(name) ?? { name });
|
||||
if (!identity) return { ok: false, kind: 'conflict', message: 'Map identity unavailable.', warnings: [] };
|
||||
assertScalingOwnership(identity.game, identity.mapSlug, autoscalingDecisionId);
|
||||
} else if (owned) return { ok: false, kind: 'conflict', message: owned, warnings: [] };
|
||||
const cancelled = cancelQueuedNativeCreation(name, triggeredBy);
|
||||
if (cancelled) return { ok: true, warnings: [], deploymentId: cancelled };
|
||||
const blocker = activeBetaRemovalBlocker(name);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { assertScalingOwnership } from '../autoscaling/ownership.js';
|
||||
import { removalAuthorization } from '../autoscaling/authorization.js';
|
||||
import { scalingIntent } from '../autoscaling/admission.js';
|
||||
import { putFileBlob, getFileBlob, blobSize } from '../db/file-blobs.js';
|
||||
import { concurrencyBlocker, preparingJobs } from './concurrency.js';
|
||||
import { isIP } from 'node:net';
|
||||
@@ -18,7 +20,7 @@ import { prepareNativeLaunch, evaluateNativePlacement, type PreparedNativeLaunch
|
||||
import { getServerKey, saveServerKey } from '../fleet/server-keys.js';
|
||||
import { activeBetaRemovalBlocker, getServer } from '../fleet/servers.js';
|
||||
import { nextLobbyNumber, type CreateServerInput, type CreateServerResult } from './instances.js';
|
||||
import { beginDeployment, deploymentStage, finishDeployment, REMOVING_SERVER } from './history.js';
|
||||
import { beginDeployment, deploymentStage, finishDeployment, REMOVING_SERVER, cancelRemoval } from './history.js';
|
||||
import { recordProgress, type CheckInput } from './progress.js';
|
||||
import { trackActivity } from '../pipeline/activity.js';
|
||||
import { fleetSettings } from '../settings.js';
|
||||
@@ -659,7 +661,11 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
JOIN watchdog_incidents i ON i.id=s.source_id JOIN server_deployments d ON d.id=s.operation_id
|
||||
WHERE s.operation_id=? AND s.kind='watchdog' AND i.kind='health' AND d.triggered_by LIKE 'watchdog:%'`).get(job.id);
|
||||
if (recoveredWatchdog && occupancy.players !== null) { resolveRecoveredHealthOperation(job.id, occupancy.players, occupancy.source); return false; }
|
||||
if (occupancy.players !== 0) { hold(job, occupancy.players !== null ? `Waiting for ${occupancy.players} player(s) to leave.` : 'Waiting for a fresh local player count.'); return false; }
|
||||
if (occupancy.players !== 0) {
|
||||
if (job.action === 'destroy' && scalingIntent(job.id)) cancelRemoval(job.id, false);
|
||||
else hold(job, occupancy.players !== null ? `Waiting for ${occupancy.players} player(s) to leave.` : 'Waiting for a fresh local player count.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -729,6 +735,13 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
? 'Host eligibility checked; player safety checks bypassed by admin override' : 'Host eligibility and safety checks passed' } });
|
||||
const payload: Record<string, unknown> = files.length ? { stageId: job.id } : {};
|
||||
if (overrideSafety) payload.safetyOverride = true;
|
||||
if (job.action === 'destroy') {
|
||||
const authorization = removalAuthorization(job.id, instance!.host_id);
|
||||
if (authorization) {
|
||||
if (overrideSafety) throw new Error('Autoscaling never bypasses player safety.');
|
||||
payload.autoscaling = authorization;
|
||||
}
|
||||
}
|
||||
if (prepared) {
|
||||
deploymentStage(job.id, 'Preparing launch files for the host agent', { phase: 'prepare', recipeStatus: 'attempted',
|
||||
checkpoint: { key: 'startup-queue', label: 'Startup capacity available' } });
|
||||
|
||||
@@ -19,6 +19,7 @@ export function safetyOverrideAvailability(id: string): { applicable: boolean; u
|
||||
AND d.state IN ('queued','waiting') AND d.submitted_at IS NULL AND n.operation_id IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM ec_host_migrations m WHERE m.deployment_id=d.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM native_creation_cancellations c WHERE c.cleanup_id=d.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM autoscaling_decisions a WHERE a.operation_id=d.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM provisioning_safety_overrides o WHERE o.deployment_id=d.id)`)
|
||||
.get(id) as { host_id: string } | undefined;
|
||||
if (!target) return { applicable: false, unavailableReason: null };
|
||||
|
||||
Reference in New Issue
Block a user