Files
MoviePilot/app/application/scheduling.py

182 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""调度器工具服务门面。
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)