From 2afdb2d9ea436f18decc7baefa10d23cb34d45ab Mon Sep 17 00:00:00 2001 From: lyon Date: Thu, 18 Jun 2026 00:16:42 +0800 Subject: [PATCH] fix: tune v03 cloud api health probe budget --- deploy/deploy.schema.json | 21 +++++++++++++++++++++ deploy/deploy.yaml | 10 ++++++++++ scripts/gitops-render.mjs | 24 ++++++++++++++++++++++++ scripts/src/ci-plan-lib.mjs | 25 +++++++++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/deploy/deploy.schema.json b/deploy/deploy.schema.json index 5658281a..71722837 100644 --- a/deploy/deploy.schema.json +++ b/deploy/deploy.schema.json @@ -374,6 +374,26 @@ }, "additionalProperties": false }, + "probeTiming": { + "type": "object", + "properties": { + "initialDelaySeconds": { "type": "integer", "minimum": 0 }, + "periodSeconds": { "type": "integer", "minimum": 1 }, + "timeoutSeconds": { "type": "integer", "minimum": 1 }, + "failureThreshold": { "type": "integer", "minimum": 1 }, + "successThreshold": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + }, + "serviceHealthProbe": { + "type": "object", + "properties": { + "readiness": { "$ref": "#/$defs/probeTiming" }, + "liveness": { "$ref": "#/$defs/probeTiming" }, + "startup": { "$ref": "#/$defs/probeTiming" } + }, + "additionalProperties": false + }, "serviceDeclaration": { "type": "object", "required": ["runtimeKind", "entrypoint", "componentPaths"], @@ -383,6 +403,7 @@ "artifactKind": { "type": "string", "enum": ["bun-command", "cloud-web", "skills-bundle", "node-command", "go-service"] }, "healthPath": { "type": "string", "pattern": "^/" }, "healthPort": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "healthProbe": { "$ref": "#/$defs/serviceHealthProbe" }, "componentPaths": { "type": "array", "minItems": 1, diff --git a/deploy/deploy.yaml b/deploy/deploy.yaml index 2e473800..2eb663ac 100644 --- a/deploy/deploy.yaml +++ b/deploy/deploy.yaml @@ -277,6 +277,16 @@ lanes: artifactKind: bun-command healthPath: /health/live healthPort: 6667 + healthProbe: + readiness: + timeoutSeconds: 3 + failureThreshold: 5 + liveness: + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 6 + startup: + timeoutSeconds: 3 componentPaths: - cmd/hwlab-cloud-api/ - cmd/hwlab-codex-api-responses-forwarder/ diff --git a/scripts/gitops-render.mjs b/scripts/gitops-render.mjs index 86baba38..acc235b9 100644 --- a/scripts/gitops-render.mjs +++ b/scripts/gitops-render.mjs @@ -527,6 +527,27 @@ function applyEnvReuseStartupProbe(container) { }; } +function applyServiceHealthProbe(container, healthProbe) { + if (!healthProbe || typeof healthProbe !== "object" || Array.isArray(healthProbe)) return; + for (const [probeName, key] of [["readinessProbe", "readiness"], ["livenessProbe", "liveness"], ["startupProbe", "startup"]]) { + const probe = container?.[probeName]; + const timing = normalizeProbeTiming(healthProbe[key]); + if (!probe || Object.keys(timing).length === 0) continue; + Object.assign(probe, timing); + } +} + +function normalizeProbeTiming(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + const result = {}; + for (const field of ["initialDelaySeconds", "periodSeconds", "timeoutSeconds", "failureThreshold", "successThreshold"]) { + const raw = value[field]; + const minimum = field === "initialDelaySeconds" ? 0 : 1; + if (Number.isInteger(raw) && raw >= minimum) result[field] = raw; + } + return result; +} + function useLocalHealthProbe(container, pathValue = "/health") { if (!container) return; for (const probeName of ["readinessProbe", "livenessProbe"]) { @@ -1129,6 +1150,9 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = }); if (container.name === serviceId) annotateEnvReusePodTemplate(podTemplate.metadata, bootMetadata); } + if (runtimeLane && container.name === serviceId) { + applyServiceHealthProbe(container, deploy?.lanes?.[profile]?.serviceDeclarations?.[serviceId]?.healthProbe); + } if (serviceId === "hwlab-cloud-api" || serviceId === "hwlab-edge-proxy") { upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", runtimeEndpoint); } diff --git a/scripts/src/ci-plan-lib.mjs b/scripts/src/ci-plan-lib.mjs index b632ebb8..6d0f1343 100644 --- a/scripts/src/ci-plan-lib.mjs +++ b/scripts/src/ci-plan-lib.mjs @@ -201,6 +201,7 @@ export async function createCiPlan(options = {}) { artifactKind: model.artifactKind, healthPath: model.healthPath, healthPort: model.healthPort, + healthProbe: model.healthProbe, componentPaths: model.componentPaths, sharedPaths: model.sharedPaths, runtimeDeps: model.runtimeDeps, @@ -306,6 +307,7 @@ export function componentModelsForServices(serviceIds, laneConfig = null, lane = artifactKind: declaration.artifactKind, healthPath: declaration.healthPath, healthPort: declaration.healthPort, + healthProbe: declaration.healthProbe, env: declaration.env, observable: declaration.observable, componentPaths: uniqueSorted(declaration.componentPaths.map(normalizeRepoPath)), @@ -338,6 +340,7 @@ function serviceDeclarationFor(laneConfig, serviceId, lane) { artifactKind, healthPath: normalizeDeclarationString(configured?.healthPath) || "/health/live", healthPort: Number.isInteger(configured?.healthPort) ? configured.healthPort : null, + healthProbe: normalizeServiceHealthProbe(configured?.healthProbe), componentPaths: configured.componentPaths, env: normalizeServiceEnv(configured?.env), observable: configured?.observable === true @@ -349,6 +352,27 @@ function normalizeServiceEnv(value) { return Object.fromEntries(Object.entries(value).map(([key, raw]) => [String(key), raw == null ? null : String(raw)]).sort(([left], [right]) => left.localeCompare(right))); } +function normalizeServiceHealthProbe(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + const result = {}; + for (const key of ["readiness", "liveness", "startup"]) { + const timing = normalizeProbeTiming(value[key]); + if (Object.keys(timing).length > 0) result[key] = timing; + } + return result; +} + +function normalizeProbeTiming(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + const result = {}; + for (const field of ["initialDelaySeconds", "periodSeconds", "timeoutSeconds", "failureThreshold", "successThreshold"]) { + const raw = value[field]; + const minimum = field === "initialDelaySeconds" ? 0 : 1; + if (Number.isInteger(raw) && raw >= minimum) result[field] = raw; + } + return result; +} + function requireDeclarationString(value, lane, label) { const text = normalizeDeclarationString(value); if (!text) throw new Error(`deploy.lanes.${lane}.${label} is required`); @@ -571,6 +595,7 @@ function serviceRuntimeConfigIdentity(deployJson, lane, serviceId) { entrypoint: normalizeRepoPath(declaration.entrypoint), healthPath: normalizeDeclarationString(declaration.healthPath), healthPort: Number.isInteger(declaration.healthPort) ? declaration.healthPort : null, + healthProbe: normalizeServiceHealthProbe(declaration.healthProbe), env: normalizeServiceEnv(declaration.env), observable: declaration.observable === true };