Add exclusive map ownership and capacity-aware autoscaling creation

This commit is contained in:
2026-10-06 22:59:17 -04:00
parent 36cf2b9c05
commit e012278bd9
17 changed files with 270 additions and 18 deletions
+9 -4
View File
@@ -1,3 +1,4 @@
import { assertScalingOwnership } from '../autoscaling/ownership.js';
import { putFileBlob, getFileBlob, blobSize } from '../db/file-blobs.js';
import { concurrencyBlocker, preparingJobs } from './concurrency.js';
import { isIP } from 'node:net';
@@ -152,11 +153,14 @@ export function queueNativeCreation(input: CreateServerInput): CreateServerResul
/** Persist the reviewed logical identity; all host/port/resource decisions belong to provisioning. */
export function queueNativeCreationIntent(input: { server: string; game: 't5' | 't6'; mapSlug: string; selection: NativeSelection;
plutoKey?: string; createdBy?: string; blueprintName?: string | null; recipe?: LaunchRecipePlan }): CreateServerResult {
plutoKey?: string; createdBy?: string; blueprintName?: string | null; recipe?: LaunchRecipePlan; autoscalingDecisionId?: string }): CreateServerResult {
try {
return db().transaction(() => {
assertScalingOwnership(input.game, input.mapSlug, input.autoscalingDecisionId);
const expected = `${input.game}_zombie_${input.mapSlug}_${nextLobbyNumber(`${input.game}_zombie_${input.mapSlug}`)}`;
if (input.server !== expected || nativeInstance(input.server)) throw new Error('Available lobby changed. Review a fresh preview.');
const reserved = db().prepare(`SELECT 1 FROM server_creation_jobs j JOIN server_deployments d ON d.id=j.deployment_id WHERE j.server=?
AND d.server_deleted_at IS NULL AND (d.state IN ('queued','waiting','running','awaiting-health','interrupted') OR (d.state='cancelled' AND d.submitted_at IS NOT NULL))`).get(input.server);
if ((!input.autoscalingDecisionId && input.server !== expected) || nativeInstance(input.server) || getServer(input.server) || reserved) throw new Error('Available lobby changed. Review a fresh preview.');
const key = input.plutoKey?.trim() || getServerKey(input.server);
if (!key) throw new Error(`Save a Plutonium server key for ${input.server}.`);
saveServerKey(input.server, key);
@@ -402,7 +406,7 @@ function sameLaunchFiles(first: LaunchRecipePlan, second: LaunchRecipePlan): boo
}
/** Capacity history and host ranking run in the background reader, one job at a time. */
async function placeQueuedCreation(job: Job, instance: NativeInstance | null): Promise<NativeInstance | null> {
async function placeQueuedCreation(job: Job, instance: NativeInstance | null, eligibility: (job: Job) => string | null): Promise<NativeInstance | null> {
const recipe = recipeForDeployment(job.id)!.recipe as NativeRecipe;
const prior: NativeSelection = job.selection ? JSON.parse(job.selection) : recipe.native!;
const network = prior.automatic ? prior.network ?? {} : prior;
@@ -413,6 +417,7 @@ async function placeQueuedCreation(job: Job, instance: NativeInstance | null): P
return db().transaction(() => {
const state = db().prepare('SELECT state FROM server_deployments WHERE id=? AND server_deleted_at IS NULL').get(job.id) as { state: string } | undefined;
if (state?.state !== 'queued') return null;
if (eligibility(job)) return null;
const live = nativeInstance(job.server);
if (instance ? live?.id !== instance.id || live.generation !== instance.generation : !!live || !!getServer(job.server)) return null;
const active = (db().prepare(`SELECT count(*) AS n FROM server_deployments WHERE id!=? AND server_deleted_at IS NULL
@@ -467,7 +472,7 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
const capacityBlocker = concurrencyBlocker(job.id, job.action);
if (capacityBlocker) { hold(job, capacityBlocker); return; }
if (eligibility(job)) return;
instance = await placeQueuedCreation(job, instance);
instance = await placeQueuedCreation(job, instance, eligibility);
if (!instance) return;
job.instance_id = instance.id; job.generation = instance.generation;
}