已合并
delete useless k8s service env var #391
delete useless k8s service env var #391
已合并
王明琦创建于 7月23日
7 个文件变更+155-9
@@ -17,6 +17,7 @@ import os
17from pathlib import Path17from pathlib import Path
18from urllib.parse import quote18from urllib.parse import quote
19 19 
20+from openjiuwen_runtime.foundation.db.engine_options import build_async_engine_kwargs
20from openjiuwen_runtime.foundation.log import get_logger21from 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
11from sqlalchemy import select, update, delete, func11from sqlalchemy import select, update, delete, func
12 12 
13from ..log import get_logger13from ..log import get_logger
14+from .engine_options import build_async_engine_kwargs
14from .handler import DBHandler15from .handler import DBHandler
15from .table_def import TableDefinition, ColumnDefinition, IndexDefinition16from .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=False52 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 @@
5from __future__ import annotations5from __future__ import annotations
6 6 
7import asyncio7import asyncio
8+import os
8import re9import re
9import secrets10import secrets
10import string11import 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+@dataclass(frozen=True)
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@dataclass(frozen=True)116@dataclass(frozen=True)
92class ContainerSpec:117class ContainerSpec:
93 name: str118 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] = None152 cpu_request: Optional[str] = None
125 memory_request: Optional[str] = None153 memory_request: Optional[str] = None
126 cpu_limit: Optional[str] = None154 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 = namespace229 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_policy232 self._restart_policy = restart_policy
202 self._kubeconfig = kubeconfig233 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+ @classmethod
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 @classmethod297 @classmethod
259 def _build_security_context(cls, spec: ContainerSpec) -> Optional[client.V1SecurityContext]:298 def _build_security_context(cls, spec: ContainerSpec) -> Optional[client.V1SecurityContext]:
260 capabilities = None299 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 # lint25 # 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# 要连接的数据库名称
17RUNTIME_DB_NAME=jiuwen_runtime17RUNTIME_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# -----------------------------------------------------------------------------