Skip to content

fix(offline-download): preserve canceled task state - #3007

Open
laorentou666 wants to merge 4 commits into
OpenListTeam:mainfrom
laorentou666:fix/offline-download-cancel-state
Open

fix(offline-download): preserve canceled task state#3007
laorentou666 wants to merge 4 commits into
OpenListTeam:mainfrom
laorentou666:fix/offline-download-cancel-state

Conversation

@laorentou666

Copy link
Copy Markdown

Summary / 摘要

修复离线下载任务取消时的状态和清理问题:

  • 在下载完成状态更新后重新检查取消上下文,避免任务被误标记为成功。
  • 统一取消任务的状态更新、Provider 清理和 context.Canceled 返回逻辑。
  • Transmission 删除任务时使用不受取消影响的上下文,确保远程任务能够正常清理。
  • 增加取消与完成状态竞态的回归测试。
  • This PR has breaking changes.
    / 此 PR 包含破坏性变更。
  • This PR changes public API, config, storage format, or migration behavior.
    / 此 PR 修改了公开 API、配置、存储格式或迁移行为。
  • This PR requires corresponding changes in related repositories.
    / 此 PR 需要关联仓库同步修改。

Related repository PRs / 关联仓库 PR:

  • OpenList-Frontend:
  • OpenList-Docs:

Testing / 测试

  • go test ./...
  • Manual test / 手动测试:
  • go test -race ./internal/offline_download/tool
  • go test -vet=off ./internal/offline_download/...

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmtgo fmtprettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明):

Usage scope / 使用范围:

  • Code generation / 代码生成

  • Refactoring / 重构

  • Documentation / 文档

  • Tests / 测试

  • Translation / 翻译

  • Review assistance / 审查辅助

  • I have reviewed and validated all AI-assisted content included in this PR.
    / 我已审核并验证此 PR 中的所有 AI 辅助内容。

  • I have ensured that all AI-assisted commits include Co-Authored-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

laorentou666 and others added 2 commits August 31, 2026 13:49
- Return context.Canceled after successful provider cleanup so tache records cancellation instead of success.
- Update the task detail status and add regression coverage for canceled cleanup.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
- Prioritize canceled contexts after terminal status updates.
- Run Transmission cleanup with a non-cancelable context.
- Add regression coverage for completion and cancellation races.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
jyxjjj
jyxjjj previously requested changes Aug 31, 2026

@jyxjjj jyxjjj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update() performs the transfer before Run() checks whether the task has been canceled.

When Status() reports Completed: true, Update() immediately calls t.Transfer(). If cancellation happens between the status update and this point, the transfer side effect can still run, and Run() only notices the canceled context afterward.

The new regression test uses Thunder, where Transfer() returns without performing a transfer when TempDir == DstDirPath, so it does not cover this race.

Consider checking t.Ctx().Err() inside the info.Completed branch before calling Transfer(), and add a test that exercises an actual transfer path.

@laorentou666

Copy link
Copy Markdown
Author

Update() performs the transfer before Run() checks whether the task has been canceled.

When Status() reports Completed: true, Update() immediately calls t.Transfer(). If cancellation happens between the status update and this point, the transfer side effect can still run, and Run() only notices the canceled context afterward.

The new regression test uses Thunder, where Transfer() returns without performing a transfer when TempDir == DstDirPath, so it does not cover this race.

Consider checking t.Ctx().Err() inside the info.Completed branch before calling Transfer(), and add a test that exercises an actual transfer path.

已追加commit

@jyxjjj

jyxjjj commented Aug 31, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T09:51:40.998202Z 2ff94e6 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ff94e677c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return err
}
err = t.client.TorrentRemove(task.Ctx(), transmissionrpc.TorrentRemovePayload{
err = t.client.TorrentRemove(context.WithoutCancel(task.Ctx()), transmissionrpc.TorrentRemovePayload{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound the uncancelable Transmission cleanup

When the Transmission RPC endpoint accepts a request but stops responding, this context can never terminate: context.WithoutCancel removes both cancellation and any deadline from the task context, while Init constructs the RPC client with the default client configuration. Consequently, canceling the download can leave it permanently in the canceling state and occupy an offline-download worker indefinitely; use a detached context with a finite cleanup timeout instead.

Useful? React with 👍 / 👎.

Comment on lines +70 to +71
if err := t.tool.Remove(t); err != nil {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve cancellation when provider cleanup fails

When Remove returns an error—deterministically for 123 Open, whose implementation returns errs.NotSupport, and transiently for other providers—this helper returns that ordinary error after setting the status to canceled. The task manager can therefore classify or retry the task as failed rather than canceled, leaving its state inconsistent with the new status and defeating the state-preservation change; retain the cleanup error while ensuring the returned error still wraps context.Canceled.

Useful? React with 👍 / 👎.

laorentou666 and others added 2 commits August 31, 2026 21:06
- Check the task context before transferring completed downloads.
- Cover cancellation between status reporting and transfer dispatch.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
- Interrupt completion waits and preserve cancellation across cleanup and AddURL failures.
- Bound Transmission cleanup and guard direct transfer dispatch.
- Add cancellation-state and cleanup-timeout regression coverage.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
@laorentou666
laorentou666 force-pushed the fix/offline-download-cancel-state branch from 2ff94e6 to f172010 Compare August 31, 2026 13:19
@laorentou666

Copy link
Copy Markdown
Author

已修复有关问题

pikachuren

This comment was marked as off-topic.

@pikachuren
pikachuren dismissed their stale review September 1, 2026 09:20

Dismissed: superseded by a re-issued review in the standard format.

@pikachuren pikachuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 感谢 @laorentou666 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Opus 5 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出是否合并的决策。

🎯 结论

🔄 Request Changes — 修的是真 bug 且方向正确,但改动范围超出标题、清理逻辑只覆盖了一个 provider,建议调整后再合并

📖 概要

fix(offline-download): preserve canceled task state · 修复离线下载任务被取消后仍被记为「成功」的问题。
核心改动:取消路径改为返回非 nil 错误以命中 tache 的取消终态判定,并为 Transmission 的清理请求 detach 掉已取消的 ctx;同时顺带重构了 provider 分支逻辑。

🧭 整体方案

技术路线是「让 Run() 在取消时返回错误」:tache 的 Worker.Execute 只有在 task.Run() 返回非 nil error 时才会进入 onError,而 onErrorisCanceled(task.Ctx()) 判定终态、needRetry 也只对 StateErrored/StateFailed 重试。旧代码收到 CtxDoneRemove 成功就 return nil,于是被记成 StateSucceeded——这个根因分析是准确的,方案方向正确。附带一点:既然终态由 ctx 决定,返回任意非 nil 错误即可,不一定要构造 context.CanceledcancellationError 或许可以更简单些~

📊 变更统计

4 个文件(+484 / -50 行) | 功能 ⭐⭐⭐⭐ | 最小改动 ⭐⭐ | 前向兼容 ⭐⭐⭐ | 方案设计 ⭐⭐⭐

🚨 关键问题

P0(阻塞合并)

  • ⚠️ internal/offline_download/tool/download.go — 取消时的 Remove 清理只对 Transmission 做了 ctx detach,其它 provider(aria2 / qBittorrent / 115 等)在取消路径上仍使用已取消的 ctx 调用 Remove,HTTP 请求会立刻返回 context canceled,远端任务留成孤儿。请问是否考虑在 DownloadTask.cancelDownload 这一层统一处理(调用 Remove 前统一换成 context.WithoutCancel + 超时),而不是逐个 driver 打补丁呢?目前只改 Transmission 容易让人误以为问题已整体解决~

P1(建议修复)

  • ⚠️ 改动范围超出标题,是否考虑拆成两个 PR?第 4 个 commit 把一长串 if t.tool.Name() == ... 重写成了 switch,并把 qBittorrent/Transmission 的做种等待收拢进 waitAndRemove。重构本身质量不错(逐个分支核对过,行为等价,115 Open/123 Open 归组也没问题),但与取消语义修复混在一起会放大 review 成本和回归风险。建议重构单独成 PR,本 PR 保持最小 diff。
  • ⚠️ 集成测试 TestDownloadTaskManagerKeepsCanceledStateWhenCleanupFails 存在 flake 风险:它拉起真实 tache manager,靠两个 1 秒超时的 channel 同步,CI 高负载时容易假失败。更需要注意的是 observedDownloadTask.SetState 中的 close(t.canceled)——onError 会先后两次调用 SetState(StateCanceled)(一次在 isCanceled 分支,一次在 needRetry 为 false 之后),目前靠 failedHookCalled 标志位恰好只关闭一次,这个不变量依赖 tache 内部调用顺序,上游一改就会 panic。是否可以用 sync.Once 保护 close,或者把它降级为直接断言 Run() 返回值的单元测试呢?

P2(可选)

  • 💡 cancelDownloadt.Status 被赋值了两次:先设为 "offline download canceled",紧接着 cancellationError 又设一遍。删掉前一行即可~
  • 💡 行为变更建议写进 PR 描述:旧代码在 qBittorrent/Transmission 做种等待期间被取消最终是 return nil(成功),新代码走 cancelDownload 后会变成 canceled。我认为这个改变是对的,但它是用户可见的状态变化,值得在描述里点明。
  • 💡 stderrors.Join(ctxErr, extra) 的错误消息是换行拼接的,展示到任务列表会是多行。功能上没问题,想确认一下 UI 侧若只渲染单行,用户看到的会不会是被截断的信息?
  • 💡 transmissionCleanupTimeout = 15 * time.Second 是硬编码常量,而项目里其它超时(做种时长等)都走了 setting。这里是否也考虑做成可配置?这条更适合听维护者的意见。

📂 逐文件分析

internal/offline_download/tool/download.go

改动意图:让取消路径返回错误以保留 canceled 状态,并重构 provider 分支。
代码逻辑cancelDownload 设置状态并构造 cancellationError;原先散落的 if t.tool.Name() == ... 判断被收敛为 switch,做种等待与清理合并进 waitAndRemove
问题分析:取消语义的修复正确;但 ① 清理阶段未统一 detach ctx(见 P0);② t.Status 重复赋值(见 P2);③ 重构与修复耦合在同一 PR(见 P1)。
详细建议:建议在调用各 provider 的 Remove 前统一处理 ctx,这样一次覆盖所有 provider:

// 统一在 cancelDownload/waitAndRemove 中 detach,替代逐 driver 修改
cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), cleanupTimeout)
defer cancel()
if err := t.tool.Remove(cleanupCtx, t); err != nil {
    // 清理失败不改变 canceled 终态,仅附加到错误信息
}

drivers/.../transmission/client.go

改动意图:为 Transmission 的清理请求 detach 已取消的 ctx。
问题分析:本身写法正确,但作为单点修复不完整——如果按上面的建议在 download.go 层统一处理,这里的改动或许就可以回退了。
详细建议:确认统一方案后,此文件建议保持原样以缩小 diff。

测试文件

问题分析:见 P1 第二条,主要是同步方式与 close(t.canceled) 的一次性保证。
详细建议sync.Once 保护 close,或改为直接断言 Run() 返回 error 且 ctx 已取消的纯单元测试,避免依赖 tache 内部调用次数。

其余文件无重大问题。

✅ 待处理清单

  • [P0] 在 cancelDownload 层统一为 Remove 提供 detach 后的 ctx,覆盖 aria2 / qBittorrent / 115 等全部 provider
  • [P1] 将 switch 重构与 waitAndRemove 收敛拆分为独立 PR,本 PR 保持最小 diff
  • [P1] 用 sync.Once 保护 close(t.canceled),或将集成测试降级为单元测试
  • [P2] 删除 cancelDownload 中重复的 t.Status 赋值
  • [P2] 在 PR 描述中说明做种等待期取消的状态变化(success → canceled)
  • [P2] 确认多行错误消息在任务列表 UI 中的展示效果
  • [P2] 与维护者确认 transmissionCleanupTimeout 是否需要可配置

🎯 结论:🔄 Request Changes — 根因与方向都对,但取消清理需覆盖全部 provider,且建议把重构拆出去以缩小回归面。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants