refactor: unify protected task completion

This commit is contained in:
jxxghp
2026-08-24 03:20:45 +08:00
parent 10582277e2
commit 22848eeda2
7 changed files with 64 additions and 34 deletions
+2 -8
View File
@@ -45,6 +45,7 @@ from app.adapters.system.plugin.manifest import (
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.observability import observe_compat_facade from app.runtime.observability import observe_compat_facade
from app.runtime.execution import ( from app.runtime.execution import (
await_task_to_terminal,
run_in_threadpool_to_completion as _await_thread_operation, run_in_threadpool_to_completion as _await_thread_operation,
) )
from app.runtime.tasks import get_task_registry from app.runtime.tasks import get_task_registry
@@ -1499,15 +1500,8 @@ class PluginHelper(metaclass=WeakSingleton):
await asyncio.to_thread(created_file.unlink, missing_ok=True) await asyncio.to_thread(created_file.unlink, missing_ok=True)
cleanup_task = asyncio.create_task(cleanup_created_file()) cleanup_task = asyncio.create_task(cleanup_created_file())
while not cleanup_task.done():
try:
await asyncio.shield(cleanup_task)
except asyncio.CancelledError:
continue
except Exception:
break
try: try:
await cleanup_task await await_task_to_terminal(cleanup_task)
except Exception as err: except Exception as err:
logger.warning(f"[UV] 取消后清理运行环境约束文件失败:{err}") logger.warning(f"[UV] 取消后清理运行环境约束文件失败:{err}")
raise raise
+4 -19
View File
@@ -9,6 +9,7 @@ from typing import Any, ContextManager, Optional
from app.schemas.exception import PersistenceUnavailableError from app.schemas.exception import PersistenceUnavailableError
from app.application.plugin.lifecycle import plugin_lifecycle from app.application.plugin.lifecycle import plugin_lifecycle
from app.runtime.execution import await_task_to_terminal
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.exception import PluginMutationRejectedError from app.schemas.exception import PluginMutationRejectedError
@@ -29,17 +30,6 @@ PluginRegistrationRefresher = Callable[[str], Awaitable[object]]
PluginMutationAdmission = Callable[[str], ContextManager[None]] PluginMutationAdmission = Callable[[str], ContextManager[None]]
PluginPackageWriteGuard = Callable[[str], ContextManager[None]] PluginPackageWriteGuard = Callable[[str], ContextManager[None]]
async def _await_task_to_terminal(task: asyncio.Task[Any]) -> Any:
"""忽略调用方的重复取消,直到受保护子任务进入真实终态。"""
while not task.done():
try:
return await asyncio.shield(task)
except asyncio.CancelledError:
continue
return task.result()
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class PluginInstallRollback: class PluginInstallRollback:
"""描述失败安装中各类可补偿副作用的恢复结果。""" """描述失败安装中各类可补偿副作用的恢复结果。"""
@@ -193,7 +183,7 @@ class PluginInstallCommand:
state.checkpoint = checkpoint state.checkpoint = checkpoint
except asyncio.CancelledError: except asyncio.CancelledError:
try: try:
state.checkpoint = await _await_task_to_terminal(checkpoint_task) state.checkpoint = await await_task_to_terminal(checkpoint_task)
except BaseException: except BaseException:
pass pass
raise raise
@@ -369,7 +359,7 @@ class PluginInstallCommand:
) )
) )
try: try:
result = await _await_task_to_terminal(rollback_task) result = await await_task_to_terminal(rollback_task)
except BaseException as err: except BaseException as err:
logger.error(f"插件 {plugin_id} 取消后的补偿失败:{err}") logger.error(f"插件 {plugin_id} 取消后的补偿失败:{err}")
return return
@@ -410,12 +400,7 @@ class PluginInstallCommand:
cleanup_task = asyncio.create_task( cleanup_task = asyncio.create_task(
self._restore_refreshed_runtime(plugin_id) self._restore_refreshed_runtime(plugin_id)
) )
while not cleanup_task.done(): rollback = await await_task_to_terminal(cleanup_task)
try:
await asyncio.shield(cleanup_task)
except asyncio.CancelledError:
continue
rollback = await cleanup_task
state.refresh_compensated = True state.refresh_compensated = True
if rollback.errors: if rollback.errors:
logger.error( logger.error(
+18 -1
View File
@@ -3,12 +3,29 @@ import inspect
import time import time
from contextvars import copy_context from contextvars import copy_context
from functools import partial, wraps from functools import partial, wraps
from typing import Any, Callable from typing import Any, Callable, TypeVar
from app.schemas.exception import ImmediateException from app.schemas.exception import ImmediateException
from anyio.to_thread import run_sync from anyio.to_thread import run_sync
TaskResult = TypeVar("TaskResult")
async def await_task_to_terminal(
task: asyncio.Future[TaskResult],
) -> TaskResult:
"""忽略当前调用方的重复取消,直到受保护任务进入真实终态。"""
while not task.done():
try:
await asyncio.shield(task)
except asyncio.CancelledError:
continue
except BaseException:
break
return task.result()
async def run_in_threadpool( async def run_in_threadpool(
func: Callable[..., Any], func: Callable[..., Any],
*args: Any, *args: Any,
@@ -102,6 +102,8 @@ Event Contract Registry 是 53 个事件的逐项机器清单。下表按相同
- 市场适配器和插件包适配器统一复用 `runtime.execution.run_in_threadpool_to_completion`;两个模块内的 - 市场适配器和插件包适配器统一复用 `runtime.execution.run_in_threadpool_to_completion`;两个模块内的
`_await_thread_operation` 私有接缝保留为同一函数别名,不改变 `PluginHelper``PluginPackageManager` `_await_thread_operation` 私有接缝保留为同一函数别名,不改变 `PluginHelper``PluginPackageManager`
的同步/异步调用合同。 的同步/异步调用合同。
- 插件安装快照、取消补偿和临时约束文件清理统一复用 `runtime.execution.await_task_to_terminal`,连续取消
不再由 Application 与 Adapter 各自维护近似循环;数据库 worker 的可中断队列等待仍保留独立职责。
### Transfer pending / 文件整理 ### Transfer pending / 文件整理
@@ -6,7 +6,7 @@
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 > 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文 > 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文
> 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md` > 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md`
> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义。 > 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待
## 当前复核结论(2026-08-24 ## 当前复核结论(2026-08-24
@@ -15,7 +15,7 @@
### 长期整改阶段 0:治理门禁恢复(2026-08-23 ### 长期整改阶段 0:治理门禁恢复(2026-08-23
- 宿主依赖基线已审查 TaskRegistry、有界后台 owner 与插件变更准入接入后的语义差异:当前为 `806` 个模块、`6531` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。 - 宿主依赖基线已审查 TaskRegistry、有界后台 owner 与插件变更准入接入后的语义差异:当前为 `806` 个模块、`6532` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。
- 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistrynormal/safe 组件数分别为 `23`/`11`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。 - 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistrynormal/safe 组件数分别为 `23`/`11`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。
- 官方插件快照覆盖 `plugins.v3``plugins.v2` 以及 V3 实际会从 `package.json` 回退加载的 31 个默认实现;`app/plugins/**` 仍只是宿主运行副本,不进入扫描。 - 官方插件快照覆盖 `plugins.v3``plugins.v2` 以及 V3 实际会从 `package.json` 回退加载的 31 个默认实现;`app/plugins/**` 仍只是宿主运行副本,不进入扫描。
- SDK 快照以各模块显式 `__all__` 为公开合同,能够记录赋值别名;`typing``__future__` 等实现期导入不再被误冻结,既有数据库备份门面已补精确导出清单。 - SDK 快照以各模块显式 `__all__` 为公开合同,能够记录赋值别名;`typing``__future__` 等实现期导入不再被误冻结,既有数据库备份门面已补精确导出清单。
@@ -112,7 +112,7 @@
- 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。 - 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。
- `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。 - `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。
- 依赖图当前为 `806` 个 Python 模块、`6531` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。 - 依赖图当前为 `806` 个 Python 模块、`6532` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
- 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。 - 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。
综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。 综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。
@@ -138,6 +138,8 @@
`stop_modules()` 会刷新未到期摘要并等待已启动回调;它属于 E1 观测,不扩大 TaskRegistry 或 Outbox 范围。 `stop_modules()` 会刷新未到期摘要并等待已启动回调;它属于 E1 观测,不扩大 TaskRegistry 或 Outbox 范围。
插件市场与插件包适配器原有两套线程取消 wrapper 也已统一到 `runtime.execution`:连续取消必须等同步 插件市场与插件包适配器原有两套线程取消 wrapper 也已统一到 `runtime.execution`:连续取消必须等同步
文件 worker 到达终态后再传播,避免提前释放 mutation owner;旧模块内私有名称保留为 canonical 别名。 文件 worker 到达终态后再传播,避免提前释放 mutation owner;旧模块内私有名称保留为 canonical 别名。
插件安装快照、取消补偿与市场临时文件清理原有两套协程终态循环也已合并为
`await_task_to_terminal`;数据库 worker 的 interruptible 等待属于队列 owner,未被机械合并。
2. **Model/Base 的数据库装饰器和隐式会话 ABI 已全部清零。** 查询、写事务和 `legacy_*` 装饰器均为 `0`;所有 Model `db` 参数要求显式 Session,Base CRUD 仅在调用方事务内查询或 stage。可无会话构造的入口统一留在 Oper,经组合根事务执行器运行;插件 SDK 不再导出宿主 Model。后续重点转为减少 ORM 对象跨层流转,并保持 Model 隐式事务零回退。 2. **Model/Base 的数据库装饰器和隐式会话 ABI 已全部清零。** 查询、写事务和 `legacy_*` 装饰器均为 `0`;所有 Model `db` 参数要求显式 Session,Base CRUD 仅在调用方事务内查询或 stage。可无会话构造的入口统一留在 Oper,经组合根事务执行器运行;插件 SDK 不再导出宿主 Model。后续重点转为减少 ORM 对象跨层流转,并保持 Model 隐式事务零回退。
Oper 内部的执行入口也已统一:最后一处 `AgentTaskOper` 直接 transaction runner 调用已迁入 Oper 内部的执行入口也已统一:最后一处 `AgentTaskOper` 直接 transaction runner 调用已迁入
+3 -2
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [], "runtime_to_db": [],
"workflow_to_db": [] "workflow_to_db": []
}, },
"edge_count": 6531, "edge_count": 6532,
"edge_sha256": "86b2ad0b585deba78c5e0e5e7e4b55cd66e05a2d5e988ed28ac7f6a4ae15ff2f", "edge_sha256": "3fdba2c6862f9ea28ab4f059c04dd6bb819b86fa9d9fb74d09a0cfcf69f6b711",
"edges": [ "edges": [
"app -> app.runtime", "app -> app.runtime",
"app -> app.runtime.compat", "app -> app.runtime.compat",
@@ -2704,6 +2704,7 @@
"app.application.plugin.install -> app.application.plugin", "app.application.plugin.install -> app.application.plugin",
"app.application.plugin.install -> app.application.plugin.lifecycle", "app.application.plugin.install -> app.application.plugin.lifecycle",
"app.application.plugin.install -> app.runtime", "app.application.plugin.install -> app.runtime",
"app.application.plugin.install -> app.runtime.execution",
"app.application.plugin.install -> app.runtime.log", "app.application.plugin.install -> app.runtime.log",
"app.application.plugin.install -> app.schemas", "app.application.plugin.install -> app.schemas",
"app.application.plugin.install -> app.schemas.exception", "app.application.plugin.install -> app.schemas.exception",
+30 -1
View File
@@ -8,7 +8,10 @@ from anyio.to_thread import current_default_thread_limiter
from app.adapters.external import market as market_adapter from app.adapters.external import market as market_adapter
from app.adapters.system.plugin import package as plugin_package_adapter from app.adapters.system.plugin import package as plugin_package_adapter
from app.runtime.execution import run_in_threadpool_to_completion from app.runtime.execution import (
await_task_to_terminal,
run_in_threadpool_to_completion,
)
def test_plugin_file_adapters_share_runtime_completion_contract() -> None: def test_plugin_file_adapters_share_runtime_completion_contract() -> None:
@@ -20,6 +23,32 @@ def test_plugin_file_adapters_share_runtime_completion_contract() -> None:
) )
@pytest.mark.asyncio
async def test_await_task_to_terminal_ignores_repeated_cancellation() -> None:
"""调用方连续取消时,受保护任务仍须结束并返回真实结果。"""
started = asyncio.Event()
release = asyncio.Event()
async def protected_operation() -> str:
"""阻塞到测试释放,用于观察受保护任务的真实终态。"""
started.set()
await release.wait()
return "completed"
protected_task = asyncio.create_task(protected_operation())
waiter = asyncio.create_task(await_task_to_terminal(protected_task))
await started.wait()
waiter.cancel()
await asyncio.sleep(0)
waiter.cancel()
await asyncio.sleep(0)
assert waiter.done() is False
release.set()
assert await waiter == "completed"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_threadpool_capacity_is_held_until_cancelled_call_finishes() -> None: async def test_threadpool_capacity_is_held_until_cancelled_call_finishes() -> None:
"""调用方取消后,执行令牌必须由真实同步调用持有到终态。""" """调用方取消后,执行令牌必须由真实同步调用持有到终态。"""