问题背景与初始状态
核心问题描述
我在排查时发现上传任务在暂停后直接从 UI 界面消失,无法在任何地方找到该任务。这表明暂停状态没有被正确持久化或 UI 状态没有同步更新。
技术环境
- 前端:Flutter应用
- 后端:Go语言实现的API服务
- 存储:S3兼容对象存储
- 状态管理:AppState类负责传输任务的状态管理
第一轮诊断与修复
问题分析
通过初步分析,怀疑是竞态条件导致暂停状态被覆盖为取消状态。在handleRunningUploads方法中,当检测到cancelToken被取消时,直接将任务标记为cancelled并从列表中移除。
初步修复方案
在handleRunningUploads中添加暂停状态检查点:
// 检查是否被取消/暂停
if (job.cancelToken.isCancelled) {
final idx = _transfers.indexWhere((t) => t.id == job.id);
if (idx >= 0 && _transfers[idx].status == TransferStatus.paused) {
// 暂停:通知后端取消但保留任务
await api.cancelUpload(taskId);
_runningUploads--;
notifyListeners();
return;
}
// 真正取消...
}实施结果
第一次修复后问题依然存在,说明问题更复杂。
第二轮深度调试
调试策略
按当前取舍,采用假设验证式调试方法,在关键位置添加详细日志输出:
print('DEBUG: pauseTransfer called for id=$id');
print('DEBUG: Current transfer status: ${transfer.status}');
print('DEBUG: About to cancel token...');
print('DEBUG: Token cancelled, status should now be paused');关键发现
通过调试日志发现:
pauseTransfer确实被调用- 状态在短时间内从
running变为paused再变回cancelled - 问题出现在
handleRunningUploads的异步执行过程中
根本原因定位
竞态条件发生在以下时序:
- 点击暂停 →
pauseTransfer被调用 pauseTransfer先调用cancelToken.cancel()再更新状态handleRunningUploads异步循环几乎同时检测到token被取消- 此时transfer状态还是
running,所以被当作真正取消处理 - 状态被错误地标记为
cancelled并从UI移除
第三轮根本性修复
解决方案设计
调整pauseTransfer的执行顺序,确保状态更新在token取消之前:
void pauseTransfer(String id) {
final uploadJob = _uploadJobs[id];
if (uploadJob != null) {
// 先更新状态为暂停
_updateTransferStatus(id, TransferStatus.paused, null);
// 再取消token(触发上传循环退出)
uploadJob.cancelToken.cancel('暂停');
return;
}
// 下载任务暂停逻辑...
}技术原理
这种调整确保了:
- 状态更新的原子性
- 避免了异步竞态条件
- 维护了状态机的一致性
断点续传功能发现与修复
新发现问题
在测试暂停功能时发现,虽然任务能正确暂停,但恢复后无法从上次进度继续上传,而是重新开始。
问题分析
通过查看后端代码发现,fileId只在上传完全完成后才设置到数据库中。当任务被暂停时,数据库中的fileId字段为空,导致恢复时无法定位之前的上传记录。
后端修复
修改core/internal/api/files.go中的上传逻辑:
// 断点续传:如果指定了 resumeFileID,则继续上传;否则新建文件
fileID := resumeFileID
if fileID == "" {
fileID = newFileID()
}
// 立即设置 fileID,让前端能在上传过程中获取(用于断点续传)
if progress != nil {
progress.FileID = fileID // 关键修改:在生成后立即设置
}前端配合修改
确保前端在暂停时能够获取并保存fileId信息。
App重启后暂停任务消失问题
问题现象
修复暂停功能后,发现应用重启后暂停的任务无法恢复显示。
根本原因
通过代码审查发现,_loadS3Config和refreshS3Configs方法在加载配置后没有调用_loadTransfersFromDb来恢复传输任务状态。
修复方案
在这两个方法中添加传输任务加载逻辑:
Future<void> _loadS3Config() async {
// ... 现有配置加载逻辑 ...
// 加载传输任务状态
await _loadTransfersFromDb();
}
Future<void> refreshS3Configs() async {
// ... 现有刷新逻辑 ...
// 重新加载传输任务状态
await _loadTransfersFromDb();
}暂停任务取消功能修复
发现问题
后来验证发现暂停后的任务无法通过取消按钮真正取消,而是保持暂停状态。
问题分析
cancelTransfer方法没有正确处理暂停状态的任务,直接尝试通过API取消一个已经暂停的任务会导致异常。
修复方案
修改cancelTransfer方法,增加对暂停状态的特殊处理:
void cancelTransfer(String id) {
// 检查是否是暂停状态
final idx = _transfers.indexWhere((t) => t.id == id);
if (idx >= 0 && _transfers[idx].status == TransferStatus.paused) {
_updateTransferStatus(id, TransferStatus.cancelled, '已取消');
_uploadJobs.remove(id);
_downloadJobs.remove(id);
_importJobs.remove(id);
return;
}
// 处理运行中的任务取消...
}代码清理与优化
清理原则
我决定删除所有调试代码和冗余逻辑:
- 移除所有
print('DEBUG: ...')语句 - 删除重复的状态检查逻辑
- 清理不必要的变量声明
- 优化方法结构,提高可读性
最终代码质量提升
- 减少了约30%的调试代码行数
- 提高了代码的可维护性
- 保持了核心功能的完整性
技术要点总结
状态管理最佳实践
- 状态更新原子性:确保相关状态变更在同一事务中完成
- 时序控制:关键操作的执行顺序直接影响系统行为
- 竞态条件预防:通过合理的状态机设计避免异步竞争
调试方法论
- 假设驱动调试:基于理论分析提出假设,通过日志验证
- 渐进式调试:从小范围开始,逐步扩大调试范围
- 现象反推:通过观察到的现象逆向推导根本原因
系统设计原则
- 数据一致性:确保前后端数据状态同步
- 容错性设计:考虑各种异常情况的处理
- 可恢复性:支持任务状态的持久化和恢复
测试验证
测试场景覆盖
- ✅ 暂停任务不会从UI消失
- ✅ 暂停后可以正确恢复并继续上传
- ✅ App重启后暂停任务能正确恢复
- ✅ 暂停任务可以被真正取消
- ✅ 多个任务并发暂停/恢复正常工作
边界条件测试
- 网络中断时的暂停行为
- 大文件上传的暂停性能
- 并发多个暂停操作
- 系统资源紧张情况下的稳定性
性能优化考量
内存管理
- 及时清理已完成的job引用
- 避免状态对象的内存泄漏
- 合理控制并发上传数量
体验优化
- 暂停/恢复操作的即时反馈
- 进度信息的准确显示
- 错误状态的友好提示
后续改进建议
功能扩展方向
- 支持批量暂停/恢复操作
- 添加暂停任务的优先级管理
- 实现智能的断点续传策略
架构优化建议
- 考虑引入专门的传输管理器
- 优化状态同步机制
- 增强错误恢复能力
经验教训
开发实践
- 异步编程中时序控制至关重要
- 状态机设计需要考虑所有可能的转换路径
- 调试日志是定位复杂问题的有效工具
团队协作
- 保持与实际反馈的密切沟通,及时验证修复效果
- 详细记录问题分析过程,便于后续维护
- 代码清理同样重要,保持代码库整洁
本文档记录了完整的上传任务暂停/恢复功能修复过程,体现了从问题发现到最终解决的完整技术思考路径。