Phase6
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 8s
CI / Go — edge-gateway build (push) Successful in 13s

This commit is contained in:
smueller
2026-06-26 13:11:28 +02:00
parent d29a02f2a4
commit 49a98ee31d
81 changed files with 1462 additions and 53 deletions

View File

@@ -0,0 +1,38 @@
-- Phase 6: Multi-node scheduling, start queue, node capacity
CREATE TYPE "StartQueueStatus" AS ENUM ('QUEUED', 'DISPATCHED', 'CANCELLED', 'EXPIRED');
ALTER TYPE "GameServerStatus" ADD VALUE IF NOT EXISTS 'QUEUED';
ALTER TYPE "GameServerStatus" ADD VALUE IF NOT EXISTS 'UNKNOWN';
ALTER TABLE "game_nodes"
ADD COLUMN IF NOT EXISTS "maxRamMb" INTEGER NOT NULL DEFAULT 16384,
ADD COLUMN IF NOT EXISTS "platformRamMb" INTEGER NOT NULL DEFAULT 2048,
ADD COLUMN IF NOT EXISTS "drainRequestedAt" TIMESTAMPTZ(3);
CREATE TABLE "server_start_queue" (
"id" TEXT NOT NULL,
"serverId" TEXT NOT NULL,
"nodeId" TEXT NOT NULL,
"status" "StartQueueStatus" NOT NULL DEFAULT 'QUEUED',
"priority" INTEGER NOT NULL DEFAULT 0,
"correlationId" TEXT,
"requestedAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMPTZ(3),
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "server_start_queue_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "server_start_queue_serverId_key" ON "server_start_queue"("serverId");
CREATE INDEX "server_start_queue_status_priority_requestedAt_idx" ON "server_start_queue"("status", "priority", "requestedAt");
CREATE INDEX "server_start_queue_nodeId_idx" ON "server_start_queue"("nodeId");
ALTER TABLE "server_start_queue"
ADD CONSTRAINT "server_start_queue_serverId_fkey"
FOREIGN KEY ("serverId") REFERENCES "game_servers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "server_start_queue"
ADD CONSTRAINT "server_start_queue_nodeId_fkey"
FOREIGN KEY ("nodeId") REFERENCES "game_nodes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -34,11 +34,13 @@ enum GameServerStatus {
PROVISIONING
INSTALLING
STOPPED
QUEUED
STARTING
RUNNING
STOPPING
BACKING_UP
RESTORING
UNKNOWN
ERROR
DELETING
DELETED
@@ -57,6 +59,13 @@ enum AllocationStatus {
RELEASED
}
enum StartQueueStatus {
QUEUED
DISPATCHED
CANCELLED
EXPIRED
}
enum JobStatus {
PENDING
RUNNING
@@ -282,6 +291,7 @@ model GameServer {
backupSchedule BackupSchedule?
worldUploads WorldUpload[]
installedAddons InstalledAddon[]
startQueueEntry ServerStartQueueEntry?
@@index([userId])
@@index([nodeId])
@@ -333,18 +343,22 @@ model GameNode {
hostname String
status GameNodeStatus @default(OFFLINE)
maxServers Int @default(10)
maxRamMb Int @default(16384)
platformRamMb Int @default(2048)
activeServers Int @default(0)
lastHeartbeatAt DateTime? @db.Timestamptz(3)
agentVersion String?
enrollmentTokenHash String?
enrolledAt DateTime? @db.Timestamptz(3)
drainRequestedAt DateTime? @db.Timestamptz(3)
metadata Json?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
gameServers GameServer[]
heartbeats GameNodeHeartbeat[]
allocations GameServerAllocation[]
gameServers GameServer[]
heartbeats GameNodeHeartbeat[]
allocations GameServerAllocation[]
startQueueEntries ServerStartQueueEntry[]
@@map("game_nodes")
}
@@ -362,6 +376,26 @@ model GameNodeHeartbeat {
@@map("game_node_heartbeats")
}
model ServerStartQueueEntry {
id String @id @default(uuid())
serverId String @unique
nodeId String
status StartQueueStatus @default(QUEUED)
priority Int @default(0)
correlationId String?
requestedAt DateTime @default(now()) @db.Timestamptz(3)
expiresAt DateTime? @db.Timestamptz(3)
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
server GameServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
node GameNode @relation(fields: [nodeId], references: [id])
@@index([status, priority, requestedAt])
@@index([nodeId])
@@map("server_start_queue")
}
model Plan {
id String @id @default(uuid())
name String @unique

View File

@@ -5,10 +5,15 @@ import { hashToken } from '@hexahost/auth';
/** Stable dev node ID — must match NODE_ID in .env for local agent */
export const DEV_NODE_ID = '00000000-0000-4000-8000-000000000001';
export const DEV_NODE_ID_2 = '00000000-0000-4000-8000-000000000002';
/** Shown once in seed output; hash stored in DB */
export const DEV_NODE_ENROLLMENT_TOKEN =
'local-dev-enrollment-token-change-me-32chars';
export const DEV_NODE_2_ENROLLMENT_TOKEN =
'local-dev-node2-enrollment-token-32ch';
async function main(): Promise<void> {
const prisma = new PrismaClient();
@@ -38,19 +43,46 @@ async function main(): Promise<void> {
hostname: 'localhost',
status: 'OFFLINE',
maxServers: 10,
maxRamMb: 16384,
platformRamMb: 2048,
enrollmentTokenHash: hashToken(DEV_NODE_ENROLLMENT_TOKEN),
},
update: {
name: 'local-dev',
hostname: 'localhost',
maxRamMb: 16384,
platformRamMb: 2048,
enrollmentTokenHash: hashToken(DEV_NODE_ENROLLMENT_TOKEN),
},
});
const devNode2 = await prisma.gameNode.upsert({
where: { id: DEV_NODE_ID_2 },
create: {
id: DEV_NODE_ID_2,
name: 'local-dev-2',
hostname: 'localhost',
status: 'OFFLINE',
maxServers: 10,
maxRamMb: 16384,
platformRamMb: 2048,
enrollmentTokenHash: hashToken(DEV_NODE_2_ENROLLMENT_TOKEN),
},
update: {
name: 'local-dev-2',
hostname: 'localhost',
maxRamMb: 16384,
platformRamMb: 2048,
enrollmentTokenHash: hashToken(DEV_NODE_2_ENROLLMENT_TOKEN),
},
});
console.log('Seed complete:');
console.log(` Plan Free: ${freePlan.id}`);
console.log(` Dev node: ${devNode.id} (${devNode.name})`);
console.log(` Dev node 2: ${devNode2.id} (${devNode2.name})`);
console.log(` Enrollment token (set NODE_TOKEN): ${DEV_NODE_ENROLLMENT_TOKEN}`);
console.log(` Node 2 enrollment token: ${DEV_NODE_2_ENROLLMENT_TOKEN}`);
} finally {
await prisma.$disconnect();
}

View File

@@ -19,6 +19,7 @@ export type {
InstalledAddon,
GameNode,
GameNodeHeartbeat,
ServerStartQueueEntry,
Plan,
FeatureFlag,
SystemSetting,

File diff suppressed because one or more lines are too long