fix(hwpod): 使用 YAML 数据库和健康超时
This commit is contained in:
@@ -28,8 +28,9 @@ export function hwpodSpecRegistryFromEnv(env: Record<string, string | undefined>
|
||||
const databaseUrl = requiredEnv(env, "HWPOD_SPEC_DATABASE_URL");
|
||||
const schema = postgresIdentifier(requiredEnv(env, "HWPOD_SPEC_DATABASE_SCHEMA"), "HWPOD_SPEC_DATABASE_SCHEMA");
|
||||
const table = postgresIdentifier(requiredEnv(env, "HWPOD_SPEC_DATABASE_TABLE"), "HWPOD_SPEC_DATABASE_TABLE");
|
||||
const connectionTimeoutMs = positiveIntegerEnv(env, "HWPOD_SPEC_DATABASE_CONNECTION_TIMEOUT_MS");
|
||||
const builtIns = parseBuiltIns(env.HWPOD_BUILTIN_SPEC_DOCUMENTS_JSON);
|
||||
return createHwpodSpecRegistry({ store: createPostgresHwpodRuntimeSpecStore({ databaseUrl, schema, table }), builtIns });
|
||||
return createHwpodSpecRegistry({ store: createPostgresHwpodRuntimeSpecStore({ databaseUrl, schema, table, connectionTimeoutMs }), builtIns });
|
||||
}
|
||||
|
||||
export function createHwpodSpecRegistry(options: { store: HwpodRuntimeSpecStore; builtIns?: BuiltInInput[]; now?: () => string }) {
|
||||
@@ -85,13 +86,14 @@ export function createHwpodSpecRegistry(options: { store: HwpodRuntimeSpecStore;
|
||||
return { list, get, create, update, delete: remove, close: async () => store.close?.() };
|
||||
}
|
||||
|
||||
export function createPostgresHwpodRuntimeSpecStore(options: { databaseUrl: string; schema: string; table: string }): HwpodRuntimeSpecStore {
|
||||
export function createPostgresHwpodRuntimeSpecStore(options: { databaseUrl: string; schema: string; table: string; connectionTimeoutMs: number }): HwpodRuntimeSpecStore {
|
||||
const schema = postgresIdentifier(options.schema, "schema");
|
||||
const table = postgresIdentifier(options.table, "table");
|
||||
const qualifiedTable = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
||||
const pool = new Pool(buildPostgresPoolConfig({
|
||||
dbUrl: options.databaseUrl,
|
||||
sslMode: postgresSslModeFromConnectionString(options.databaseUrl) ?? "disable",
|
||||
timeoutMs: options.connectionTimeoutMs,
|
||||
}));
|
||||
let ready: Promise<void> | undefined;
|
||||
|
||||
@@ -187,6 +189,7 @@ function normalizeBuiltIns(inputs: BuiltInInput[]) {
|
||||
}
|
||||
|
||||
function requiredEnv(env: Record<string, string | undefined>, key: string) { const value = String(env[key] ?? "").trim(); if (!value) throw codedError("hwpod_spec_database_config_required", `${key} is required`); return value; }
|
||||
function positiveIntegerEnv(env: Record<string, string | undefined>, key: string) { const value = Number(requiredEnv(env, key)); if (!Number.isSafeInteger(value) || value <= 0) throw codedError("invalid_hwpod_spec_database_config", `${key} must be a positive integer`); return value; }
|
||||
function postgresIdentifier(value: string, name: string) { if (!/^[a-z_][a-z0-9_]*$/u.test(value)) throw codedError("invalid_hwpod_spec_database_identifier", `${name} must be a lowercase PostgreSQL identifier`); return value; }
|
||||
function quoteIdentifier(value: string) { return `"${postgresIdentifier(value, "identifier")}"`; }
|
||||
function isoTimestamp(value: unknown) { const date = value instanceof Date ? value : new Date(String(value ?? "")); if (!Number.isFinite(date.getTime())) throw codedError("invalid_hwpod_runtime_spec", "runtime HWPOD spec timestamp is invalid"); return date.toISOString(); }
|
||||
|
||||
@@ -29,11 +29,12 @@ async function start(service: ServiceName, stateDir: string, stateFile: string,
|
||||
await mkdir(stateDir, { recursive: true });
|
||||
const endpoints = serviceEndpoints(service, env);
|
||||
const command = serviceCommand(service);
|
||||
const healthTimeoutMs = positiveIntegerEnv(env, "HWPOD_HEALTH_TIMEOUT_MS");
|
||||
const logFd = openSync(logFile, "w");
|
||||
const child = spawn(command[0], command.slice(1), { cwd: service === "web" ? path.join(cwd, "web/hwlab-cloud-web") : cwd, env: serviceEnv(service, env), detached: true, stdio: ["ignore", logFd, logFd] });
|
||||
closeSync(logFd);
|
||||
child.unref();
|
||||
const state = { service, pid: child.pid, status: "starting", command, cwd, logFile, endpoints, healthUrl: endpoints.probe.url, startedAt: new Date().toISOString() };
|
||||
const state = { service, pid: child.pid, status: "starting", command, cwd, logFile, endpoints, healthUrl: endpoints.probe.url, healthTimeoutMs, startedAt: new Date().toISOString() };
|
||||
await writeFile(stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
||||
return { ok: true, operation: `hwpod.service.${service}.start`, ...state, nextCommand: `hwlab-cli hwpod service ${service} status` };
|
||||
}
|
||||
@@ -49,7 +50,9 @@ async function stop(service: ServiceName, stateFile: string, logFile: string) {
|
||||
async function status(service: ServiceName, stateFile: string, logFile: string) {
|
||||
const state = await readState(stateFile);
|
||||
const processRunning = Boolean(state?.pid && alive(state.pid));
|
||||
const health = processRunning && state?.healthUrl ? await probeHealth(state.healthUrl) : { ok: false, status: null, error: processRunning ? "health-url-not-declared" : "process-not-running" };
|
||||
const health = processRunning && state?.healthUrl && state?.healthTimeoutMs
|
||||
? await probeHealth(state.healthUrl, state.healthTimeoutMs)
|
||||
: { ok: false, status: null, error: processRunning ? "health-config-not-declared" : "process-not-running" };
|
||||
const running = processRunning && health.ok;
|
||||
return { ok: running, operation: `hwpod.service.${service}.status`, service, status: running ? "running" : processRunning ? "degraded" : "stopped", pid: processRunning ? state.pid : null, endpoints: state?.endpoints ?? null, health, stateFile, logFile, nextCommand: `hwlab-cli hwpod service ${service} logs` };
|
||||
}
|
||||
@@ -83,8 +86,9 @@ function endpoint(host: string, port: string, pathname: string) { const numericP
|
||||
function validPort(value: string) { const port = Number(value); if (!Number.isInteger(port) || port < 1 || port > 65535) throw codedError("invalid_service_port", `invalid HWPOD service port: ${value}`); return port; }
|
||||
function validService(value: string): ServiceName { if (value === "api" || value === "worker" || value === "web") return value; throw codedError("invalid_service", "HWPOD service must be api, worker, or web"); }
|
||||
function requiredEnv(env: Record<string, string | undefined>, key: string) { const value = String(env[key] ?? "").trim(); if (!value) throw codedError("native_exposure_config_required", `${key} is required from YAML-first HWPOD native development config`); return value; }
|
||||
function positiveIntegerEnv(env: Record<string, string | undefined>, key: string) { const value = Number(requiredEnv(env, key)); if (!Number.isSafeInteger(value) || value <= 0) throw codedError("native_exposure_config_invalid", `${key} must be a positive integer`); return value; }
|
||||
function fixedHttpsOrigin(value: string, key: string) { try { const parsed = new URL(value); if (parsed.protocol === "https:" && parsed.pathname === "/" && !parsed.search && !parsed.hash && !parsed.port) return parsed.origin; } catch {} throw codedError("native_exposure_config_invalid", `${key} must be a pathless HTTPS origin`); }
|
||||
async function readState(file: string) { return JSON.parse(await readFile(file, "utf8").catch(() => "null")); }
|
||||
function alive(pid: number) { try { process.kill(pid, 0); return true; } catch { return false; } }
|
||||
async function probeHealth(url: string) { try { const response = await fetch(url, { signal: AbortSignal.timeout(1000) }); return { ok: response.ok, status: response.status, url }; } catch (error) { return { ok: false, status: null, url, error: error instanceof Error ? error.message : String(error) }; } }
|
||||
async function probeHealth(url: string, timeoutMs: number) { try { const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }); return { ok: response.ok, status: response.status, url }; } catch (error) { return { ok: false, status: null, url, error: error instanceof Error ? error.message : String(error) }; } }
|
||||
function codedError(code: string, message: string, details?: unknown) { return Object.assign(new Error(message), { code, details }); }
|
||||
|
||||
Reference in New Issue
Block a user