ForcePilot/backend/package/yuxi/scheduler/use_cases/mappers.py
Kris 36d0add930 feat: 新增回收站任务清理handler,完善任务统计与批量操作能力
本次提交包含多维度功能增强:
1. 新增TaskRecycleCleanupHandler,实现过期软删除任务物理清理能力
2. 扩展任务统计模型,新增死信任务计数并完善统计逻辑
3. 新增异常任务聚合查询接口,支持按死信/连续失败/长期未执行分类返回
4. 实现任务批量暂停/恢复/软删除操作
5. 扩展运行日志与任务查询过滤条件,新增handler_name维度
6. 优化健康检查逻辑,支持动态健康窗口并返回配置快照
7. 完善数据映射与DTO定义,补充缺失字段与类型支持
2026-07-11 21:53:56 +08:00

236 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""领域模型(dataclass) ↔ Output DTO(Pydantic) 转换。
规范 §6.1/§7.3(对齐 ``external_systems/use_cases/mappers.py``
- persistence 返回领域模型 dataclassservice 返回 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 时返回秒级执行时长,否则返回 Nonerunning 状态)。
"""
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])