本次提交完成了定时任务调度限界上下文的全量基础架构搭建,包括: 1. 基于六边形架构的完整分层(core/use_cases/framework/adapters/infrastructure) 2. 任务调度核心领域模型、端口契约与校验工具 3. 持久化适配器层与SQLAlchemy仓储实现 4. 调度运行时核心组件(handler注册表、执行引擎) 5. 内置维护型任务handler(日志清理、幂等记录清理) 6. 全局异常处理器与PostgreSQL表结构适配 7. ARQ worker调度任务集成与启动装配逻辑
582 lines
18 KiB
Python
582 lines
18 KiB
Python
"""scheduler 限界上下文用例 DTO 定义。
|
||
|
||
包含:
|
||
- 任务管理 Input/Output DTO(CRUD / 暂停 / 恢复 / 手动触发)
|
||
- 执行日志 Input/Output DTO(列表查询)
|
||
- 可观测性 Output DTO(handler 摘要 / 状态计数 / 即将执行 / 健康检查)
|
||
- 运维恢复 Input/Output DTO(回收僵尸执行)
|
||
|
||
字段约束(``min_length`` / ``max_length`` / ``pattern`` / ``ge`` / ``le``)在 DTO 中
|
||
声明,由 Pydantic 在构造时校验。DateTime 字段在 Output DTO 中以 ISO 格式字符串
|
||
表达(``str | None``),由 Router 层在序列化时格式化。JSON 字段(如 ``payload``)
|
||
使用 ``dict[str, Any]``。
|
||
|
||
对齐 ``external_systems/use_cases/dto/system.py`` 写法。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 任务管理 DTO
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class CreateTaskInput(BaseModel):
|
||
"""创建定时任务输入 DTO。
|
||
|
||
``schedule_kind`` 决定调度类型:``cron`` 必填 ``cron_expression``,
|
||
``at`` 必填 ``run_at``。``task_id`` 由系统生成(UUID4),不接受客户端指定。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
handler_name: str = Field(
|
||
...,
|
||
min_length=1,
|
||
max_length=128,
|
||
pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]*$",
|
||
description="handler 标识,与 TaskHandler.name 引用一致",
|
||
)
|
||
owner_scope: str = Field(
|
||
...,
|
||
min_length=1,
|
||
max_length=64,
|
||
description="任务归属范围:system(平台级)/ business(业务方)",
|
||
)
|
||
owner_id: str = Field(
|
||
...,
|
||
min_length=1,
|
||
max_length=128,
|
||
description="业务方标识,格式如 channel_type:account_id",
|
||
)
|
||
schedule_kind: str = Field(
|
||
...,
|
||
pattern=r"^(cron|at)$",
|
||
description="调度类型:cron(周期)/ at(一次性)",
|
||
)
|
||
cron_expression: str | None = Field(
|
||
default=None,
|
||
max_length=128,
|
||
description="cron 表达式(标准 5 字段),schedule_kind=cron 时必填",
|
||
)
|
||
run_at: str | None = Field(
|
||
default=None,
|
||
description="一次性执行时间(ISO 格式字符串),schedule_kind=at 时必填且须为未来时间",
|
||
)
|
||
tz: str = Field(
|
||
default="Asia/Shanghai",
|
||
max_length=64,
|
||
description="时区标识,如 Asia/Shanghai",
|
||
)
|
||
payload: dict[str, Any] = Field(
|
||
default_factory=dict,
|
||
description="任务执行时传给 handler 的参数,序列化后不超过 64KB",
|
||
)
|
||
enabled: bool = Field(default=True, description="是否启用")
|
||
delete_after_run: bool = Field(
|
||
default=False,
|
||
description="一次性任务执行完成后是否自动删除(schedule_kind=at 时有意义)",
|
||
)
|
||
block_strategy: str = Field(
|
||
default="discard_later",
|
||
pattern=r"^(discard_later)$",
|
||
description="阻塞策略:discard_later(存在 running 执行时丢弃本次触发)",
|
||
)
|
||
created_by: str = Field(default="system", max_length=128)
|
||
|
||
|
||
class UpdateTaskInput(BaseModel):
|
||
"""更新定时任务输入 DTO。
|
||
|
||
允许更新字段(PRD §FR-ST-05):``cron_expression`` / ``run_at`` / ``tz`` /
|
||
``payload`` / ``enabled`` / ``delete_after_run`` / ``owner_scope`` / ``owner_id``。
|
||
``schedule_kind`` 与 ``handler_name`` 不可修改。修改 ``cron_expression`` 时
|
||
由用例层重新计算 ``next_run_at`` 与 ``stagger_seconds``。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str = Field(
|
||
...,
|
||
min_length=1,
|
||
max_length=64,
|
||
description="任务标识(UUID4)",
|
||
)
|
||
cron_expression: str | None = Field(
|
||
default=None,
|
||
max_length=128,
|
||
description="新的 cron 表达式,传入时重算 next_run_at 与 stagger_seconds",
|
||
)
|
||
run_at: str | None = Field(
|
||
default=None,
|
||
description="新的一次性执行时间(ISO 格式字符串)",
|
||
)
|
||
tz: str | None = Field(
|
||
default=None,
|
||
max_length=64,
|
||
description="新的时区标识",
|
||
)
|
||
payload: dict[str, Any] | None = Field(
|
||
default=None,
|
||
description="新的 payload,传入时整体替换",
|
||
)
|
||
enabled: bool | None = Field(default=None, description="是否启用")
|
||
delete_after_run: bool | None = Field(
|
||
default=None,
|
||
description="一次性任务执行完成后是否自动删除",
|
||
)
|
||
owner_scope: str | None = Field(
|
||
default=None,
|
||
min_length=1,
|
||
max_length=64,
|
||
)
|
||
owner_id: str | None = Field(
|
||
default=None,
|
||
min_length=1,
|
||
max_length=128,
|
||
)
|
||
updated_by: str | None = Field(default=None, max_length=128)
|
||
|
||
|
||
class GetTaskInput(BaseModel):
|
||
"""获取任务详情输入 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str = Field(..., min_length=1, max_length=64)
|
||
|
||
|
||
class DeleteTaskInput(BaseModel):
|
||
"""删除任务输入 DTO(软删除)。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str = Field(..., min_length=1, max_length=64)
|
||
updated_by: str | None = Field(default=None, max_length=128)
|
||
|
||
|
||
class PauseTaskInput(BaseModel):
|
||
"""暂停任务输入 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str = Field(..., min_length=1, max_length=64)
|
||
updated_by: str | None = Field(default=None, max_length=128)
|
||
|
||
|
||
class ResumeTaskInput(BaseModel):
|
||
"""恢复任务输入 DTO。
|
||
|
||
对 ``status=paused`` 任务调用 ``pause`` 的逆操作;对 ``status=dead_letter``
|
||
任务重置 ``consecutive_errors`` / ``last_error`` 并恢复为 ``active``。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str = Field(..., min_length=1, max_length=64)
|
||
updated_by: str | None = Field(default=None, max_length=128)
|
||
|
||
|
||
class TriggerTaskInput(BaseModel):
|
||
"""手动触发任务输入 DTO。
|
||
|
||
``triggered_by`` 标识触发来源(``auto`` / ``manual``),手动触发不入正常
|
||
``next_run_at`` 流,不影响下一次自动执行。死信任务允许手动触发用于验证
|
||
handler 是否已修复,但不重置 ``status``。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str = Field(..., min_length=1, max_length=64)
|
||
triggered_by: str = Field(
|
||
default="manual",
|
||
pattern=r"^(auto|manual)$",
|
||
description="触发来源:auto(tick 自动)/ manual(管理员手动)",
|
||
)
|
||
payload: dict[str, Any] | None = Field(
|
||
default=None,
|
||
description="可选 payload 覆盖,None 时使用任务定义中的 payload",
|
||
)
|
||
|
||
|
||
class TaskOutput(BaseModel):
|
||
"""任务输出 DTO。字段与 ``ScheduledTask`` dataclass 对齐。
|
||
|
||
DateTime 字段(``run_at`` / ``last_run_at`` / ``next_run_at`` / ``created_at``
|
||
/ ``updated_at`` / ``deleted_at``)以 ISO 格式字符串表达,由 Router 层格式化。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str
|
||
handler_name: str
|
||
owner_scope: str
|
||
owner_id: str
|
||
schedule_kind: str
|
||
cron_expression: str | None = None
|
||
run_at: str | None = None
|
||
tz: str = "Asia/Shanghai"
|
||
payload: dict[str, Any] = Field(default_factory=dict)
|
||
enabled: bool = True
|
||
delete_after_run: bool = False
|
||
block_strategy: str = "discard_later"
|
||
stagger_seconds: int | None = None
|
||
consecutive_errors: int = 0
|
||
status: str = "active"
|
||
last_run_at: str | None = None
|
||
next_run_at: str | None = None
|
||
last_error: str | None = None
|
||
created_by: str = "system"
|
||
updated_by: str | None = None
|
||
id: int | None = None
|
||
created_at: str | None = None
|
||
updated_at: str | None = None
|
||
is_deleted: int = 0
|
||
deleted_at: str | None = None
|
||
|
||
|
||
class ListTasksInput(BaseModel):
|
||
"""列出任务输入 DTO。
|
||
|
||
过滤字段对齐 PRD §FR-ST-05 列表 API 查询参数与仓储层 ``list`` 方法签名。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
page: int = Field(default=1, ge=1, description="页码,从 1 开始")
|
||
page_size: int = Field(default=20, ge=1, le=100, description="每页条数,最大 100")
|
||
owner_scope: str | None = Field(default=None, max_length=64)
|
||
owner_id: str | None = Field(default=None, max_length=128)
|
||
handler_name: str | None = Field(default=None, max_length=128)
|
||
enabled: bool | None = None
|
||
status: str | None = Field(
|
||
default=None,
|
||
pattern=r"^(active|paused|dead_letter)$",
|
||
description="任务状态过滤",
|
||
)
|
||
|
||
|
||
class ListTasksOutput(BaseModel):
|
||
"""列出任务输出 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
items: list[TaskOutput] = Field(default_factory=list)
|
||
total: int = 0
|
||
page: int = 1
|
||
page_size: int = 20
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 执行日志 DTO
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ListRunLogsInput(BaseModel):
|
||
"""列出执行日志输入 DTO。
|
||
|
||
``task_id`` 必填,按任务维度查询执行明细。``status`` / ``start_date`` /
|
||
``end_date`` 为可选过滤字段,``start_date`` / ``end_date`` 为 ISO 格式字符串,
|
||
由用例层解析为 ``datetime`` 后传入仓储层。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str = Field(..., min_length=1, max_length=64)
|
||
page: int = Field(default=1, ge=1)
|
||
page_size: int = Field(default=20, ge=1, le=100)
|
||
status: str | None = Field(
|
||
default=None,
|
||
pattern=r"^(running|success|failure|timeout|skipped)$",
|
||
description="执行状态过滤",
|
||
)
|
||
start_date: str | None = Field(
|
||
default=None,
|
||
description="起始时间(ISO 格式字符串),按 started_at 过滤",
|
||
)
|
||
end_date: str | None = Field(
|
||
default=None,
|
||
description="截止时间(ISO 格式字符串),按 started_at 过滤",
|
||
)
|
||
|
||
|
||
class GetRunLogInput(BaseModel):
|
||
"""获取执行日志详情输入 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
run_id: str = Field(..., min_length=1, max_length=64)
|
||
|
||
|
||
class RunLogOutput(BaseModel):
|
||
"""执行日志输出 DTO。字段与 ``ScheduledTaskRunLog`` dataclass 对齐。
|
||
|
||
DateTime 字段(``started_at`` / ``finished_at`` / ``created_at`` /
|
||
``updated_at`` / ``deleted_at``)以 ISO 格式字符串表达。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str
|
||
run_id: str
|
||
triggered_by: str = "auto"
|
||
status: str = "running"
|
||
error_message: str | None = None
|
||
output: dict[str, Any] | None = None
|
||
started_at: str | None = None
|
||
finished_at: str | None = None
|
||
created_by: str = "system"
|
||
updated_by: str | None = None
|
||
id: int | None = None
|
||
created_at: str | None = None
|
||
updated_at: str | None = None
|
||
is_deleted: int = 0
|
||
deleted_at: str | None = None
|
||
|
||
|
||
class ListRunLogsOutput(BaseModel):
|
||
"""列出执行日志输出 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
items: list[RunLogOutput] = Field(default_factory=list)
|
||
total: int = 0
|
||
page: int = 1
|
||
page_size: int = 20
|
||
|
||
|
||
class ListAllRunLogsInput(BaseModel):
|
||
"""跨任务执行日志查询输入 DTO。
|
||
|
||
``task_id`` 与 ``status`` 均可选,但不可同时为空(由用例层校验),
|
||
避免无过滤的全表扫描。``start_date`` / ``end_date`` 为 ISO 格式字符串,
|
||
由用例层解析为 ``datetime`` 后传入仓储层。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
page: int = Field(default=1, ge=1)
|
||
page_size: int = Field(default=20, ge=1, le=100)
|
||
status: str | None = Field(
|
||
default=None,
|
||
pattern=r"^(running|success|failure|timeout|skipped)$",
|
||
description="执行状态过滤",
|
||
)
|
||
start_date: str | None = Field(
|
||
default=None,
|
||
description="起始时间(ISO 格式字符串),按 started_at 过滤",
|
||
)
|
||
end_date: str | None = Field(
|
||
default=None,
|
||
description="截止时间(ISO 格式字符串),按 started_at 过滤",
|
||
)
|
||
task_id: str | None = Field(default=None, min_length=1, max_length=64)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 可观测性 DTO
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class HandlerSummaryOutput(BaseModel):
|
||
"""handler 聚合摘要输出 DTO。
|
||
|
||
对齐 PRD §FR-ST-05 ``GET /handlers`` 端点响应字段,走 DB 聚合
|
||
(``task_repo.list_handler_summary``),不查内存注册表。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
handler_name: str
|
||
description: str = ""
|
||
task_count: int = 0
|
||
active_task_count: int = 0
|
||
last_active_at: str | None = None
|
||
|
||
|
||
class ListHandlerSummaryOutput(BaseModel):
|
||
"""handler 聚合摘要列表输出 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
items: list[HandlerSummaryOutput] = Field(default_factory=list)
|
||
|
||
|
||
class CountByStatusOutput(BaseModel):
|
||
"""任务状态计数输出 DTO。
|
||
|
||
对齐 PRD §FR-ST-07 健康检查与指标需求,按 ``status`` 维度统计任务数。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
active: int = 0
|
||
paused: int = 0
|
||
dead_letter: int = 0
|
||
|
||
|
||
class ListUpcomingInput(BaseModel):
|
||
"""即将执行任务查询输入 DTO。
|
||
|
||
对齐 PRD §FR-ST-09 统计查询,按 ``next_run_at`` 升序返回即将执行的任务列表。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
limit: int = Field(default=100, ge=1, le=1000, description="返回条数上限")
|
||
handler_name: str | None = Field(default=None, max_length=128)
|
||
|
||
|
||
class ListUpcomingOutput(BaseModel):
|
||
"""即将执行任务列表输出 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
items: list[TaskOutput] = Field(default_factory=list)
|
||
|
||
|
||
class GetHealthOutput(BaseModel):
|
||
"""调度器健康检查输出 DTO。
|
||
|
||
对齐 PRD §FR-ST-07 健康检查端点响应:``status`` 表示 worker 是否在最近
|
||
5 分钟内执行 tick;``last_tick_at`` 为最近 tick 时间;``active_task_count``
|
||
/ ``dead_letter_count`` / ``running_count`` 为关键状态计数。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
status: str = Field(
|
||
default="healthy",
|
||
pattern=r"^(healthy|unhealthy)$",
|
||
description="健康状态:healthy(最近 5 分钟内有 tick)/ unhealthy",
|
||
)
|
||
last_tick_at: str | None = Field(default=None, description="最近 tick 时间(ISO 格式字符串)")
|
||
active_task_count: int = 0
|
||
dead_letter_count: int = 0
|
||
running_count: int = 0
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 日聚合统计 DTO
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ListDailyStatsInput(BaseModel):
|
||
"""日聚合统计查询输入 DTO。
|
||
|
||
``start_date`` / ``end_date`` 为 ISO 日期字符串(``YYYY-MM-DD``),由用例层
|
||
解析为 ``date`` 后传入仓储层。日期范围上限 90 天。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
start_date: str = Field(..., description="ISO 日期 YYYY-MM-DD")
|
||
end_date: str = Field(..., description="ISO 日期 YYYY-MM-DD")
|
||
handler_name: str | None = Field(default=None, max_length=128)
|
||
|
||
|
||
class DailyStatOutput(BaseModel):
|
||
"""日聚合统计输出 DTO。字段对齐 ``DailyStat`` dataclass,补充派生字段。
|
||
|
||
``total_count`` / ``success_rate`` 为派生字段,由 mapper 计算:
|
||
``total_count = success_count + failure_count + timeout_count``,
|
||
``success_rate = success_count / total_count if total_count > 0 else 0.0``。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
stat_date: str
|
||
handler_name: str
|
||
success_count: int = 0
|
||
failure_count: int = 0
|
||
timeout_count: int = 0
|
||
dead_letter_count: int = 0
|
||
total_count: int = 0
|
||
success_rate: float = 0.0
|
||
|
||
|
||
class ListDailyStatsOutput(BaseModel):
|
||
"""日聚合统计列表输出 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
items: list[DailyStatOutput] = Field(default_factory=list)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 运维恢复 DTO
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ReclaimStaleRunsInput(BaseModel):
|
||
"""回收僵尸执行输入 DTO。
|
||
|
||
用于 scheduler 重启后回收 ``status=running`` 但实际已超时的执行记录,
|
||
将其标记为 ``failure`` 并写入回收错误信息。``timeout_seconds`` 为可选参数,
|
||
未传入时由用例层从配置取默认值。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
timeout_seconds: int | None = Field(
|
||
default=None,
|
||
ge=1,
|
||
description="执行超时阈值(秒),None 时从配置取默认值",
|
||
)
|
||
|
||
|
||
class ReclaimStaleRunsOutput(BaseModel):
|
||
"""回收僵尸执行输出 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
reclaimed_count: int = 0
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 任务回收站 DTO
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ListDeletedTasksInput(BaseModel):
|
||
"""列出已删除任务(回收站)输入 DTO。
|
||
|
||
``start_date`` / ``end_date`` 为 ISO 格式字符串,由用例层解析为 ``datetime``
|
||
后按 ``deleted_at`` 过滤。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
page: int = Field(default=1, ge=1)
|
||
page_size: int = Field(default=20, ge=1, le=100)
|
||
start_date: str | None = Field(
|
||
default=None,
|
||
description="起始时间(ISO 格式字符串),按 deleted_at 过滤",
|
||
)
|
||
end_date: str | None = Field(
|
||
default=None,
|
||
description="截止时间(ISO 格式字符串),按 deleted_at 过滤",
|
||
)
|
||
|
||
|
||
class RestoreTaskInput(BaseModel):
|
||
"""恢复已删除任务输入 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str = Field(..., min_length=1, max_length=64)
|
||
updated_by: str | None = Field(default=None, max_length=128)
|
||
|
||
|
||
class HardDeleteTaskInput(BaseModel):
|
||
"""硬删除任务输入 DTO。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
task_id: str = Field(..., min_length=1, max_length=64)
|
||
updated_by: str | None = Field(default=None, max_length=128)
|