ForcePilot/backend/package/yuxi/scheduler/use_cases/mappers.py
Kris f04418fb0c feat(scheduler): 新增多项功能与优化,完善幂等、审计与执行逻辑
1. 新增TaskResult.skipped字段,标记分布式锁抢占失败跳过场景
2. 新增时区校验validate_tz,owner_scope校验新增枚举校验
3. 完善仓储接口与实现,新增finalize_one_shot_task方法
4. 为各API新增idempotency_key幂等支持
5. 新增任务执行超时检测与处理逻辑
6. 为运行日志添加duration_seconds派生字段
7. 重构僵尸回收逻辑,补录超时统计
8. 完善审计字段传递与一次性任务处理逻辑
2026-07-11 05:40:16 +08:00

211 lines
7.4 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,
ListDailyStatsOutput,
ListHandlerSummaryOutput,
ListUpcomingOutput,
RunLogOutput,
TaskOutput,
)
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_daily_stats_output",
"to_run_log_output",
"to_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_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,
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。
``description`` 字段仓储层不聚合DB 无此列),由调用方按需补充;
``active_task_count`` 同理,默认 0由调用方按需补充。
"""
return HandlerSummaryOutput(
handler_name=summary.name,
task_count=summary.task_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,
) -> GetHealthOutput:
"""构造 ``GetHealthOutput`` DTO。
``healthy`` 由调用方按"最近 tick 是否在 5 分钟内"判断后传入。
"""
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,
)
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``)。
"""
total_count = stat.success_count + stat.failure_count + stat.timeout_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_handler_summary_output(
summaries: list[HandlerSummary],
) -> ListHandlerSummaryOutput:
"""``HandlerSummary`` 列表 → ``ListHandlerSummaryOutput`` DTO。"""
return ListHandlerSummaryOutput(items=[to_handler_summary_output(s) for s in summaries])