Add autoscaling policy model and deterministic planner
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { zombieMapBySlug, type ZombieGame } from './map-aliases.js';
|
||||
|
||||
export const autoscalingModes = ['off', 'dry-run', 'create-only', 'create-destroy'] as const;
|
||||
export type AutoscalingMode = typeof autoscalingModes[number];
|
||||
export interface ScalingSettings {
|
||||
game: ZombieGame; mapSlug: string; mode: AutoscalingMode;
|
||||
minimum: number; maximum: number; spares: number; emptyMinutes: number; cooldownMinutes: number;
|
||||
}
|
||||
export interface ScalingPolicy extends ScalingSettings {
|
||||
id: string; revision: number; nextActionAt: number; pausedReason: string | null; updatedAt: string;
|
||||
}
|
||||
export interface ScalingMember {
|
||||
name: string; identity: string; state: 'occupied' | 'empty' | 'creating' | 'removing' | 'stopped' | 'unknown' | 'protected';
|
||||
players: number | null; observedAt: number | null; source: string | null;
|
||||
emptySince: number | null; blocker: string | null; operationId: string | null;
|
||||
}
|
||||
export interface ScalingSnapshot {
|
||||
members: ScalingMember[]; reservedNames: string[]; keyedNames: string[]; blockers: string[];
|
||||
}
|
||||
export interface ScalingDecision {
|
||||
action: 'create' | 'destroy' | 'wait'; reason: string; server: string | null;
|
||||
desired: number; occupied: number; empty: number; pending: number; unknown: number;
|
||||
}
|
||||
export const liveScaling = (mode: AutoscalingMode): boolean => mode === 'create-only' || mode === 'create-destroy';
|
||||
|
||||
export function scalingSettings(input: unknown): ScalingSettings {
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Provide an autoscaling policy.');
|
||||
const value = input as Record<string, unknown>;
|
||||
const { game, mapSlug } = value;
|
||||
if ((game !== 't5' && game !== 't6') || typeof mapSlug !== 'string' || zombieMapBySlug(game, mapSlug)?.slug !== mapSlug) throw new Error('Choose a supported game and map variant.');
|
||||
const mode = value.mode ?? 'off';
|
||||
if (!autoscalingModes.includes(mode as AutoscalingMode)) throw new Error('Choose a valid autoscaling mode.');
|
||||
const integer = (key: string, fallback: number | undefined, max: number) => {
|
||||
const n = value[key] ?? fallback;
|
||||
if (typeof n !== 'number' || !Number.isInteger(n) || n < 1 || n > max) throw new Error(`${key} must be an integer from 1 to ${max}.`);
|
||||
return n;
|
||||
};
|
||||
const minimum = integer('minimum', 1, 20), maximum = integer('maximum', undefined, 20), spares = integer('spares', 1, 20);
|
||||
if (minimum > maximum || spares > maximum) throw new Error('Minimum and spare target cannot exceed the maximum.');
|
||||
return { game, mapSlug, mode: mode as AutoscalingMode, minimum, maximum, spares,
|
||||
emptyMinutes: integer('emptyMinutes', 20, 1440), cooldownMinutes: integer('cooldownMinutes', 5, 1440) };
|
||||
}
|
||||
Reference in New Issue
Block a user