Repository navigation
fix(backend): 修复章节导出成品在 24 小时有效期内被清理删除的问题 - #385
Merged
syrizelink merged 2 commits intoOct 6, 2026
Merged
syrizelink merged 2 commits into
syrizelink merged 2 commits into
Conversation
cleanup_chapter_export_files 里的分支条件写反了顺序:先判断
`path.suffix in {".part", ".txt"}`,再在 elif 里判断
`path.suffix == ".txt" and job.status == JOB_STATUS_SUCCEEDED`。
_job_id_from_export_path 已经保证后缀只会是 .part 或 .txt,所以第一个
条件恒为真,带有效期判断的 elif 分支永远不会执行。任务成功后状态不再
属于 pending/running/cancel_requested,成品 .txt 于是在下一次清理时
被直接删除。
清理由后台 watchdog 每 background_running_stale_seconds/2(默认 300 秒)
触发一次,也在应用启动维护阶段跑一次,因此导出完成后最多五分钟,
EXPORT_FILE_TTL 声称的 24 小时下载窗口就失效:
is_export_download_available 因为 output_path.is_file() 为假而返回 False,
前端的 download_url 消失,直接请求下载接口返回 409。
改为先按任务状态判断是否仍在进行(进行中 .part 与 .txt 都保留,成品是
os.replace 落盘的,可能早于状态提交),再对成功任务的 .txt 走有效期判断。
成功任务残留的 .part 不再有读者,按原意删除。
新增两个用例覆盖成功任务:有效期内保留 .txt 并删除残留 .part,过期后
删除 .txt。
syrizelink
approved these changes
Oct 6, 2026
Owner
|
Thank you for your work. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR 标题
fix(backend): 修复章节导出成品在 24 小时有效期内被清理删除的问题
变更说明
backend/app/chapter_export/service.py的cleanup_chapter_export_files()里两个分支的判断顺序反了。第 415 行先判断if path.suffix in {".part", ".txt"},第 421 行才在elif里判断path.suffix == ".txt" and job.status == JOB_STATUS_SUCCEEDED。但循环体只有通过第 460 行_job_id_from_export_path()的文件才会走到这里,而它已经把后缀限定成.part或.txt了,所以第一个条件恒为真,带expires_at有效期判断的elif分支是永远进不去的死代码。任务成功后状态是succeeded,不在{pending, running, cancel_requested}里,should_keep于是为假,成品.txt被直接unlink。backend/app/background/runtime/watchdog.py第 48 行每max(background_running_stale_seconds / 2, 1.0)秒调用一次(默认 600 / 2 即 300 秒),main.py的启动维护阶段还会再跑一次。所以导出任务成功后最多五分钟,EXPORT_FILE_TTL承诺的 24 小时下载窗口就作废了:is_export_download_available()因为output_path.is_file()为假返回 False,状态接口不再带download_url,直接请求下载接口返回 409「导出文件不可用或已过期」。用户看到的现象是刚导完的文件放一会儿就下载不了,只能重新导出。pending / running / cancel_requested时.part与.txt都保留,这一条维持原语义不变:write_chapter_export()第 388 行用os.replace把.part换成.txt,成品落盘可能早于任务状态提交,已有用例test_cleanup_keeps_output_while_export_is_still_running防的就是这个竞态。其余情况才看后缀,成功任务的.txt走expires_at有效期判断,口径与is_export_download_available()一致;成功任务残留的.part已经没有读者,按原意删除。tests/api/test_chapter_exports.py新增两个用例,覆盖成功任务的两侧边界:有效期内保留.txt并删掉残留的.part;过期后删除.txt。关联 Issue / PR
无
变更类型
问题原因
if与elif的条件顺序按"先后缀、后状态"写,而后缀在进入循环体前已经被_job_id_from_export_path()过滤成只剩.part和.txt两种,第一个条件因此恒真,把第二个分支完全遮蔽。两个分支想表达的是两件不同的事:.part是仍在写入的临时文件,只在任务进行中有意义;.txt是成品,成功后要按expires_at留满 24 小时。按状态先分流之后,两件事各自成立。自查清单
补充信息
复现与验证都在本地跑过(macOS 15,Python 3.12,
uv sync --frozen)。复现用一次性用例串起完整链路:建项目和章节、创建导出任务、dispatch_job写完成品、mark_succeeded,然后调一次cleanup_chapter_export_files()再请求下载接口。在未修改的 main 上,清理前下载返回 200,清理删掉 1 个文件,清理后下载返回 409{"detail":"导出文件不可用或已过期"};带本 PR 的改动则是清理删除 0 个文件,清理后下载仍为 200。该一次性用例只用于复现,没有提交。CI 对应的命令结果:
uv run ruff check .全部通过,uv run ty check app全部通过,uv run pytest1717 passed(60.47s),uv build两个产物都构建成功。单独跑改动涉及的文件:uv run pytest tests/api/test_chapter_exports.py13 passed;新增的第一个用例在未修改的 main 上确实失败(assert 2 == 1,即成品和残留临时文件被一起删掉)。