已合并
delete useless k8s service env var #391
王明琦创建于 7月23日
delete useless k8s service env var #391
已合并
共 7 个文件变更+155-9
| @@ -17,6 +17,7 @@ import os | |||
| 17 | from pathlib import Path | 17 | from pathlib import Path |
| 18 | from urllib.parse import quote | 18 | from urllib.parse import quote |
| 19 | 19 | ||
| 20 | +from openjiuwen_runtime.foundation.db.engine_options import build_async_engine_kwargs | ||
| 20 | from openjiuwen_runtime.foundation.log import get_logger | 21 | from openjiuwen_runtime.foundation.log import get_logger |
| 21 | 22 | ||
| 22 | _log = get_logger(__name__) | 23 | _log = get_logger(__name__) |
| @@ -107,7 +108,7 @@ class MemoryEngineManager: | |||
| 107 | async_database_url = get_async_database_url(sync_database_url) | 108 | async_database_url = get_async_database_url(sync_database_url) |
| 108 | 109 | ||
| 109 | db_store = DefaultDbStore( | 110 | db_store = DefaultDbStore( |
| 110 | - create_async_engine(async_database_url, pool_size=20, max_overflow=20) | 111 | + create_async_engine(async_database_url, **build_async_engine_kwargs()) |
| 111 | ) | 112 | ) |
| 112 | kv_store = cls._create_kv_store(async_database_url) | 113 | kv_store = cls._create_kv_store(async_database_url) |
| 113 | 114 | ||
| @@ -190,7 +191,7 @@ class MemoryEngineManager: | |||
| 190 | if kv_type in ("db", "sql", "sqlite", "mysql"): | 191 | if kv_type in ("db", "sql", "sqlite", "mysql"): |
| 191 | _log.info("Memory engine KV: DbBasedKVStore (same DSN as DB_TYPE)") | 192 | _log.info("Memory engine KV: DbBasedKVStore (same DSN as DB_TYPE)") |
| 192 | return DbBasedKVStore( | 193 | return DbBasedKVStore( |
| 193 | - create_async_engine(async_database_url, pool_pre_ping=True, echo=False) | 194 | + create_async_engine(async_database_url, **build_async_engine_kwargs()) |
| 194 | ) | 195 | ) |
| 195 | raise ValueError( | 196 | raise ValueError( |
| 196 | f"Unknown KV_STORE_TYPE={kv_type!r}; expected 'redis', 'inmemory', or 'db'." | 197 | f"Unknown KV_STORE_TYPE={kv_type!r}; expected 'redis', 'inmemory', or 'db'." |
| @@ -28,6 +28,11 @@ class Settings(BaseSettings): | |||
| 28 | RUNTIME_DB_PASSWORD: Optional[str] = Field(default=None, env="RUNTIME_DB_PASSWORD") | 28 | RUNTIME_DB_PASSWORD: Optional[str] = Field(default=None, env="RUNTIME_DB_PASSWORD") |
| 29 | RUNTIME_DB_NAME: Optional[str] = Field(default=None, env="RUNTIME_DB_NAME") | 29 | RUNTIME_DB_NAME: Optional[str] = Field(default=None, env="RUNTIME_DB_NAME") |
| 30 | 30 | ||
| 31 | + # SQLAlchemy 连接池默认值(亦支持 DB_POOL_SIZE / DB_MAX_OVERFLOW / DB_POOL_TIMEOUT 别名) | ||
| 32 | + RUNTIME_DB_POOL_SIZE: int = Field(default=2, env="RUNTIME_DB_POOL_SIZE") | ||
| 33 | + RUNTIME_DB_MAX_OVERFLOW: int = Field(default=20, env="RUNTIME_DB_MAX_OVERFLOW") | ||
| 34 | + RUNTIME_DB_POOL_TIMEOUT: int = Field(default=30, env="RUNTIME_DB_POOL_TIMEOUT") | ||
| 35 | + | ||
| 31 | # -------------------------- | 36 | # -------------------------- |
| 32 | # 【服务配置】 | 37 | # 【服务配置】 |
| 33 | # -------------------------- | 38 | # -------------------------- |
| @@ -0,0 +1,66 @@ | |||
| 1 | +# coding: utf-8 | ||
| 2 | +# Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved | ||
| 3 | + | ||
| 4 | +"""SQLAlchemy async engine 连接池参数(可通过环境变量覆盖)。""" | ||
| 5 | + | ||
| 6 | +from __future__ import annotations | ||
| 7 | + | ||
| 8 | +import os | ||
| 9 | +from typing import Any | ||
| 10 | + | ||
| 11 | +# 定期回收池内连接,应小于 MySQL wait_timeout / 中间层 idle 超时。 | ||
| 12 | +DEFAULT_POOL_RECYCLE_SECONDS = 1800 | ||
| 13 | + | ||
| 14 | +# 可通过 RUNTIME_DB_POOL_* / DB_POOL_* 覆盖 | ||
| 15 | +DEFAULT_POOL_SIZE = 2 | ||
| 16 | +DEFAULT_MAX_OVERFLOW = 20 | ||
| 17 | +DEFAULT_POOL_TIMEOUT = 30 | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +def _int_env(*names: str, default: int) -> int: | ||
| 21 | + for name in names: | ||
| 22 | + raw = os.getenv(name, "").strip() | ||
| 23 | + if not raw: | ||
| 24 | + continue | ||
| 25 | + try: | ||
| 26 | + return max(1, int(raw)) | ||
| 27 | + except ValueError: | ||
| 28 | + continue | ||
| 29 | + return default | ||
| 30 | + | ||
| 31 | + | ||
| 32 | +def get_pool_size() -> int: | ||
| 33 | + return _int_env("RUNTIME_DB_POOL_SIZE", "DB_POOL_SIZE", default=DEFAULT_POOL_SIZE) | ||
| 34 | + | ||
| 35 | + | ||
| 36 | +def get_max_overflow() -> int: | ||
| 37 | + return _int_env( | ||
| 38 | + "RUNTIME_DB_MAX_OVERFLOW", | ||
| 39 | + "DB_MAX_OVERFLOW", | ||
| 40 | + default=DEFAULT_MAX_OVERFLOW, | ||
| 41 | + ) | ||
| 42 | + | ||
| 43 | + | ||
| 44 | +def get_pool_timeout() -> int: | ||
| 45 | + return _int_env( | ||
| 46 | + "RUNTIME_DB_POOL_TIMEOUT", | ||
| 47 | + "DB_POOL_TIMEOUT", | ||
| 48 | + default=DEFAULT_POOL_TIMEOUT, | ||
| 49 | + ) | ||
| 50 | + | ||
| 51 | + | ||
| 52 | +def build_async_engine_kwargs( | ||
| 53 | + *, | ||
| 54 | + connect_args: dict[str, Any] | None = None, | ||
| 55 | + echo: bool = False, | ||
| 56 | +) -> dict[str, Any]: | ||
| 57 | + """构造 ``create_async_engine`` 的通用连接池参数。""" | ||
| 58 | + return { | ||
| 59 | + "echo": echo, | ||
| 60 | + "connect_args": connect_args or {}, | ||
| 61 | + "pool_pre_ping": True, | ||
| 62 | + "pool_recycle": DEFAULT_POOL_RECYCLE_SECONDS, | ||
| 63 | + "pool_size": get_pool_size(), | ||
| 64 | + "max_overflow": get_max_overflow(), | ||
| 65 | + "pool_timeout": get_pool_timeout(), | ||
| 66 | + } | ||
| @@ -11,6 +11,7 @@ from sqlalchemy.orm import DeclarativeBase | |||
| 11 | from sqlalchemy import select, update, delete, func | 11 | from sqlalchemy import select, update, delete, func |
| 12 | 12 | ||
| 13 | from ..log import get_logger | 13 | from ..log import get_logger |
| 14 | +from .engine_options import build_async_engine_kwargs | ||
| 14 | from .handler import DBHandler | 15 | from .handler import DBHandler |
| 15 | from .table_def import TableDefinition, ColumnDefinition, IndexDefinition | 16 | from .table_def import TableDefinition, ColumnDefinition, IndexDefinition |
| 16 | 17 | ||
| @@ -45,15 +46,17 @@ class SQLAlchemyHandler(DBHandler): | |||
| 45 | logger.info("Connecting to database") | 46 | logger.info("Connecting to database") |
| 46 | # 关闭 aiosqlite 的 DEBUG 日志 | 47 | # 关闭 aiosqlite 的 DEBUG 日志 |
| 47 | logging.getLogger("aiosqlite").setLevel(logging.WARNING) | 48 | logging.getLogger("aiosqlite").setLevel(logging.WARNING) |
| 48 | - self.engine = create_async_engine( | 49 | + engine_kwargs = build_async_engine_kwargs(connect_args=self.connect_args) |
| 49 | - self.database_url, | 50 | + self.engine = create_async_engine(self.database_url, **engine_kwargs) |
| 50 | - echo=False, | ||
| 51 | - connect_args=self.connect_args | ||
| 52 | - ) | ||
| 53 | self.session_factory = async_sessionmaker( | 51 | self.session_factory = async_sessionmaker( |
| 54 | self.engine, class_=AsyncSession, expire_on_commit=False | 52 | self.engine, class_=AsyncSession, expire_on_commit=False |
| 55 | ) | 53 | ) |
| 56 | - logger.info("Database connected") | 54 | + logger.info( |
| 55 | + "Database connected (pool_size=%s max_overflow=%s pool_timeout=%s)", | ||
| 56 | + engine_kwargs["pool_size"], | ||
| 57 | + engine_kwargs["max_overflow"], | ||
| 58 | + engine_kwargs["pool_timeout"], | ||
| 59 | + ) | ||
| 57 | 60 | ||
| 58 | async def disconnect(self) -> None: | 61 | async def disconnect(self) -> None: |
| 59 | logger.info("Disconnecting from database") | 62 | logger.info("Disconnecting from database") |
| @@ -5,6 +5,7 @@ | |||
| 5 | from __future__ import annotations | 5 | from __future__ import annotations |
| 6 | 6 | ||
| 7 | import asyncio | 7 | import asyncio |
| 8 | +import os | ||
| 8 | import re | 9 | import re |
| 9 | import secrets | 10 | import secrets |
| 10 | import string | 11 | import string |
| @@ -88,6 +89,30 @@ class HostPathMount: | |||
| 88 | raise ValueError("HostPathMount.mount_path is required") | 89 | raise ValueError("HostPathMount.mount_path is required") |
| 89 | 90 | ||
| 90 | 91 | ||
| 92 | + | ||
| 93 | +class ConfigMapMount: | ||
| 94 | + """容器内的 ConfigMap 挂载声明。 | ||
| 95 | + | ||
| 96 | + - ``config_map_name``: ConfigMap 名称,写入 ``V1ConfigMapVolumeSource.name`` | ||
| 97 | + - ``mount_path``: 容器内目标路径 | ||
| 98 | + - ``sub_path``: 可选,对应 ``V1VolumeMount.sub_path``,挂载 ConfigMap 中的单个 key 到指定文件路径 | ||
| 99 | + - ``items``: 可选,``[(key, path), ...]`` 列表,写入 ``V1ConfigMapVolumeSource.items``,选择性挂载部分 key | ||
| 100 | + - ``read_only``: 默认 True,ConfigMap 挂载为只读 | ||
| 101 | + """ | ||
| 102 | + | ||
| 103 | + config_map_name: str | ||
| 104 | + mount_path: str | ||
| 105 | + sub_path: Optional[str] = None | ||
| 106 | + items: Optional[List[Tuple[str, str]]] = None | ||
| 107 | + read_only: bool = True | ||
| 108 | + | ||
| 109 | + def __post_init__(self) -> None: | ||
| 110 | + if not self.config_map_name: | ||
| 111 | + raise ValueError("ConfigMapMount.config_map_name is required") | ||
| 112 | + if not self.mount_path: | ||
| 113 | + raise ValueError("ConfigMapMount.mount_path is required") | ||
| 114 | + | ||
| 115 | + | ||
| 91 | 116 | ||
| 92 | class ContainerSpec: | 117 | class ContainerSpec: |
| 93 | name: str | 118 | name: str |
| @@ -121,6 +146,9 @@ class ContainerSpec: | |||
| 121 | # ---- hostPath 挂载(对应 ``docker run -v HOST:CTR``) ---- | 146 | # ---- hostPath 挂载(对应 ``docker run -v HOST:CTR``) ---- |
| 122 | host_path_mounts: List[HostPathMount] = field(default_factory=list) | 147 | host_path_mounts: List[HostPathMount] = field(default_factory=list) |
| 123 | 148 | ||
| 149 | + # ---- ConfigMap 挂载(对应 ``volumes.configMap`` + ``volumeMounts``) ---- | ||
| 150 | + configmap_mounts: List[ConfigMapMount] = field(default_factory=list) | ||
| 151 | + | ||
| 124 | cpu_request: Optional[str] = None | 152 | cpu_request: Optional[str] = None |
| 125 | memory_request: Optional[str] = None | 153 | memory_request: Optional[str] = None |
| 126 | cpu_limit: Optional[str] = None | 154 | cpu_limit: Optional[str] = None |
| @@ -144,6 +172,7 @@ class ContainerSpec: | |||
| 144 | object.__setattr__(self, "capabilities_add", list(self.capabilities_add or [])) | 172 | object.__setattr__(self, "capabilities_add", list(self.capabilities_add or [])) |
| 145 | object.__setattr__(self, "capabilities_drop", list(self.capabilities_drop or [])) | 173 | object.__setattr__(self, "capabilities_drop", list(self.capabilities_drop or [])) |
| 146 | object.__setattr__(self, "host_path_mounts", list(self.host_path_mounts or [])) | 174 | object.__setattr__(self, "host_path_mounts", list(self.host_path_mounts or [])) |
| 175 | + object.__setattr__(self, "configmap_mounts", list(self.configmap_mounts or [])) | ||
| 147 | if self.host_port is not None: | 176 | if self.host_port is not None: |
| 148 | hp = int(self.host_port) | 177 | hp = int(self.host_port) |
| 149 | if hp <= 0 or hp > 65535: | 178 | if hp <= 0 or hp > 65535: |
| @@ -165,6 +194,7 @@ class K8sServiceHandler: | |||
| 165 | namespace: str = "default", | 194 | namespace: str = "default", |
| 166 | pod_name: str = None, | 195 | pod_name: str = None, |
| 167 | extra_labels: Optional[Dict[str, str]] = None, | 196 | extra_labels: Optional[Dict[str, str]] = None, |
| 197 | + owner_reference: Optional[client.V1OwnerReference] = None, # Pod ownerReference,用于 owner 删除时级联清理 | ||
| 168 | restart_policy: str = "Always", | 198 | restart_policy: str = "Always", |
| 169 | kubeconfig: Optional[str] = None, | 199 | kubeconfig: Optional[str] = None, |
| 170 | ready_timeout: float = 300.0, | 200 | ready_timeout: float = 300.0, |
| @@ -198,6 +228,7 @@ class K8sServiceHandler: | |||
| 198 | self._name_prefix = pod_name if pod_name else self._sanitize_prefix(name_prefix) | 228 | self._name_prefix = pod_name if pod_name else self._sanitize_prefix(name_prefix) |
| 199 | self._namespace = namespace | 229 | self._namespace = namespace |
| 200 | self._extra_labels: Dict[str, str] = dict(extra_labels or {}) | 230 | self._extra_labels: Dict[str, str] = dict(extra_labels or {}) |
| 231 | + self._owner_reference = owner_reference | ||
| 201 | self._restart_policy = restart_policy | 232 | self._restart_policy = restart_policy |
| 202 | self._kubeconfig = kubeconfig | 233 | self._kubeconfig = kubeconfig |
| 203 | self._ready_timeout = float(ready_timeout) | 234 | self._ready_timeout = float(ready_timeout) |
| @@ -255,6 +286,14 @@ class K8sServiceHandler: | |||
| 255 | suffix = f"-{idx}-{mount_idx}" | 286 | suffix = f"-{idx}-{mount_idx}" |
| 256 | return f"hp-{base[: 63 - len('hp-') - len(suffix)]}{suffix}" | 287 | return f"hp-{base[: 63 - len('hp-') - len(suffix)]}{suffix}" |
| 257 | 288 | ||
| 289 | + | ||
| 290 | + def _build_configmap_volume_name(cls, name: str, idx: int, mount_idx: int) -> str: | ||
| 291 | + # 预留 "cm-" 前缀和索引后缀,避免同一 Pod 内多容器、多挂载重名。 | ||
| 292 | + sanitized = cls._NAME_INVALID_CHARS.sub("-", (name or "").lower()).strip("-") | ||
| 293 | + base = sanitized or f"c{idx}" | ||
| 294 | + suffix = f"-{idx}-{mount_idx}" | ||
| 295 | + return f"cm-{base[: 63 - len('cm-') - len(suffix)]}{suffix}" | ||
| 296 | + | ||
| 258 | 297 | ||
| 259 | def _build_security_context(cls, spec: ContainerSpec) -> Optional[client.V1SecurityContext]: | 298 | def _build_security_context(cls, spec: ContainerSpec) -> Optional[client.V1SecurityContext]: |
| 260 | capabilities = None | 299 | capabilities = None |
| @@ -419,6 +458,31 @@ class K8sServiceHandler: | |||
| 419 | ) | 458 | ) |
| 420 | ) | 459 | ) |
| 421 | 460 | ||
| 461 | + for cm_idx, cm_mount in enumerate(spec.configmap_mounts): | ||
| 462 | + volume_name = self._build_configmap_volume_name(cm_mount.config_map_name, idx, cm_idx) | ||
| 463 | + cm_items = None | ||
| 464 | + if cm_mount.items: | ||
| 465 | + cm_items = [ | ||
| 466 | + client.V1KeyToPath(key=k, path=p) for k, p in cm_mount.items | ||
| 467 | + ] | ||
| 468 | + volumes.append( | ||
| 469 | + client.V1Volume( | ||
| 470 | + name=volume_name, | ||
| 471 | + config_map=client.V1ConfigMapVolumeSource( | ||
| 472 | + name=cm_mount.config_map_name, | ||
| 473 | + items=cm_items, | ||
| 474 | + ), | ||
| 475 | + ) | ||
| 476 | + ) | ||
| 477 | + container_volume_mounts.append( | ||
| 478 | + client.V1VolumeMount( | ||
| 479 | + name=volume_name, | ||
| 480 | + mount_path=cm_mount.mount_path, | ||
| 481 | + sub_path=cm_mount.sub_path, | ||
| 482 | + read_only=cm_mount.read_only, | ||
| 483 | + ) | ||
| 484 | + ) | ||
| 485 | + | ||
| 422 | if spec.apparmor_unconfined: | 486 | if spec.apparmor_unconfined: |
| 423 | annotations[ | 487 | annotations[ |
| 424 | f"container.apparmor.security.beta.kubernetes.io/{spec.name}" | 488 | f"container.apparmor.security.beta.kubernetes.io/{spec.name}" |
| @@ -457,6 +521,7 @@ class K8sServiceHandler: | |||
| 457 | namespace=self._namespace, | 521 | namespace=self._namespace, |
| 458 | labels=labels, | 522 | labels=labels, |
| 459 | annotations=annotations or None, | 523 | annotations=annotations or None, |
| 524 | + owner_references=[self._owner_reference] if self._owner_reference else None, | ||
| 460 | ), | 525 | ), |
| 461 | spec=client.V1PodSpec( | 526 | spec=client.V1PodSpec( |
| 462 | containers=pod_containers, | 527 | containers=pod_containers, |
| @@ -464,6 +529,7 @@ class K8sServiceHandler: | |||
| 464 | volumes=volumes or None, | 529 | volumes=volumes or None, |
| 465 | node_name=self._node_name if self._mode == "dev" else None, | 530 | node_name=self._node_name if self._mode == "dev" else None, |
| 466 | security_context=pod_security_context, | 531 | security_context=pod_security_context, |
| 532 | + enable_service_links=(os.getenv("ENABLE_SERVICE_LINKS", "false").lower() == "true"), | ||
| 467 | ), | 533 | ), |
| 468 | ) | 534 | ) |
| 469 | 535 | ||
| @@ -24,7 +24,7 @@ dependencies = [ | |||
| 24 | "redis==7.1.0", | 24 | "redis==7.1.0", |
| 25 | # lint | 25 | # lint |
| 26 | "ruff==0.9.10", | 26 | "ruff==0.9.10", |
| 27 | - "openjiuwen_runtime_foundation @ git+https://gitcode.com/openJiuwen/agent-runtime.git@develop#subdirectory=foundation", | 27 | + "openjiuwen-runtime-foundation==0.1.0", |
| 28 | "fastapi>=0.110.0", | 28 | "fastapi>=0.110.0", |
| 29 | "httpx>=0.27.0", | 29 | "httpx>=0.27.0", |
| 30 | "uvicorn[standard]>=0.29.0", | 30 | "uvicorn[standard]>=0.29.0", |
| @@ -16,6 +16,11 @@ RUNTIME_DB_PASSWORD=root | |||
| 16 | # 要连接的数据库名称 | 16 | # 要连接的数据库名称 |
| 17 | RUNTIME_DB_NAME=jiuwen_runtime | 17 | RUNTIME_DB_NAME=jiuwen_runtime |
| 18 | 18 | ||
| 19 | +# SQLAlchemy 连接池(默认 pool_size=2、max_overflow=20;高并发时可按需覆盖,亦可用 DB_POOL_SIZE 等别名) | ||
| 20 | +# RUNTIME_DB_POOL_SIZE=2 | ||
| 21 | +# RUNTIME_DB_MAX_OVERFLOW=20 | ||
| 22 | +# RUNTIME_DB_POOL_TIMEOUT=30 | ||
| 23 | + | ||
| 19 | # ----------------------------------------------------------------------------- | 24 | # ----------------------------------------------------------------------------- |
| 20 | # GaussDB/openGauss POC 验证建议值 | 25 | # GaussDB/openGauss POC 验证建议值 |
| 21 | # ----------------------------------------------------------------------------- | 26 | # ----------------------------------------------------------------------------- |