From 46014fe782d424b75e5899a86a5c84823b9e5cb6 Mon Sep 17 00:00:00 2001 From: lyon Date: Mon, 22 Jun 2026 12:27:32 +0800 Subject: [PATCH] feat: add workbench redis cache runtime config --- deploy/deploy.schema.json | 118 ++++++++++++++++++++++++++++++++++++++ deploy/deploy.yaml | 40 +++++++++++++ scripts/gitops-render.mjs | 47 ++++++++++++++- 3 files changed, 203 insertions(+), 2 deletions(-) diff --git a/deploy/deploy.schema.json b/deploy/deploy.schema.json index b7b272ba..4e954ecc 100644 --- a/deploy/deploy.schema.json +++ b/deploy/deploy.schema.json @@ -337,6 +337,7 @@ "workbench": { "$ref": "#/$defs/workbenchUiPolicy" }, "observability": { "$ref": "#/$defs/runtimeObservabilityPolicy" }, "runtimeStore": { "$ref": "#/$defs/runtimeStore" }, + "workbenchRuntime": { "$ref": "#/$defs/workbenchRuntime" }, "sourceRepo": { "type": "string" }, "envReuseServices": { "type": "array", @@ -387,6 +388,123 @@ }, "additionalProperties": false }, + "workbenchRuntime": { + "type": "object", + "properties": { + "cache": { "$ref": "#/$defs/workbenchRuntimeCache" } + }, + "additionalProperties": false + }, + "workbenchRuntimeCache": { + "type": "object", + "properties": { + "redis": { "$ref": "#/$defs/workbenchRuntimeRedisCache" } + }, + "additionalProperties": false + }, + "workbenchRuntimeRedisCache": { + "type": "object", + "required": ["enabled", "namespace", "serviceName", "port", "image", "resources", "memoryPolicy", "ttl", "limits", "timeouts", "metrics", "disableSwitchEnv", "unavailableBehavior"], + "properties": { + "enabled": { "type": "boolean" }, + "namespace": { "type": "string", "pattern": "^hwlab-v[0-9]{2,}$" }, + "serviceName": { + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "image": { "type": "string", "minLength": 1 }, + "resources": { "$ref": "#/$defs/workbenchRuntimeRedisResources" }, + "memoryPolicy": { "$ref": "#/$defs/workbenchRuntimeRedisMemoryPolicy" }, + "ttl": { "$ref": "#/$defs/workbenchRuntimeRedisTtl" }, + "limits": { "$ref": "#/$defs/workbenchRuntimeRedisLimits" }, + "timeouts": { "$ref": "#/$defs/workbenchRuntimeRedisTimeouts" }, + "metrics": { "$ref": "#/$defs/workbenchRuntimeRedisMetrics" }, + "disableSwitchEnv": { "type": "string", "pattern": "^[A-Z0-9_]+$" }, + "unavailableBehavior": { "$ref": "#/$defs/workbenchRuntimeRedisUnavailableBehavior" } + }, + "additionalProperties": false + }, + "workbenchRuntimeRedisResources": { + "type": "object", + "required": ["requests", "limits"], + "properties": { + "requests": { "$ref": "#/$defs/workbenchRuntimeResourcePair" }, + "limits": { "$ref": "#/$defs/workbenchRuntimeResourcePair" } + }, + "additionalProperties": false + }, + "workbenchRuntimeResourcePair": { + "type": "object", + "required": ["cpu", "memory"], + "properties": { + "cpu": { "type": "string", "minLength": 1 }, + "memory": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "workbenchRuntimeRedisMemoryPolicy": { + "type": "object", + "required": ["persistence", "maxMemory", "eviction"], + "properties": { + "persistence": { "type": "string", "enum": ["disabled"] }, + "maxMemory": { "type": "string", "minLength": 1 }, + "eviction": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "workbenchRuntimeRedisTtl": { + "type": "object", + "required": ["sessionsSummaryMs", "terminalTurnMs", "terminalTracePageMs", "runningPageMs"], + "properties": { + "sessionsSummaryMs": { "type": "integer", "minimum": 0 }, + "terminalTurnMs": { "type": "integer", "minimum": 0 }, + "terminalTracePageMs": { "type": "integer", "minimum": 0 }, + "runningPageMs": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false + }, + "workbenchRuntimeRedisLimits": { + "type": "object", + "required": ["maxKeyBytes", "maxPayloadBytes"], + "properties": { + "maxKeyBytes": { "type": "integer", "minimum": 1 }, + "maxPayloadBytes": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + }, + "workbenchRuntimeRedisTimeouts": { + "type": "object", + "required": ["connectMs", "operationMs"], + "properties": { + "connectMs": { "type": "integer", "minimum": 1 }, + "operationMs": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + }, + "workbenchRuntimeRedisMetrics": { + "type": "object", + "required": ["labels"], + "properties": { + "labels": { + "type": "object", + "additionalProperties": { "type": "string", "minLength": 1 } + } + }, + "additionalProperties": false + }, + "workbenchRuntimeRedisUnavailableBehavior": { + "type": "object", + "required": ["livenessFailure", "readMode", "fallbackStateSource"], + "properties": { + "livenessFailure": { "type": "boolean" }, + "readMode": { "type": "string", "enum": ["db-or-degraded"] }, + "fallbackStateSource": { "type": "boolean" } + }, + "additionalProperties": false + }, "workbenchUiPolicy": { "type": "object", "properties": { diff --git a/deploy/deploy.yaml b/deploy/deploy.yaml index c800922c..1b00acd8 100644 --- a/deploy/deploy.yaml +++ b/deploy/deploy.yaml @@ -306,6 +306,46 @@ lanes: queryRetryMaxAttempts: 5 queryRetryInitialDelayMs: 250 queryRetryMaxDelayMs: 5000 + workbenchRuntime: + cache: + redis: + enabled: true + namespace: hwlab-v03 + serviceName: hwlab-workbench-redis + port: 6379 + image: redis:7.2-alpine + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi + memoryPolicy: + persistence: disabled + maxMemory: 192mb + eviction: allkeys-lru + ttl: + sessionsSummaryMs: 30000 + terminalTurnMs: 300000 + terminalTracePageMs: 300000 + runningPageMs: 5000 + limits: + maxKeyBytes: 512 + maxPayloadBytes: 262144 + timeouts: + connectMs: 200 + operationMs: 100 + metrics: + labels: + lane: v03 + service: hwlab-workbench-runtime + cacheKeyClass: workbench-derived-read + disableSwitchEnv: HWLAB_WORKBENCH_CACHE_REDIS_ENABLED + unavailableBehavior: + livenessFailure: false + readMode: db-or-degraded + fallbackStateSource: false envReuseServices: - hwlab-cloud-api - hwlab-workbench-runtime diff --git a/scripts/gitops-render.mjs b/scripts/gitops-render.mjs index ff54b054..567d467a 100644 --- a/scripts/gitops-render.mjs +++ b/scripts/gitops-render.mjs @@ -938,7 +938,8 @@ function deployServicesForProfile(deploy, profile) { copy.env = mergeEnvMaps( copy.env, serviceDeclarationEnvForProfile(deploy, profile, service.serviceId), - runtimeStoreEnvForProfile(deploy, profile, service.serviceId) + runtimeStoreEnvForProfile(deploy, profile, service.serviceId), + workbenchRuntimeRedisEnvForProfile(deploy, profile, service.serviceId) ); return [service.serviceId, copy]; })); @@ -950,7 +951,8 @@ function deployServicesForProfile(deploy, profile) { serviceId: override.serviceId, env: mergeEnvMaps( serviceDeclarationEnvForProfile(deploy, profile, override.serviceId), - runtimeStoreEnvForProfile(deploy, profile, override.serviceId) + runtimeStoreEnvForProfile(deploy, profile, override.serviceId), + workbenchRuntimeRedisEnvForProfile(deploy, profile, override.serviceId) ) }; services.set(override.serviceId, { @@ -1006,6 +1008,47 @@ function runtimeStoreEnvForProfile(deploy, profile, serviceId) { return env; } +function workbenchRuntimeRedisEnvForProfile(deploy, profile, serviceId) { + if (!isRuntimeLane(profile) || serviceId !== "hwlab-workbench-runtime") return {}; + const redis = runtimeLaneConfig(deploy, profile)?.workbenchRuntime?.cache?.redis; + if (!redis || typeof redis !== "object" || Array.isArray(redis)) return {}; + const env = { + HWLAB_WORKBENCH_CACHE_REDIS_ENABLED: booleanEnv(Boolean(redis.enabled)) + }; + const namespace = typeof redis.namespace === "string" ? redis.namespace.trim() : ""; + const serviceName = typeof redis.serviceName === "string" ? redis.serviceName.trim() : ""; + const port = Number.isInteger(redis.port) ? redis.port : null; + if (namespace) env.HWLAB_WORKBENCH_CACHE_REDIS_NAMESPACE = namespace; + if (serviceName) env.HWLAB_WORKBENCH_CACHE_REDIS_SERVICE_NAME = serviceName; + if (port !== null) env.HWLAB_WORKBENCH_CACHE_REDIS_PORT = String(port); + if (namespace && serviceName && port !== null) { + env.HWLAB_WORKBENCH_CACHE_REDIS_URL = `redis://${serviceName}.${namespace}.svc.cluster.local:${port}/0`; + } + const ttl = redis.ttl ?? {}; + if (Number.isInteger(ttl.sessionsSummaryMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_TTL_SESSIONS_SUMMARY_MS = String(ttl.sessionsSummaryMs); + if (Number.isInteger(ttl.terminalTurnMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_TTL_TERMINAL_TURN_MS = String(ttl.terminalTurnMs); + if (Number.isInteger(ttl.terminalTracePageMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_TTL_TERMINAL_TRACE_PAGE_MS = String(ttl.terminalTracePageMs); + if (Number.isInteger(ttl.runningPageMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_TTL_RUNNING_PAGE_MS = String(ttl.runningPageMs); + const limits = redis.limits ?? {}; + if (Number.isInteger(limits.maxKeyBytes)) env.HWLAB_WORKBENCH_CACHE_REDIS_MAX_KEY_BYTES = String(limits.maxKeyBytes); + if (Number.isInteger(limits.maxPayloadBytes)) env.HWLAB_WORKBENCH_CACHE_REDIS_MAX_PAYLOAD_BYTES = String(limits.maxPayloadBytes); + const timeouts = redis.timeouts ?? {}; + if (Number.isInteger(timeouts.connectMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_CONNECT_TIMEOUT_MS = String(timeouts.connectMs); + if (Number.isInteger(timeouts.operationMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_OPERATION_TIMEOUT_MS = String(timeouts.operationMs); + const labels = redis.metrics?.labels; + if (labels && typeof labels === "object" && !Array.isArray(labels)) { + env.HWLAB_WORKBENCH_CACHE_REDIS_METRICS_LABELS = JSON.stringify(labels); + } + const behavior = redis.unavailableBehavior ?? {}; + if (typeof behavior.livenessFailure === "boolean") env.HWLAB_WORKBENCH_CACHE_REDIS_UNAVAILABLE_LIVENESS_FAILURE = booleanEnv(behavior.livenessFailure); + if (typeof behavior.readMode === "string") env.HWLAB_WORKBENCH_CACHE_REDIS_UNAVAILABLE_READ_MODE = behavior.readMode; + if (typeof behavior.fallbackStateSource === "boolean") env.HWLAB_WORKBENCH_CACHE_REDIS_UNAVAILABLE_FALLBACK_STATE_SOURCE = booleanEnv(behavior.fallbackStateSource); + if (typeof redis.disableSwitchEnv === "string" && redis.disableSwitchEnv.trim()) { + env[redis.disableSwitchEnv.trim()] = booleanEnv(Boolean(redis.enabled)); + } + return env; +} + function booleanEnv(value) { return value ? "1" : "0"; }