mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-28 19:47:41 +08:00
182 lines
5.8 KiB
Python
182 lines
5.8 KiB
Python
"""调度器工具服务门面。
|
|
|
|
Agent 工具与 API 端点对运行时调度器的操作统一经本模块调用,
|
|
Scheduler 实现由 startup 组合根在导入期注册,避免 application 层
|
|
静态依赖顶层 scheduler 模块(scheduler 反向依赖 chain,会成环)。
|
|
|
|
依赖方向:
|
|
|
|
agent.tools / api.endpoints -> application.scheduling <- startup(注册 Scheduler 类)
|
|
"""
|
|
|
|
import asyncio
|
|
from dataclasses import dataclass, field
|
|
from enum import StrEnum
|
|
from typing import Any, Awaitable, Callable, List, Optional, cast
|
|
|
|
# Agent 自主定时任务在运行时调度器中的任务 ID 前缀。
|
|
AGENT_TASK_JOB_PREFIX = "agent-task"
|
|
|
|
# Scheduler 类:由 startup/initializers/scheduler.py 在导入期注册。
|
|
_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
|
|
_scheduler_class = scheduler_class
|
|
|
|
|
|
def get_scheduler() -> Any:
|
|
"""返回调度器实例。"""
|
|
if _scheduler_class is None:
|
|
raise RuntimeError(
|
|
"调度器服务未初始化:请先通过 register_scheduler_class 注册 Scheduler 类"
|
|
)
|
|
return _scheduler_class()
|
|
|
|
|
|
class Scheduler:
|
|
"""应用层调度器兼容门面,不直接导入顶层 Scheduler 实现。"""
|
|
|
|
def __new__(cls) -> Any:
|
|
"""返回组合根注册的调度器实例。"""
|
|
return get_scheduler()
|
|
|
|
|
|
def list_scheduler_jobs() -> List[Any]:
|
|
"""列出运行时调度器的全部任务。"""
|
|
return cast(List[Any], get_scheduler().list())
|
|
|
|
|
|
def start_scheduler_job(job_id: str, **kwargs: Any) -> None:
|
|
"""立即运行指定的运行时定时任务。"""
|
|
get_scheduler().start(job_id, **kwargs)
|
|
|
|
|
|
def update_plugin_job(plugin_id: str) -> None:
|
|
"""更新插件的定时任务。"""
|
|
get_scheduler().update_plugin_job(plugin_id)
|
|
|
|
|
|
def remove_plugin_job(plugin_id: str) -> None:
|
|
"""移除插件的定时任务。"""
|
|
get_scheduler().remove_plugin_job(plugin_id)
|
|
|
|
|
|
def start_agent_task(task_id: int) -> bool:
|
|
"""立即执行 Agent 自主定时任务。"""
|
|
return cast(bool, get_scheduler().start_agent_task(task_id))
|
|
|
|
|
|
def get_agent_task_next_run(task_id: int) -> Optional[Any]:
|
|
"""查询 Agent 自主定时任务的下一次运行时间。"""
|
|
return get_scheduler().get_agent_task_next_run(task_id)
|
|
|
|
|
|
def update_agent_task_job(task_id: int) -> Optional[Any]:
|
|
"""更新 Agent 自主定时任务的注册信息,返回下一次运行时间。"""
|
|
return get_scheduler().update_agent_task_job(task_id)
|
|
|
|
|
|
def remove_agent_task_job(task_id: int) -> None:
|
|
"""移除 Agent 自主定时任务的注册信息。"""
|
|
get_scheduler().remove_agent_task_job(task_id)
|