mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 00:16:57 +08:00
refactor: dataize scheduler job contracts
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user