ForcePilot/backend/package/yuxi/channels/plugins/wecom/lifecycle.py

167 lines
6.3 KiB
Python
Raw Normal View History

"""企业微信渠道插件生命周期钩子处理。
实现 ``LifecycleHookHandler`` Protocol管理插件级资源httpx 连接池
响应宿主生命周期状态变迁
设计要点
- LifecycleHandler 为单实例 per-account可持有 mutable 状态用于
资源管理与适配器的 INV-5 mutable 状态约束不同
- httpx 连接池在 ``onInit`` 创建并注入 ``WecomClient````onUnload``
解除引用后关闭连接数上限对齐 manifest.json
``resource_quota.max_connections=50``
- ``onReconfigure`` 从配置 schema 派生不可热更新键列表
``hot_reloadable=False``变更时抛 ``ConfigRestartRequiredError``
- ``onFail`` 不抛异常不阻断降级流程logger 不可用时回退 ``stderr``
依赖方向 import ``yuxi.channels.contract.*`` + 标准库 + httpx
不污染框架层
"""
from __future__ import annotations
import sys
from typing import TYPE_CHECKING, Any
import httpx
from yuxi.channels.contract.dtos.config import ConfigField
from yuxi.channels.contract.errors import ConfigRestartRequiredError
from yuxi.channels.contract.ports.driven.cache_port import CachePort
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
from ._constants import (
HTTP_MAX_CONNECTIONS,
HTTP_MAX_KEEPALIVE_CONNECTIONS,
HTTP_TIMEOUT_SECONDS,
)
if TYPE_CHECKING:
from .wecom_client import WecomClient
class WecomLifecycleHandler:
"""企业微信渠道插件生命周期钩子处理。
单实例管理插件级资源生命周期``_http_client`` 为共享连接池通过
``attach_http_client`` 注入 ``WecomClient`` 供其复用避免每请求新建
TCP 连接``onReconfigure`` 通过入参 ``old_config`` 做差异化校验
不再自行维护基线缓存不可热更新键列表从配置 schema 派生
``hot_reloadable=False````config_schema`` 必填
"""
def __init__(
self,
config_port: ConfigPort,
logger_port: LoggerPort,
cache_port: CachePort,
config_schema: tuple[ConfigField, ...],
client: WecomClient | None = None,
) -> None:
self._config = config_port
self._logger = logger_port
self._cache = cache_port
self._client = client
self._http_client: httpx.AsyncClient | None = None
self._started: bool = False
self._restart_required_keys: tuple[str, ...] = tuple(
field.key for field in config_schema if not field.hot_reloadable
)
# ------------------------------------------------------------------
# LifecycleHookHandler Protocol 实现
# ------------------------------------------------------------------
async def onInit(self) -> None:
"""初始化资源:创建 httpx 连接池并注入 WecomClient。"""
self._http_client = httpx.AsyncClient(
timeout=HTTP_TIMEOUT_SECONDS,
limits=httpx.Limits(
max_connections=HTTP_MAX_CONNECTIONS,
max_keepalive_connections=HTTP_MAX_KEEPALIVE_CONNECTIONS,
),
)
if self._client is not None:
self._client.attach_http_client(self._http_client)
await self._logger.info("Wecom plugin initialized")
async def onStart(self) -> None:
"""标记插件就绪。"""
self._started = True
await self._logger.info("Wecom plugin started")
async def onStop(self) -> None:
"""停止插件:标记未就绪,等待在途请求完成。"""
await self._logger.info(
"Wecom plugin stopping, waiting for in-flight requests",
)
self._started = False
async def onPause(self) -> None:
"""暂停插件:停止接收新请求。"""
self._started = False
await self._logger.info("Wecom plugin paused")
async def onResume(self) -> None:
"""恢复插件:重新接收请求。"""
self._started = True
await self._logger.info("Wecom plugin resumed")
async def onUnload(self) -> None:
"""释放所有资源:解除 WecomClient 引用后关闭 httpx 连接池。"""
if self._client is not None:
self._client.detach_http_client()
if self._http_client is not None:
try:
await self._http_client.aclose()
except Exception as exc:
await self._logger.warn(
"Wecom httpx client close failed during unload",
error=str(exc),
)
self._http_client = None
self._started = False
await self._logger.info("Wecom plugin unloaded")
async def onReconfigure(
self,
*,
old_config: dict[str, Any] | None = None,
new_config: dict[str, Any],
) -> None:
"""配置热更新:校验不可热更新字段。
- 不可热更新键``hot_reloadable=False`` ``corp_id`` /
``corp_secret`` / ``agent_id`` / ``encoding_aes_key`` / ``token`` /
``webhook_url`` / ``external_contact_secret`` / ``kf_secret``变更
``ConfigRestartRequiredError``FR-37
- ``old_config`` ``None``首次调用或无基线时仅记录日志不做对比
- 校验失败由宿主回滚本方法无副作用
"""
if old_config is None:
await self._logger.info("Wecom plugin initial config baseline set")
return
for key in self._restart_required_keys:
new_value = new_config.get(key)
old_value = old_config.get(key)
if new_value != old_value:
raise ConfigRestartRequiredError(key=key) from None
await self._logger.info("Wecom plugin reconfigured")
async def onFail(self, error: str) -> None:
"""失败清理不抛异常不阻断降级流程FR-36
logger 不可用时回退 ``sys.stderr.write``确保失败信息不丢失
"""
self._started = False
try:
await self._logger.error("Wecom plugin failed", error=error)
except Exception as exc:
sys.stderr.write(f"Wecom plugin failed: {error}\nLogger error: {exc}\n")
__all__ = ["WecomLifecycleHandler"]