Add safe manual instance rebalancing

This commit is contained in:
2026-10-08 12:37:56 -04:00
parent 26de43639f
commit 012cd51a66
45 changed files with 2006 additions and 154 deletions
+3 -2
View File
@@ -3,6 +3,7 @@ import { fleetSettings } from '../settings.js';
import { subscribe } from '../pipeline/bus.js';
import { capacityCalibration } from './calibration.js';
import { HEARTBEAT_MAX_AGE_MS, listHosts } from './index.js';
import { committedRebalanceStartup } from '../provisioning/concurrency.js';
import type { CapacityCalibration, Host, NativePlacement } from '../../hosts.js';
import type { LaunchAvailability } from '../../deployment-plan.js';
@@ -37,9 +38,9 @@ function snapshotSchemaReady(handle: ReturnType<typeof db>): boolean {
function queueSnapshot(handle: ReturnType<typeof db>, hosts: Map<string, HostAvailability>): Snapshot {
const counts = handle.prepare(`SELECT
COALESCE(SUM(state IN ('running','awaiting-health')),0) AS active,
COALESCE(SUM(state IN ('running','awaiting-health') OR ${committedRebalanceStartup}),0) AS active,
COALESCE(SUM(state='queued'),0) AS queued FROM server_deployments
WHERE action IN ('create','rebuild') AND state IN ('queued','running','awaiting-health')
WHERE action IN ('create','rebuild') AND (state IN ('queued','running','awaiting-health') OR ${committedRebalanceStartup})
AND server_deleted_at IS NULL`).get() as { active: number; queued: number };
return { hosts, ...counts, startupLimit: fleetSettings().createConcurrency, updatedAt: new Date().toISOString() };
}
+2 -1
View File
@@ -11,6 +11,7 @@ const CPU_USABLE_FRACTION = 0.95;
export class CapacityError extends Error {}
export interface PlacementOptions {
hostId?: string; libraryId: string; image: string; bindAddress?: string;
excludeHostIds?: string[];
thorough?: boolean; excludeInstanceId?: string;
// Execution only: not-yet-submitted launches wait their turn instead of
// requiring their entire backlog to fit before any one can start.
@@ -100,7 +101,7 @@ function projectedLoad(host: Host, options: PlacementOptions): number {
export function choosePlacement(options: PlacementOptions): { host: Host; port: number; load: number } {
const candidates: { host: Host; port: number; load: number }[] = [];
const reasons: string[] = [];
for (const host of listHosts().filter(host => !options.hostId || host.id === options.hostId)) {
for (const host of listHosts().filter(host => (!options.hostId || host.id === options.hostId) && !options.excludeHostIds?.includes(host.id))) {
try {
requireReadyHost(host.id, { image: options.image, requiredBytes: INSTANCE_STORAGE_BYTES });
assertReadyLibrary(host.id, options.libraryId);