长流程为什么半路断
整段自动化跑起来不难,难在跑得久。举个典型场景:让一个常驻程序去邮箱里翻电子票据附件,提取关键字段登记进协作套件的表格,再拉一遍支付应用的流水核对有没有漏项。三个应用轮着碰,任何一步都可能因为网络抖动、限流、授权过期而中断。中断本身不可怕,可怕的是进程一退,内存里的进度全部清零,恢复时不知道断在哪一段,只好整轮重跑。
游戏行业几十年前就给出过解法:存档点。打不过 Boss 先存档,读档接着来,不必把前面打过的关卡再走一遍。长任务的工程实现是同一套思路:把流程切成小段,每段收尾时把进度落到磁盘上的存档点;进程重启后先读档,从断掉的那段继续。本文就围绕这个思路,把连接层的三个模块------授权、定时任务、跨应用数据流------逐个拆开。

授权:每个连接都有寿命
常驻程序碰任何外部应用,前置动作都是授权。OAuth 换来的 access_token 通常几小时失效,refresh_token 换新也可能因为密码改动、安全策略收紧被撤销。麻烦在于失效是静默的:请求照样发出去,返回码变成 401,程序不会崩溃退出,只在日志深处躺一行。第二天回头查看,任务其实已经停了一整夜。
把 token 的状态管理单独封装成一个模块,比散落在各处好维护:
- 每次调用邮箱、表格、流水这些具体应用前,先验一次 token 的有效性;
- 快过期就提前刷新,刷新动作加分布式锁,避免多个任务并发刷新导致 token 轮换错乱;
- 刷新失败立即触发告警,别等业务请求撞上 401 再补救;
- token 的剩余寿命写进存档点,重启读档时顺手检查,先续命再干活。
如果走 MCP 这类通用协议接入,凭证管理同样按上面四条封装,协议替的是连接方式,寿命管理还得自己做。跨应用的长任务没有现成上下文,谁处理到哪一步、用了哪个凭证,全靠自己落档。存档点在这层扮演的角色,相当于连接层的记忆:授权记到什么程度、任务跑到哪一段,读一次档就全回来了。
授权的寿命,短于任务的寿命。
定时任务:骨架里必须有重试与告警
定时核查用 APScheduler 起步即可,它比裸 cron 多了任务状态、错过触发补偿(misfire_grace_time)和时区处理。裸 cron 只知道到点拉起进程,进程崩了它不负责;APScheduler 能在进程内持有任务上下文,配合持久化 jobstore,触发记录本身也能落盘。定时策略按业务定:票据这类低频到达的数据,30 分钟一轮足够;轮询间隔压得过短,收益没多少,先撞上对方的限流阈值。定错时区是另一类静默故障,任务在凌晨三点空跑,白天该跑的点反而没人触发,部署时把调度器时区显式写死。
定时核查骨架:APScheduler 版,票据整理的巡检任务
存档点读写走 your-domain.test 的样本页,替换成自己的地址即可
import time
import requests
from apscheduler.schedulers.blocking import BlockingScheduler
CHECKPOINT = "https://your-domain.test/api/checkpoint" # 存档点
ALERT = "https://your-domain.test/api/alert" # 告警通道
def read_checkpoint():
"""读上次存档点,确认本轮该跑哪一批,这是断点续跑的入口"""
return requests.get(CHECKPOINT, timeout=10).json()"batch"
def write_checkpoint(batch):
"""每段流程收尾立刻写档,进程挂掉也只重跑当前段"""
requests.post(CHECKPOINT, json={"batch": batch}, timeout=10)
def scan(batch):
"""拉取一批新到的票据附件,返回票据号列表"""
url = f"https://mail.example.com/api/attachments?batch={batch}"
return requests.get(url, timeout=15).json()"invoice_ids"
def alert(msg):
requests.post(ALERT, json={"text": msg}, timeout=10)
def job():
batch = read_checkpoint()
ids = None
指数退避重试:停 1、5、15 分钟再试;三轮仍失败就告警并保留存档点原样
for wait in (60, 300, 900):
try:
ids = scan(batch)
write_checkpoint(batch) # 拉取成功先记档,再慢慢处理
break
except Exception as err:
time.sleep(wait)
if ids is None:
alert(f"批次 {batch} 连续三轮拉取失败")
return
process(ids) # 逐条解析、登记、对账,真实项目里调表格写入接口
write_checkpoint(batch + 1) # 全段收尾,推进存档点
scheduler = BlockingScheduler()
scheduler.add_job(job, "interval", minutes=30)
scheduler.start()
骨架里值得留意的是 write_checkpoint 的位置:拉取成功先记档,本段处理完再推进批次号。中间任何位置挂掉,下一轮读档都能接上,损失只有当前段。
跨应用数据流:票据整理当背景
用票据整理走一遍完整的数据流。需求描述如下:邮箱里陆续收到电子票据附件;程序提取票据号与金额,登记进协作套件的表格;流水导出走支付应用的开放接口,核对哪些消费漏了票据。
字段建议这样定:
- 票据号:主键,正则形如 INV-YYYY-NNNN,全半角和前后空格先清洗再比对;
- 金额:单位统一成「分」,整数入库,规避浮点误差;
- 开票日期:统一时区,避免跨日票据算错归属;
- 流水命中状态:对上、金额不符、查无流水,三态分列,别混在一个布尔值里。
流程切三段,段与段之间落存档点:
- 翻邮箱拉附件,按批次落盘,每批 50 条上下,批太大单次失败重跑代价高;
- 解析字段并登记表格,登记动作必须幂等;
- 拉流水跑对账判定,差异项写回表格的备注列。
对账判定函数如下,票据号对账加双向漏项检测:
跨应用对账判定:票据表 vs 支付流水,票据号对账加漏项检测
import re
def reconcile(invoices, payments):
"""
invoices: 协作套件表格里登记的票据
{"no": "INV-2026-0311", "amount": 12900, "date": "2026-03-11"} # 金额单位:分
payments: 支付应用导出的流水
{"memo": " Lunch INV-2026-0311 ", "amount": 12900}
"""
先给流水建票据号索引:memo 里杂讯多,用正则抠票据号
pay_index = {}
for p in payments:
m = re.search(r"INV-\d{4}-\d{4}", p.get("memo", ""))
if m:
pay_indexm.group() = int(p"amount")
known = {i"no" for i in invoices}
matched, broken = \[\], \[\]
for inv in invoices:
if inv"no" in pay_index:
if pay_indexinv\["no"] == inv"amount":
matched.append(inv"no") # 票据号与金额都对上
else:
broken.append((inv"no", "金额不符"))
else:
broken.append((inv"no", "查无流水"))
反向再扫一遍:有流水却没登记票据,属于漏登记项
unregistered = sorted(pay_index.keys() - known)
return {"matched": matched, "broken": broken, "unregistered": unregistered}
漏项检测要双向扫:登记了票据却查无流水,可能是无效票据或手误;有流水却没登记票据,属于漏登。只扫单向,对账就只做了一半。差异项回写表格时也走幂等写:按票据号定位行,备注列覆盖写,追加写遇中断会产生半截内容。
踩坑实录:三处翻车现场
轮询频率过高被限流。 头一版每分钟扫一次邮箱,跑到 20 来轮,邮箱一方直接返回 429,后续请求整批被挡。压低到 30 分钟一轮再加退避重试,整体耗时反而更短。轮询频率与对方的限流阈值之间要留余量,宁可慢一轮,不留硬伤。
无幂等导致重复登记。 登记表格前没查票据号是否已存在,任务中断续跑后,同一批票据登记了两遍,表格里出现成对的重复行,下游对账全部失真。修法很朴素:登记前按主键查一次,存在即跳过;表格侧再按票据号加约束,兜底防并发双写。幂等不做好,断点续跑反而成了事故放大器------续跑一次,重复一批。
授权过期静默失败。 前文提过 401 的问题,这里补一个教训:告警别只挂在抛异常上。返回码正常但业务语义失败的响应,要按业务码逐个判定后主动告警。另配一条巡检任务专门核对各连接的凭证状态,授权一过期就能被看见,不至于任务停了一夜无人知晓。
收尾
存档点思路可以压缩成一句话:授权记寿命,定时带重试,对账留差异,每段进度都落档。上下文自己建,记忆自己落,进程随时可以死,任务不能跟着死。这三块各管一摊,拼起来,成了一条能在半路摔倒又爬起来的自动化链路------判断标准也简单:随手 kill 掉进程,重启后看它接不接得上。接得上,这条链路过关;接不上,说明还有哪一档进度漏落盘了。
任务能断点续跑,长流程才敢放手。