本次提交包含多维度功能增强: 1. 新增TaskRecycleCleanupHandler,实现过期软删除任务物理清理能力 2. 扩展任务统计模型,新增死信任务计数并完善统计逻辑 3. 新增异常任务聚合查询接口,支持按死信/连续失败/长期未执行分类返回 4. 实现任务批量暂停/恢复/软删除操作 5. 扩展运行日志与任务查询过滤条件,新增handler_name维度 6. 优化健康检查逻辑,支持动态健康窗口并返回配置快照 7. 完善数据映射与DTO定义,补充缺失字段与类型支持
236 lines
8.5 KiB
Python
236 lines
8.5 KiB
Python
"""领域模型(dataclass) ↔ Output DTO(Pydantic) 转换。
|
||
|
||
规范 §6.1/§7.3(对齐 ``external_systems/use_cases/mappers.py``):
|
||
- persistence 返回领域模型 dataclass,service 返回 Output DTO
|
||
- scheduler 域无敏感字段,mapper 仅做字段映射与 DateTime → ISO 字符串格式化
|
||
- use_cases 层不调用 ORM 的 ``to_dict`` 方法
|
||
|
||
DateTime 字段(``run_at`` / ``last_run_at`` / ``next_run_at`` / ``started_at`` /
|
||
``finished_at`` / ``created_at`` / ``updated_at`` / ``deleted_at``)在 dataclass
|
||
中以 naive UTC ``datetime`` 表达,mapper 通过 ``format_utc_datetime`` 统一格式化
|
||
为 ISO 8601 字符串写入 Output DTO。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from yuxi.scheduler.core.models import (
|
||
DailyStat,
|
||
HandlerSummary,
|
||
ScheduledTask,
|
||
ScheduledTaskRunLog,
|
||
)
|
||
from yuxi.scheduler.use_cases.dto.scheduler import (
|
||
CountByStatusOutput,
|
||
DailyStatOutput,
|
||
GetHealthOutput,
|
||
HandlerSummaryOutput,
|
||
ListAnomaliesOutput,
|
||
ListDailyStatsOutput,
|
||
ListHandlerSummaryOutput,
|
||
ListUpcomingOutput,
|
||
RunLogOutput,
|
||
SchedulerConfigOutput,
|
||
TaskOutput,
|
||
TriggerTaskOutput,
|
||
)
|
||
from yuxi.utils.datetime_utils import format_utc_datetime
|
||
|
||
__all__ = [
|
||
"daily_stat_to_dict",
|
||
"to_count_by_status_output",
|
||
"to_daily_stat_output",
|
||
"to_get_health_output",
|
||
"to_handler_summary_output",
|
||
"to_list_anomalies_output",
|
||
"to_list_daily_stats_output",
|
||
"to_run_log_output",
|
||
"to_task_output",
|
||
"to_trigger_task_output",
|
||
]
|
||
|
||
|
||
def to_task_output(task: ScheduledTask) -> TaskOutput:
|
||
"""``ScheduledTask`` dataclass → ``TaskOutput`` DTO。
|
||
|
||
DateTime 字段通过 ``format_utc_datetime`` 格式化为 ISO 8601 字符串。
|
||
"""
|
||
return TaskOutput(
|
||
task_id=task.task_id,
|
||
handler_name=task.handler_name,
|
||
owner_scope=task.owner_scope,
|
||
owner_id=task.owner_id,
|
||
schedule_kind=task.schedule_kind,
|
||
cron_expression=task.cron_expression,
|
||
run_at=format_utc_datetime(task.run_at),
|
||
tz=task.tz,
|
||
payload=task.payload or {},
|
||
enabled=task.enabled,
|
||
delete_after_run=task.delete_after_run,
|
||
block_strategy=task.block_strategy,
|
||
stagger_seconds=task.stagger_seconds,
|
||
consecutive_errors=task.consecutive_errors,
|
||
status=task.status,
|
||
last_run_at=format_utc_datetime(task.last_run_at),
|
||
next_run_at=format_utc_datetime(task.next_run_at),
|
||
last_error=task.last_error,
|
||
created_by=task.created_by,
|
||
updated_by=task.updated_by,
|
||
id=task.id,
|
||
created_at=format_utc_datetime(task.created_at),
|
||
updated_at=format_utc_datetime(task.updated_at),
|
||
is_deleted=task.is_deleted,
|
||
deleted_at=format_utc_datetime(task.deleted_at),
|
||
)
|
||
|
||
|
||
def to_trigger_task_output(task: ScheduledTask, run_id: str) -> TriggerTaskOutput:
|
||
"""``ScheduledTask`` dataclass + run_id → ``TriggerTaskOutput`` DTO。"""
|
||
return TriggerTaskOutput(run_id=run_id, task=to_task_output(task))
|
||
|
||
|
||
def to_run_log_output(log: ScheduledTaskRunLog) -> RunLogOutput:
|
||
"""``ScheduledTaskRunLog`` dataclass → ``RunLogOutput`` DTO。
|
||
|
||
计算派生字段 ``duration_seconds``:当 ``started_at`` 与 ``finished_at``
|
||
均非 None 时返回秒级执行时长,否则返回 None(running 状态)。
|
||
"""
|
||
duration_seconds: float | None = None
|
||
if log.started_at is not None and log.finished_at is not None:
|
||
delta = log.finished_at - log.started_at
|
||
duration_seconds = round(delta.total_seconds(), 3)
|
||
return RunLogOutput(
|
||
task_id=log.task_id,
|
||
handler_name=log.handler_name,
|
||
run_id=log.run_id,
|
||
triggered_by=log.triggered_by,
|
||
status=log.status,
|
||
error_message=log.error_message,
|
||
output=log.output,
|
||
started_at=format_utc_datetime(log.started_at),
|
||
finished_at=format_utc_datetime(log.finished_at),
|
||
duration_seconds=duration_seconds,
|
||
created_by=log.created_by,
|
||
updated_by=log.updated_by,
|
||
id=log.id,
|
||
created_at=format_utc_datetime(log.created_at),
|
||
updated_at=format_utc_datetime(log.updated_at),
|
||
is_deleted=log.is_deleted,
|
||
deleted_at=format_utc_datetime(log.deleted_at),
|
||
)
|
||
|
||
|
||
def to_handler_summary_output(summary: HandlerSummary) -> HandlerSummaryOutput:
|
||
"""``HandlerSummary`` dataclass → ``HandlerSummaryOutput`` DTO。
|
||
|
||
``active_task_count`` / ``dead_letter_count`` 已由仓储层聚合,此处直接透传。
|
||
"""
|
||
return HandlerSummaryOutput(
|
||
handler_name=summary.name,
|
||
task_count=summary.task_count,
|
||
active_task_count=summary.active_task_count,
|
||
dead_letter_count=summary.dead_letter_count,
|
||
last_active_at=format_utc_datetime(summary.last_active_at),
|
||
)
|
||
|
||
|
||
def to_count_by_status_output(dist: dict[str, int]) -> CountByStatusOutput:
|
||
"""状态分布字典 → ``CountByStatusOutput`` DTO。
|
||
|
||
仓储层 ``count_by_status`` 返回 ``{status: count}``,未出现的状态不在字典中,
|
||
这里统一补 0。
|
||
"""
|
||
return CountByStatusOutput(
|
||
active=dist.get("active", 0),
|
||
paused=dist.get("paused", 0),
|
||
dead_letter=dist.get("dead_letter", 0),
|
||
)
|
||
|
||
|
||
def to_get_health_output(
|
||
*,
|
||
last_tick_at: Any,
|
||
active_task_count: int,
|
||
dead_letter_count: int,
|
||
running_count: int,
|
||
healthy: bool,
|
||
config: SchedulerConfigOutput,
|
||
) -> GetHealthOutput:
|
||
"""构造 ``GetHealthOutput`` DTO。
|
||
|
||
``healthy`` 由调用方按 worker 活跃度判断后传入;``config`` 为从
|
||
``app_config`` 读取的当前生效配置快照。
|
||
"""
|
||
return GetHealthOutput(
|
||
status="healthy" if healthy else "unhealthy",
|
||
last_tick_at=format_utc_datetime(last_tick_at),
|
||
active_task_count=active_task_count,
|
||
dead_letter_count=dead_letter_count,
|
||
running_count=running_count,
|
||
config=config,
|
||
)
|
||
|
||
|
||
def daily_stat_to_dict(stat: DailyStat) -> dict[str, Any]:
|
||
"""``DailyStat`` dataclass → dict(供指标导出使用)。
|
||
|
||
指标服务(``SchedulerMetricService``)从 ``DailyStat`` 提取字段构造
|
||
Prometheus 文本格式,此处提供统一的 dict 转换避免重复字段读取。
|
||
"""
|
||
return {
|
||
"stat_date": stat.stat_date,
|
||
"handler_name": stat.handler_name,
|
||
"success_count": stat.success_count,
|
||
"failure_count": stat.failure_count,
|
||
"timeout_count": stat.timeout_count,
|
||
"dead_letter_count": stat.dead_letter_count,
|
||
}
|
||
|
||
|
||
def to_daily_stat_output(stat: DailyStat) -> DailyStatOutput:
|
||
"""``DailyStat`` dataclass → ``DailyStatOutput`` DTO。
|
||
|
||
计算派生字段 ``total_count`` / ``success_rate``,``stat_date`` 格式化为
|
||
ISO 日期字符串(``YYYY-MM-DD``)。``dead_letter_count`` 视为失败终态,
|
||
纳入 ``total_count`` 分母以反映真实成功率。
|
||
"""
|
||
total_count = stat.success_count + stat.failure_count + stat.timeout_count + stat.dead_letter_count
|
||
success_rate = stat.success_count / total_count if total_count > 0 else 0.0
|
||
return DailyStatOutput(
|
||
stat_date=stat.stat_date.isoformat(),
|
||
handler_name=stat.handler_name,
|
||
success_count=stat.success_count,
|
||
failure_count=stat.failure_count,
|
||
timeout_count=stat.timeout_count,
|
||
dead_letter_count=stat.dead_letter_count,
|
||
total_count=total_count,
|
||
success_rate=success_rate,
|
||
)
|
||
|
||
|
||
def to_list_daily_stats_output(stats: list[DailyStat]) -> ListDailyStatsOutput:
|
||
"""``DailyStat`` 列表 → ``ListDailyStatsOutput`` DTO。"""
|
||
return ListDailyStatsOutput(items=[to_daily_stat_output(s) for s in stats])
|
||
|
||
|
||
def to_list_upcoming_output(tasks: list[ScheduledTask]) -> ListUpcomingOutput:
|
||
"""``ScheduledTask`` 列表 → ``ListUpcomingOutput`` DTO。"""
|
||
return ListUpcomingOutput(items=[to_task_output(t) for t in tasks])
|
||
|
||
|
||
def to_list_anomalies_output(groups: dict[str, list[ScheduledTask]]) -> ListAnomaliesOutput:
|
||
"""异常任务分组映射 → ``ListAnomaliesOutput`` DTO。"""
|
||
return ListAnomaliesOutput(
|
||
dead_letter=[to_task_output(t) for t in groups.get("dead_letter", [])],
|
||
consecutive_failure=[to_task_output(t) for t in groups.get("consecutive_failure", [])],
|
||
stale=[to_task_output(t) for t in groups.get("stale", [])],
|
||
)
|
||
|
||
|
||
def to_list_handler_summary_output(
|
||
summaries: list[HandlerSummary],
|
||
) -> ListHandlerSummaryOutput:
|
||
"""``HandlerSummary`` 列表 → ``ListHandlerSummaryOutput`` DTO。"""
|
||
return ListHandlerSummaryOutput(items=[to_handler_summary_output(s) for s in summaries])
|