ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

批量PDF翻译任务队列设计:断点续传与失败重试的实测方案

批量PDF翻译任务队列设计:断点续传与失败重试的实测方案 前言批量翻译几十上百份 PDF 时最怕两种场面一是跑了两小时程序崩了或被杀重启后所有文件从头再翻一遍二是队列里混进一两个损坏文件重试逻辑写得不对程序在它身上死循环后面的好文件全部卡死。这两个问题的解法是同一套东西带状态文件的任务队列 区分失败类型的重试策略。本文给出一套可复现的最小实现并在 30 份测试文档上实测断点续传、重试吸收率和状态文件损坏恢复。环境准备Python 3.13.12Windows 11依赖pip install reportlab生成测试 PDF生产环境中换成你的翻译调用即可无需数据库状态持久化用一个 JSONL 文件整体设计三个核心决策每一个都对应一个踩过的坑状态文件选 JSONL每行一个任务而不是 JSON。每处理完一个任务就追加落盘进程被杀最多丢最后一行JSON 需要整体重写大队列下既慢又容易写坏。也没有一上来就上 SQLite——几百到几千个任务的量级JSONL 肉眼可读、手工可修排查问题时可以直接打开看这个优势在调试期非常值钱。落盘必须原子先写临时文件再os.replace()覆盖。直接覆盖写原文件时进程被杀会留下半行——这正是后面实测的场景。os.replace在同一文件系统内是原子操作Windows 和 Linux 行为一致不需要额外的锁。失败分两类对待网络抖动类失败连接超时、限流用指数退避重试同一次运行内解决结构性损坏类失败文件缺%%EOF、解析崩溃、解密失败重试无意义——文件不会因为多试几次就变好——重试次数用尽后标failed留给下次运行或人工处理。示例代码为了简洁统一捕获Exception生产环境建议按异常类型分流把值得重试和不值得重试明确分开。实现步骤Step 1: 状态文件读写defload_state():tasks{}ifnotos.path.exists(STATE):returntaskswithopen(STATE,r,encodingutf-8)asf:forlineno,lineinenumerate(f,1):try:tjson.loads(line)tasks[t[file]]t# 后行覆盖前行 最新状态exceptjson.JSONDecodeError:print(f第{lineno}行损坏(半行写入), 跳过)# 容错关键returntasksdefsave_state(tasks):tmpSTATE.tmpwithopen(tmp,w,encodingutf-8)asf:fortintasks.values():f.write(json.dumps(t,ensure_asciiFalse)\n)os.replace(tmp,STATE)# 原子替换Step 2: worker 与指数退避defrun_queue(label):tasksload_state()forpathinall_pdfs:prevtasks.get(path,{status:pending})ifprev[status]done:# 断点续传核心: 已完成直接跳过continueforretryinrange(MAX_RETRY1):# 最多 3 次try:translate_one(path);okTrue;breakexceptExceptionase:time.sleep(0.02*(2**retry))# 指数退避tasks[path]{status:doneifokelsefailed,...}save_state(tasks)Step 3: 一个判断 PDF 损坏的实用细节模拟毒丸文件时发现reportlab 生成的 PDF 末尾是startxref %%EOF文件末尾 100 字节里未必包含trailer关键字。判断 PDF 文件是否完整稳妥的信号是末尾 64 字节内有没有%%EOF而不是检查 trailer。这个细节在真实流水线里同样重要批量任务里混入损坏文件的第一道防线就是在入队前做一次轻量体检读末尾字节把大多数毒丸挡在队列外面而不是等翻译引擎抛异常才发现——入队前体检一次的开销远小于 worker 重试三次的开销。实测结果测试集30 份生成的 PDF其中 3 份故意截断为毒丸。模拟翻译失败率可用环境变量调节。场景一常态运行低失败率指标Run1 全量Run2 断点续传需处理303仅失败任务跳过已完成027成功 / 失败27 / 30 / 3耗时1.49s0.43s3 份毒丸在 Run1 重试 3 次后标记failedRun2 不会再碰它们若没有状态文件做全量重跑27 份已成功文档要白白重复处理。场景二高失败率60% 抖动验证续传补齐Run1 只成功 19 份、失败 11 份3 毒丸 8 抖动耗时 3.40sRun2 断点续传只处理这 11 份8 份抖动任务全部补齐成功最终 27 done / 3 failed耗时 0.95s。对照组无断点续传则要重复处理 19 份已完成任务。场景三run 内重试的吸收率把失败率压到 15% 时3 次指数退避重试把抖动完全吸收——Run1 最终failed仍然只有 3 份毒丸零遗留。也就是说瞬时失败靠 run 内重试兜底永久失败靠跨 run 续传和人工介入两层机制各管一段。场景四状态文件半行截断进程被杀模拟把 tasks.jsonl 最后一行拦腰截断后重载解析器跳过损坏行恢复 29/30 个任务状态丢失的那条恰好是已完成记录重跑时自动重新处理并成功——最终状态与截断前完全一致27 done / 3 failed自愈。总结机制解决的问题实测效果JSONL 状态 done 跳过断点续传重跑只处理失败任务已完成 27 份零重复指数退避重试 ×3网络抖动15% 失败率被完全吸收failed 终态毒丸卡死3 份损坏文件永久隔离不阻塞队列原子落盘 损坏行跳过进程被杀半行截断后 29/30 恢复并自愈这套最小队列没有引入任何外部依赖直接可用于批量翻译、批量转换等场景。规模上去之后万级任务、多进程并发再考虑 SQLite 或消息队列但断点续传和失败分类这两个设计决策不会变。补两个扩展时的注意点。多进程写状态文件要加锁JSONL 的追加写在 Windows 上多个进程同时 open-append 不是原子的最简单的办法是每个 worker 写自己的分片状态文件汇总时合并或者改用 SQLite它自带并发控制此时才是引入它的合理时机。状态记录里带上输入文件的修改时间或哈希如果队列任务中途文件被替换仅凭路径判断已完成会翻旧账——把内容指纹写进状态键里文件变了自然当成新任务这个改动只需五行代码。最后是一点工程判断断点续传的本质不是保存进度而是让每一次运行的输入输出都可对账——状态文件就是账本账本可读、可修、可自愈批量流水线的可靠性就立住了。标签Python、PDF翻译、批量处理、任务队列、断点续传
返回列表