From c318e8eadf017b3fc82c9363a7cd988f8cc9fa3c Mon Sep 17 00:00:00 2001 From: lyon Date: Sat, 20 Jun 2026 08:01:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=AF=E6=8C=81=20runtime=20store=20?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=B1=A0=20YAML=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/deploy.schema.json | 15 +++++++++++++++ internal/db/runtime-store.test.ts | 8 ++++++-- internal/db/runtime-store.ts | 21 ++++++++++++++++++--- scripts/gitops-render.mjs | 20 ++++++++++++++++++-- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/deploy/deploy.schema.json b/deploy/deploy.schema.json index be5007e0..b6ee3104 100644 --- a/deploy/deploy.schema.json +++ b/deploy/deploy.schema.json @@ -334,6 +334,7 @@ "runtimePath": { "type": "string" }, "imageTagMode": { "type": "string", "enum": ["short", "full"] }, "workbench": { "$ref": "#/$defs/workbenchUiPolicy" }, + "runtimeStore": { "$ref": "#/$defs/runtimeStore" }, "sourceRepo": { "type": "string" }, "envReuseServices": { "type": "array", @@ -359,6 +360,20 @@ }, "additionalProperties": false }, + "runtimeStore": { + "type": "object", + "properties": { + "postgres": { "$ref": "#/$defs/postgresRuntimeStore" } + }, + "additionalProperties": false + }, + "postgresRuntimeStore": { + "type": "object", + "properties": { + "poolMax": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + }, "workbenchUiPolicy": { "type": "object", "properties": { diff --git a/internal/db/runtime-store.test.ts b/internal/db/runtime-store.test.ts index a5b9738c..0eb60e0d 100644 --- a/internal/db/runtime-store.test.ts +++ b/internal/db/runtime-store.test.ts @@ -137,11 +137,13 @@ test("postgres pool config makes HWLAB_CLOUD_DB_SSL_MODE authoritative over URL const disabled = buildPostgresPoolConfig({ dbUrl: "postgres://hwlab_user:fixture-pass@db.example.test:5432/hwlab?sslmode=require&application_name=hwlab", sslMode: "disable", - timeoutMs: "2500" + timeoutMs: "2500", + poolMax: "20" }); assert.equal(disabled.ssl, false); assert.equal(disabled.connectionTimeoutMillis, 2500); + assert.equal(disabled.max, 20); const disabledUrl = new URL(disabled.connectionString); assert.equal(disabledUrl.searchParams.get("sslmode"), null); assert.equal(disabledUrl.searchParams.get("ssl"), null); @@ -163,7 +165,8 @@ test("configured postgres runtime passes normalized pool config to pg", async () env: { HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres", HWLAB_CLOUD_RUNTIME_DURABLE: "true", - HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS: "1800" + HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS: "1800", + HWLAB_CLOUD_DB_POOL_MAX: "20" }, dbUrl: "postgres://hwlab_user:fixture-pass@db.example.test:5432/hwlab?sslmode=require", sslMode: "disable", @@ -186,6 +189,7 @@ test("configured postgres runtime passes normalized pool config to pg", async () assert.equal(pools.length, 1); assert.equal(pools[0].config.ssl, false); assert.equal(pools[0].config.connectionTimeoutMillis, 1800); + assert.equal(pools[0].config.max, 20); assert.equal(new URL(pools[0].config.connectionString).searchParams.get("sslmode"), null); assert.equal(store.summary().blocker, RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED); assert.equal(store.summary().durabilityContract.blockedLayer, "schema"); diff --git a/internal/db/runtime-store.ts b/internal/db/runtime-store.ts index 66da7420..e25ce7e6 100644 --- a/internal/db/runtime-store.ts +++ b/internal/db/runtime-store.ts @@ -36,6 +36,7 @@ export const RUNTIME_DURABILITY_REQUIRED_EVIDENCE = "runtime_adapter_schema_migr export const RUNTIME_ADAPTER_ENV = "HWLAB_CLOUD_RUNTIME_ADAPTER"; export const RUNTIME_DURABLE_ENV = "HWLAB_CLOUD_RUNTIME_DURABLE"; +export const RUNTIME_DB_POOL_MAX_ENV = "HWLAB_CLOUD_DB_POOL_MAX"; const requiredPostgresSchema = CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS; @@ -88,12 +89,14 @@ export function createConfiguredCloudRuntimeStore(options = {}) { return createCloudRuntimeStore(options); } -export function buildPostgresPoolConfig({ dbUrl, sslMode = "require", timeoutMs } = {}) { +export function buildPostgresPoolConfig({ dbUrl, sslMode = "require", timeoutMs, poolMax } = {}) { const normalizedSslMode = normalizePostgresSslMode(sslMode); + const normalizedPoolMax = normalizeRuntimePoolMax(poolMax); return { connectionString: normalizePostgresConnectionStringForSslMode(dbUrl, normalizedSslMode), ssl: postgresSslOption(normalizedSslMode), - connectionTimeoutMillis: normalizeRuntimeTimeoutMs(timeoutMs) + connectionTimeoutMillis: normalizeRuntimeTimeoutMs(timeoutMs), + ...(normalizedPoolMax !== null ? { max: normalizedPoolMax } : {}) }; } @@ -1446,7 +1449,8 @@ export class PostgresCloudRuntimeStore { this.pool = new Pool(buildPostgresPoolConfig({ dbUrl: this.dbUrl, sslMode: this.sslMode, - timeoutMs: this.env?.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS + timeoutMs: this.env?.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS, + poolMax: this.env?.[RUNTIME_DB_POOL_MAX_ENV] })); return this.pool; } @@ -2232,3 +2236,14 @@ function normalizeRuntimeTimeoutMs(value) { } return Math.min(parsed, 5000); } + +function normalizeRuntimePoolMax(value) { + if (value === undefined || value === null || String(value).trim() === "") return null; + const parsed = Number.parseInt(String(value), 10); + if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== String(value).trim()) { + const error = new Error(`${RUNTIME_DB_POOL_MAX_ENV} must be a positive integer`); + error.code = "HWLAB_CLOUD_DB_POOL_MAX_INVALID"; + throw error; + } + return parsed; +} diff --git a/scripts/gitops-render.mjs b/scripts/gitops-render.mjs index fc5d47e4..0fd2ee74 100644 --- a/scripts/gitops-render.mjs +++ b/scripts/gitops-render.mjs @@ -935,7 +935,11 @@ function deployServicesForProfile(deploy, profile) { .filter((service) => allowed.has(service.serviceId)) .map((service) => { const copy = cloneJson(service); - copy.env = mergeEnvMaps(copy.env, serviceDeclarationEnvForProfile(deploy, profile, service.serviceId)); + copy.env = mergeEnvMaps( + copy.env, + serviceDeclarationEnvForProfile(deploy, profile, service.serviceId), + runtimeStoreEnvForProfile(deploy, profile, service.serviceId) + ); return [service.serviceId, copy]; })); const laneServices = isRuntimeLane(profile) && Array.isArray(deploy.lanes?.[profile]?.services) @@ -944,7 +948,10 @@ function deployServicesForProfile(deploy, profile) { for (const override of laneServices) { const existing = services.get(override.serviceId) ?? { serviceId: override.serviceId, - env: serviceDeclarationEnvForProfile(deploy, profile, override.serviceId) + env: mergeEnvMaps( + serviceDeclarationEnvForProfile(deploy, profile, override.serviceId), + runtimeStoreEnvForProfile(deploy, profile, override.serviceId) + ) }; services.set(override.serviceId, { ...existing, @@ -972,6 +979,15 @@ function runtimeWorkbenchTraceTimelinePolicy(deploy, profile) { return Object.keys(result).length > 0 ? result : null; } +function runtimeStoreEnvForProfile(deploy, profile, serviceId) { + if (!isRuntimeLane(profile) || serviceId !== "hwlab-cloud-api") return {}; + const postgres = runtimeLaneConfig(deploy, profile)?.runtimeStore?.postgres; + if (!postgres || typeof postgres !== "object" || Array.isArray(postgres)) return {}; + const env = {}; + if (Number.isInteger(postgres.poolMax)) env.HWLAB_CLOUD_DB_POOL_MAX = String(postgres.poolMax); + return env; +} + function booleanEnv(value) { return value ? "1" : "0"; }