ForcePilot/backend/package/yuxi/scheduler/use_cases/dto/scheduler.py

582 lines
18 KiB
Python
Raw Normal View History

"""scheduler 限界上下文用例 DTO 定义。
包含
- 任务管理 Input/Output DTOCRUD / 暂停 / 恢复 / 手动触发
- 执行日志 Input/Output DTO列表查询
- 可观测性 Output DTOhandler 摘要 / 状态计数 / 即将执行 / 健康检查
- 运维恢复 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="触发来源autotick 自动)/ 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)