本次提交完成了定时任务调度限界上下文的全量基础架构搭建,包括: 1. 基于六边形架构的完整分层(core/use_cases/framework/adapters/infrastructure) 2. 任务调度核心领域模型、端口契约与校验工具 3. 持久化适配器层与SQLAlchemy仓储实现 4. 调度运行时核心组件(handler注册表、执行引擎) 5. 内置维护型任务handler(日志清理、幂等记录清理) 6. 全局异常处理器与PostgreSQL表结构适配 7. ARQ worker调度任务集成与启动装配逻辑
174 lines
6.6 KiB
Python
174 lines
6.6 KiB
Python
"""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,
|
||
)
|