ForcePilot/backend/package/yuxi/services/scheduler_tick.py

174 lines
6.6 KiB
Python
Raw Normal View History

"""scheduler 的 ARQ 桥接层。
本模块是 ARQ 队列协议模块级函数 + ``ctx`` ``SchedulerService``类实例方法
之间的适配器ARQ 只认模块级函数无法直接调用持有依赖的 ``SchedulerService`` 实例
本模块在 ARQ 函数内部从 ``ctx`` 取出 worker 进程级 ``HandlerRegistry`` ``arq_pool``
按请求创建 ``SchedulerService`` 实例转调其 ``tick`` / ``execute_task`` 方法
对外暴露
- ``run_scheduler_tick(ctx)``ARQ cron 入口周期性扫描到期任务并入队执行
``WorkerSettings.cron_jobs`` 注册默认每分钟触发对齐
``config.scheduler_tick_interval_seconds`` 默认 60s
- ``execute_scheduled_task(ctx, task_id, run_id, triggered_by, scheduled_at=None)``
ARQ worker 入口执行单个到期 / 手动触发的任务函数名与
``SchedulerService._EXECUTE_TASK_FUNCTION`` 对齐 ``tick`` / ``trigger_task``
通过 ``pool.enqueue_job`` 入队
- ``register_builtin_handlers(registry)``注册 scheduler 自带的维护型 handler
``RunLogCleanupHandler`` / ``IdempotencyCleanupHandler`` worker 启动钩子调用
依赖方向依赖 ``scheduler/infrastructure/container``factory+
``scheduler/framework/handlers``维护型 handler+ ``storage/postgres/manager``
db 会话不依赖 ``use_cases`` 的具体实现仅通过 factory 间接装配
进程级生命周期
- ``HandlerRegistry``worker 启动时创建并填充塞入 ``ctx['scheduler_handler_registry']``
- ``arq_pool``ARQ worker 自动注入到 ``ctx['pool']``
- ``SchedulerService``请求级每次 ARQ 函数调用新建含独立 db session
"""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from yuxi.config.app import config as app_config
from yuxi.scheduler.framework.handlers import (
IdempotencyCleanupHandler,
RunLogCleanupHandler,
)
from yuxi.scheduler.framework.runtime import HandlerRegistry
from yuxi.scheduler.infrastructure.container import create_worker_scheduler_service
from yuxi.storage.postgres.manager import pg_manager
from yuxi.utils.logging_config import logger
if TYPE_CHECKING:
from arq import ArqRedis
from arq.worker import WorkerContext
__all__ = [
"execute_scheduled_task",
"register_builtin_handlers",
"run_scheduler_tick",
]
# ctx 中存储 HandlerRegistry 的键名(与 _worker_startup 约定)
_HANDLER_REGISTRY_CTX_KEY = "scheduler_handler_registry"
def register_builtin_handlers(registry: HandlerRegistry) -> None:
"""注册 scheduler 自带的维护型 handler 到 ``HandlerRegistry``。
worker 启动钩子``_worker_startup``在创建 registry 后调用
注册的 handler 通过 ``scheduled_tasks`` 表配置触发周期如每天凌晨清理
Args:
registry: worker 进程级 handler 注册表
"""
session_factory = pg_manager.get_async_session_context
registry.register(
RunLogCleanupHandler(
session_factory=session_factory,
default_retention_days=app_config.scheduler_run_log_retention_days,
)
)
registry.register(
IdempotencyCleanupHandler(
session_factory=session_factory,
default_retention_hours=app_config.scheduler_idempotency_retention_hours,
)
)
logger.info(
"scheduler_builtin_handlers_registered",
extra={"handlers": ["run_log_cleanup", "idempotency_cleanup"]},
)
async def run_scheduler_tick(ctx: WorkerContext) -> None:
"""ARQ cron 入口:周期性扫描到期任务并入队执行。
``WorkerSettings.cron_jobs`` 注册默认每分钟触发内部转调
``SchedulerService.tick()``扫描 ``next_run_at <= now`` 的任务应用
stagger 抖动与 block_strategy 后入队 ``execute_scheduled_task``
Args:
ctx: ARQ worker 上下文含进程级 ``HandlerRegistry`` ``arq_pool``
"""
if not app_config.scheduler_enabled:
return
registry: HandlerRegistry | None = ctx.get(_HANDLER_REGISTRY_CTX_KEY)
if registry is None:
logger.error("scheduler_tick_handler_registry_missing")
return
arq_pool: ArqRedis | None = ctx.get("pool")
if arq_pool is None:
logger.error("scheduler_tick_arq_pool_missing")
return
async with pg_manager.get_async_session_context() as db:
service = create_worker_scheduler_service(
db, handler_registry=registry, arq_pool=arq_pool
)
await service.tick()
async def execute_scheduled_task(
ctx: WorkerContext,
task_id: str,
run_id: str,
triggered_by: str,
scheduled_at: str | None = None,
) -> None:
"""ARQ worker 入口:执行单个到期 / 手动触发的任务。
``tick``auto 触发 ``trigger_task``manual 触发通过
``pool.enqueue_job("execute_scheduled_task", ...)`` 入队函数名与
``SchedulerService._EXECUTE_TASK_FUNCTION`` 对齐
Args:
ctx: ARQ worker 上下文含进程级 ``HandlerRegistry`` ``arq_pool``
task_id: 任务标识
run_id: 本次执行标识 tick trigger_task 生成
triggered_by: 触发来源``auto`` / ``manual``
scheduled_at: 本次计划执行时间的 ISO 字符串tick 触发时为 acquire 前的
``next_run_at``手动触发时为 None
"""
registry: HandlerRegistry | None = ctx.get(_HANDLER_REGISTRY_CTX_KEY)
if registry is None:
logger.error(
"scheduler_execute_handler_registry_missing",
extra={"task_id": task_id, "run_id": run_id},
)
return
arq_pool: ArqRedis | None = ctx.get("pool")
if arq_pool is None:
logger.error(
"scheduler_execute_arq_pool_missing",
extra={"task_id": task_id, "run_id": run_id},
)
return
scheduled_at_dt: datetime | None = None
if scheduled_at is not None:
try:
scheduled_at_dt = datetime.fromisoformat(scheduled_at)
except ValueError:
logger.warning(
"scheduler_execute_invalid_scheduled_at",
extra={"task_id": task_id, "run_id": run_id, "scheduled_at": scheduled_at},
)
async with pg_manager.get_async_session_context() as db:
service = create_worker_scheduler_service(
db, handler_registry=registry, arq_pool=arq_pool
)
await service.execute_task(
task_id=task_id,
run_id=run_id,
triggered_by=triggered_by,
scheduled_at=scheduled_at_dt,
)