90 lines
6.1 KiB
TypeScript
90 lines
6.1 KiB
TypeScript
import type Database from 'better-sqlite3';
|
|
|
|
const initialized = new WeakSet<Database.Database>();
|
|
export function ensureHostSchema(handle: Database.Database): void {
|
|
if (initialized.has(handle)) return;
|
|
handle.exec(`
|
|
CREATE TABLE IF NOT EXISTS ec_hosts (
|
|
id TEXT PRIMARY KEY, name TEXT NOT NULL, hostname TEXT NOT NULL DEFAULT '',
|
|
mode TEXT NOT NULL DEFAULT 'active' CHECK (mode IN ('active','draining','disabled')),
|
|
port_min INTEGER NOT NULL DEFAULT 28960, port_max INTEGER NOT NULL DEFAULT 28980,
|
|
bind_address TEXT NOT NULL DEFAULT '0.0.0.0', public_address TEXT NOT NULL DEFAULT '',
|
|
minimum_free_bytes INTEGER NOT NULL DEFAULT 1073741824,
|
|
created_at TEXT NOT NULL, enrolled_at TEXT, last_heartbeat TEXT, inventory TEXT NOT NULL DEFAULT '{}', deleted_at TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS ec_host_enrollments (
|
|
id TEXT PRIMARY KEY, host_id TEXT NOT NULL REFERENCES ec_hosts(id), token_hash TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL, expires_at TEXT NOT NULL, consumed_at TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS ec_host_credentials (
|
|
id TEXT PRIMARY KEY, host_id TEXT NOT NULL REFERENCES ec_hosts(id), token_hash TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL, revoked_at TEXT, last_used_at TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS ec_host_operations (
|
|
id TEXT PRIMARY KEY, host_id TEXT NOT NULL REFERENCES ec_hosts(id), kind TEXT NOT NULL,
|
|
instance_id TEXT, generation INTEGER, idempotency_key TEXT,
|
|
payload TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'pending'
|
|
CHECK (state IN ('pending','accepted','running','completed','failed','unknown')),
|
|
result TEXT, error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, last_delivered_at TEXT,
|
|
UNIQUE(host_id, idempotency_key)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ec_host_operations_queue ON ec_host_operations(host_id, state, created_at);
|
|
-- Polling checks cancellations and preparation by kind across retained
|
|
-- history. The queue index cannot narrow those lookups beyond host_id,
|
|
-- causing a history scan for every queued command on Node's main thread.
|
|
CREATE INDEX IF NOT EXISTS ec_host_operations_kind ON ec_host_operations(host_id, kind, created_at);
|
|
CREATE TABLE IF NOT EXISTS ec_host_operation_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, operation_id TEXT NOT NULL REFERENCES ec_host_operations(id),
|
|
state TEXT NOT NULL, created_at TEXT NOT NULL, error TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS ec_host_ports (
|
|
host_id TEXT NOT NULL REFERENCES ec_hosts(id), instance_id TEXT NOT NULL,
|
|
bind_address TEXT NOT NULL, protocol TEXT NOT NULL CHECK (protocol IN ('udp','tcp')),
|
|
port INTEGER NOT NULL CHECK (port BETWEEN 1 AND 65535), created_at TEXT NOT NULL,
|
|
PRIMARY KEY(host_id,bind_address,protocol,port)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ec_host_ports_instance ON ec_host_ports(instance_id);
|
|
CREATE TABLE IF NOT EXISTS ec_host_resource_samples (
|
|
host_id TEXT NOT NULL REFERENCES ec_hosts(id), sampled_at TEXT NOT NULL,
|
|
cpu_count REAL, cpu_usage_percent REAL, memory_total REAL, memory_available REAL,
|
|
PRIMARY KEY(host_id, sampled_at)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS ec_instance_resource_samples (
|
|
host_id TEXT NOT NULL, instance_id TEXT NOT NULL, generation INTEGER NOT NULL,
|
|
phase TEXT NOT NULL CHECK(phase IN ('startup','running')), bucket TEXT NOT NULL,
|
|
cpu_cores REAL NOT NULL, memory_bytes REAL NOT NULL,
|
|
PRIMARY KEY(host_id,instance_id,generation,phase,bucket)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ec_instance_resources_host ON ec_instance_resource_samples(host_id,phase,bucket DESC);
|
|
CREATE INDEX IF NOT EXISTS ec_instance_resources_fleet ON ec_instance_resource_samples(phase,bucket DESC);
|
|
CREATE INDEX IF NOT EXISTS ec_instance_resources_retention ON ec_instance_resource_samples(bucket);
|
|
CREATE TABLE IF NOT EXISTS ec_resource_allowances (
|
|
scope TEXT NOT NULL, phase TEXT NOT NULL, cpu_cores REAL NOT NULL, memory_bytes REAL NOT NULL,
|
|
samples INTEGER NOT NULL, window_start TEXT NOT NULL, observed_at TEXT NOT NULL, computed_at TEXT NOT NULL,
|
|
usage_margin REAL NOT NULL DEFAULT 1,
|
|
PRIMARY KEY(scope,phase)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS ec_host_decommissions (
|
|
host_id TEXT PRIMARY KEY REFERENCES ec_hosts(id),
|
|
operation_id TEXT NOT NULL UNIQUE REFERENCES ec_host_operations(id),
|
|
token_hash TEXT NOT NULL, stage TEXT NOT NULL DEFAULT 'queued'
|
|
CHECK(stage IN ('queued','running','cleaned','failed','completed')),
|
|
requested_at TEXT NOT NULL, updated_at TEXT NOT NULL, cleaned_at TEXT, error TEXT
|
|
);
|
|
`);
|
|
const columns = handle.prepare('PRAGMA table_info(ec_host_operations)').all() as { name: string }[];
|
|
if (!columns.some(column => column.name === 'last_delivered_at')) handle.exec('ALTER TABLE ec_host_operations ADD COLUMN last_delivered_at TEXT');
|
|
const hostColumns = handle.prepare('PRAGMA table_info(ec_hosts)').all() as { name: string }[];
|
|
if (!hostColumns.some(column => column.name === 'deleted_at')) handle.exec('ALTER TABLE ec_hosts ADD COLUMN deleted_at TEXT');
|
|
const allowanceColumns = handle.prepare('PRAGMA table_info(ec_resource_allowances)').all() as { name: string }[];
|
|
if (!allowanceColumns.some(column => column.name === 'usage_margin')) {
|
|
// Existing caches include 25% padding. Preserve learned evidence while
|
|
// removing that padding exactly once, including dormant startup models.
|
|
handle.transaction(() => {
|
|
handle.exec('ALTER TABLE ec_resource_allowances ADD COLUMN usage_margin REAL NOT NULL DEFAULT 1');
|
|
handle.exec('UPDATE ec_resource_allowances SET cpu_cores=cpu_cores/1.25, memory_bytes=CAST(memory_bytes/1.25 AS INTEGER)');
|
|
})();
|
|
}
|
|
initialized.add(handle);
|
|
}
|