Python工作流断点续跑:状态持久化与多步编排实践

多步脚本最怕跑到一半挂掉:前面已经生成了中间产物,却只能从头再来。本文讲一套可落地的做法——用 state.json 持久化进度,用步骤表编排执行,用 run_id / --from-step 在失败点接着跑。

多步工作流断点续跑封面

问题与目标

典型痛点

一条流水线常拆成:读入参数 → 生成产物 → 后处理 → 校验。任一步超时、人工中断或外部依赖失败,都会让进程退出。若内存里的进度不落盘,重启只能重跑整链,浪费算力和时间。

设计目标

  • 可续跑:同一 run_id 再次启动时,读已有状态,从指定步骤继续
  • 可单步:调试某一环节时只跑一步,不污染前后产物
  • 可核对:状态文件能直接打开看 finished_steps、关键路径、业务字段

状态怎么建模

一次运行一个目录

每次执行分配一个 run_id(常用时间戳 YYYYMMDD-HHMMSS),产物与状态都落在同一工作目录,例如 build/pipeline/<run_id>/。续跑时只传 run_id,就能定位目录并加载 state.json

最小字段集

dataclass 描述状态,序列化成 JSON。核心字段建议固定三类:

类别字段示例用途
身份run_id, work_dir定位本次运行
进度finished_steps已成功完成的步骤名列表
产物input_path, output_path, 业务字段后续步骤读上游结果

示意:

from dataclasses import asdict, dataclass, field, fields
import json
from pathlib import Path
from typing import Any

@dataclass
class WfState:
    run_id: str
    prompt: str = ""
    work_dir: str = ""
    output_path: str = ""
    finished_steps: list[str] = field(default_factory=list)

    def to_json(self) -> dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_json(cls, data: dict[str, Any]) -> "WfState":
        known = {f.name for f in fields(cls)}
        return cls(**{k: v for k, v in data.items() if k in known})

def save_state(state: WfState, work_dir: Path) -> None:
    work_dir.mkdir(parents=True, exist_ok=True)
    path = work_dir / "state.json"
    path.write_text(
        json.dumps(state.to_json(), ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )

from_jsonfields 过滤未知键,状态 schema 演进时旧文件仍能加载,少踩一次升级坑。

state与工作目录关系

何时写入磁盘

落盘时机比字段设计更关键。至少覆盖:

  1. 步骤成功:mark_done 追加步骤名后立刻 save_state
  2. 步骤失败 / 抛异常:仍保存当前已写入的产物路径,方便排查
  3. KeyboardInterrupt:用户 Ctrl+C 也要先写盘再退出,否则「差一点跑完」的进度会丢
def mark_done(state: WfState, step: str) -> None:
    if step not in state.finished_steps:
        state.finished_steps.append(step)

路径建议存相对仓库根的 POSIX 字符串(as_posix()),跨 Win / macOS 读回更稳。

编排器:选步、跳步、续跑

步骤表是唯一真相

把步骤名放进有序列表,别在业务里硬编码「下一步是谁」:

STEPS = (
    "step-01-input",
    "step-02-transform",
    "step-03-validate",
    "step-04-export",
)

def select_steps(step: str, from_step: str) -> list[str]:
    if step and from_step:
        raise ValueError("step 与 from_step 不能同时使用")
    if step:
        if step not in STEPS:
            raise ValueError(f"未知步骤: {step}")
        return [step]
    if from_step:
        if from_step not in STEPS:
            raise ValueError(f"未知步骤: {from_step}")
        return list(STEPS[STEPS.index(from_step) :])
    return list(STEPS)

三种用法对应三种场景:

  • 不传:全量新跑
  • --step step-02-transform:只重跑中间一步(调试)
  • --from-step step-03-validate --run-id 20260302-143000:从失败点续到结束

续跑决策流程

主循环骨架

def run_workflow(run_id: str, step: str = "", from_step: str = "") -> int:
    work = Path("build/pipeline") / run_id
    prev = load_state(work)  # 无文件则 None
    state = prev or WfState(run_id=run_id)
    state.work_dir = str(work)
    steps = select_steps(step, from_step)
    save_state(state, work)

    for name in steps:
        try:
            code = load_step(name)(state)  # 约定 run(state) -> int
        except KeyboardInterrupt:
            save_state(state, work)
            return 130
        except Exception:
            save_state(state, work)
            return 1
        if code != 0:
            save_state(state, work)
            return code
    return 0

每步单独文件、统一 run(state) -> int,编排器用动态加载即可。步骤内部负责:读上游字段 → 写产物 → mark_donesave_state。编排器只关心退出码与是否继续。

幂等:重跑同一步时怎么办

续跑常会再次进入已完成过的步骤名(例如 --from-step 从中间开始,但该步产物其实还在)。常见两种策略:

  • 跳过:若 step 已在 finished_steps 且产物文件存在,直接 return 0
  • 覆盖:调试场景强制重算,并覆盖对应字段

把策略写进步骤开头,比在编排器里塞特例更清晰。

落地时的取舍

JSON 够用,别一上来上数据库

单机脚本、产物本就在磁盘上时,state.json 足够:可读、可 diff、可手工改字段救急。只有多机并发抢同一任务时,才值得换成 DB 行锁或队列 ack。

运行时标志不要硬持久化

像「退出时是否关浏览器」「本次是否 dry-run」这类 本次会话 开关,可以进内存字段,但序列化时剔除(to_jsonpop),避免上次调试参数污染下次正式跑。

CLI 与函数拆开

业务入口做成 run_workflow(...),CLI 只做 argparse 再调用。长文本参数走函数,不要全塞进命令行——Windows 上尤其容易被截断。

中断也是正常路径

Ctrl+C 退出码用 130,日志打 [SKIP] 已中断 即可,不必当未捕获异常。重点是:中断前写盘、释放外部资源(子进程、文件句柄),别留半截占用。

落盘时机三节点

实战里,把「步骤表 + 状态对象 + 每步落盘」三件事钉死,多步流水线就能从「能跑」变成「能断、能接、能查」。接下来你要改的,往往只是给 WfState 多加几个业务字段,而不是重写编排器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值