refactor: dataize scheduler job contracts

This commit is contained in:
jxxghp
2026-08-21 21:59:11 +08:00
parent dce7372b81
commit c755c074b9
4 changed files with 288 additions and 154 deletions
+101 -1
View File
@@ -9,7 +9,10 @@ Scheduler 实现由 startup 组合根在导入期注册,避免 application 层
agent.tools / api.endpoints -> application.scheduling <- startup(注册 Scheduler 类)
"""
from typing import Any, List, Optional
import asyncio
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any, Awaitable, Callable, List, Optional
# Agent 自主定时任务在运行时调度器中的任务 ID 前缀。
AGENT_TASK_JOB_PREFIX = "agent-task"
@@ -18,6 +21,103 @@ AGENT_TASK_JOB_PREFIX = "agent-task"
_scheduler_class: Any = None
class JobOverlapPolicy(StrEnum):
"""描述同一 job 已运行时的新触发处理策略。"""
SKIP = "skip"
class JobRecoveryPolicy(StrEnum):
"""描述进程重启后是否以及如何重建执行意图。"""
NEXT_SCHEDULE = "next_schedule"
DURABLE_QUEUE = "durable_queue"
MANUAL_ONLY = "manual_only"
@dataclass(frozen=True, slots=True)
class JobSpec:
"""数据化声明一个业务 job 的执行和恢复合同。"""
job_id: str
name: str
func: Callable[..., Any]
owner: str
overlap: JobOverlapPolicy = JobOverlapPolicy.SKIP
timeout_seconds: int | None = None
manual: bool = False
recovery: JobRecoveryPolicy = JobRecoveryPolicy.NEXT_SCHEDULE
kwargs: dict[str, Any] = field(default_factory=dict)
def to_runtime_state(self) -> dict[str, Any]:
"""生成兼容 Scheduler Facade 的可变执行状态。"""
return {
"name": self.name,
"func": self.func,
"owner": self.owner,
"overlap": self.overlap.value,
"timeout_seconds": self.timeout_seconds,
"manual": self.manual,
"recovery": self.recovery.value,
"kwargs": dict(self.kwargs),
"running": False,
}
class JobCatalog:
"""保存唯一 job ID 到声明的映射。"""
def __init__(self, specs: list[JobSpec]) -> None:
"""拒绝重复 ID,并冻结供 Scheduler 初始化的声明集合。"""
self._specs = {spec.job_id: spec for spec in specs}
if len(self._specs) != len(specs):
raise ValueError("JobSpec job_id 不得重复")
def runtime_states(self) -> dict[str, dict[str, Any]]:
"""返回兼容旧 Scheduler `_jobs` 的全新状态字典。"""
return {
job_id: spec.to_runtime_state()
for job_id, spec in self._specs.items()
}
class JobExecutionState:
"""集中维护兼容 job 字典的 overlap 与终态字段。"""
@staticmethod
def begin(job: dict[str, Any], started_at: str) -> bool:
"""按 overlap policy 尝试进入 running,已运行时返回 False。"""
if job.get("running") and job.get(
"overlap", JobOverlapPolicy.SKIP.value
) == JobOverlapPolicy.SKIP.value:
return False
job.update(
running=True,
last_started_at=started_at,
last_finished_at=None,
last_error=None,
)
return True
@staticmethod
def finish(job: dict[str, Any], finished_at: str, error: str | None) -> None:
"""写入成功或失败终态并释放 running 标记。"""
job.update(
running=False,
last_finished_at=finished_at,
last_error=error,
)
@staticmethod
async def await_result(
awaitable: Awaitable[Any], timeout_seconds: int | None
) -> Any:
"""等待协程任务;声明了超时时由 asyncio 负责取消底层任务。"""
if timeout_seconds is None:
return await awaitable
return await asyncio.wait_for(awaitable, timeout=timeout_seconds)
def register_scheduler_class(scheduler_class: Any) -> None:
"""注册 Scheduler 类(组合根在导入期调用)。"""
global _scheduler_class