Use live host usage and running estimates for collection admission

This commit is contained in:
2026-10-04 23:41:48 -04:00
parent b0f592b41c
commit caa7672bec
3 changed files with 70 additions and 37 deletions
+33 -2
View File
@@ -730,6 +730,37 @@ describe('automatic capacity admission and placement', () => {
expect(capacityCalibration(hostId).startup.source).toBe('host');
expect(queueNativeCreation(input)).toMatchObject({ ok: true });
});
it('queues a collection at running demand and releases startup reservations as live usage arrives', async () => {
await prepareLibrary();
const { heartbeat } = await import('../../src/lib/server/hosts/index.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js');
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
const { previewCollection, queueCollection } = await import('../../src/lib/server/fleet/collections.js');
const GiB = 1024 ** 3;
const measured = { ...inventory(), cpuCount: 4, cpuUsagePercent: 5, memoryTotal: 4 * GiB, memoryAvailable: 3.75 * GiB };
heartbeat(hostId, measured);
saveFleetSettings({ ...fleetSettings(), createConcurrency: 3 });
const insert = db().prepare(`INSERT INTO ec_resource_allowances
(scope,phase,cpu_cores,memory_bytes,samples,window_start,observed_at,computed_at) VALUES(?,?,?,?,30,?,?,?)`);
const now = new Date().toISOString();
const start = new Date(Date.now() - 30 * 60_000).toISOString();
insert.run(hostId, 'startup', 2, 2 * GiB, start, now, now);
insert.run(hostId, 'running', 0.2, GiB / 4, start, now, now);
const blueprint = saveBlueprint({ name: 'Staggered lobbies', entries: [{ game: 't5', mapSlug: 'theater', count: 3 }] });
const preview = previewCollection(blueprint.id);
expect(preview.ready).toBe(true);
expect(queueCollection({ previewId: preview.id, keys: Object.fromEntries(preview.servers.map(server => [server.name!, 'key'])) }).ok).toBe(true);
await advance();
const creates = () => db().prepare("SELECT instance_id FROM ec_host_operations WHERE kind='instance.create'").all() as { instance_id: string }[];
expect(creates()).toHaveLength(1);
await advance();
expect(creates()).toHaveLength(1); // No heartbeat yet: the first launch still reserves startup resources.
heartbeat(hostId, { ...measured, cpuUsagePercent: 10, memoryAvailable: 3.5 * GiB,
instanceResources: [{ instanceId: creates()[0].instance_id, generation: 1, uptimeSeconds: 60, cpuCores: 0.2, memoryBytes: GiB / 4 }] });
await advance();
expect(creates()).toHaveLength(2); // Its actual usage replaces the peak even before health verification.
});
it('charges queued reservations and releases them on cancellation', async () => {
await prepareLibrary();
const { heartbeat } = await import('../../src/lib/server/hosts/index.js');
@@ -801,8 +832,8 @@ describe('automatic capacity admission and placement', () => {
expect(first.ok).toBe(true); if (!first.ok) return;
await advance();
const instance = nativeInstance(first.name)!;
// 0.2 GiB OS + 0.8 GiB container. The remaining 0.2 GiB of its 1 GiB
// reservation plus a new 1 GiB server fit below 2.53 GiB usable memory.
// 0.2 GiB OS + 0.8 GiB container are already in host usage.
// Adding a new 1 GiB server fits below 2.53 GiB usable memory.
heartbeat(hostId, { ...host, memoryAvailable: 2 * GiB, instanceResources: [{ instanceId: instance.id,
generation: instance.generation, uptimeSeconds: 60, cpuCores: 0.1, memoryBytes: Math.floor(0.8 * GiB) }] });
const second = queueNativeCreation(input);