fix: 修复大画廊下载时进程静默闪退(fatal error: concurrent map write)
现象:点「开始下载」后,单个画廊图片较多时整个程序无报错直接退出。
只在图片多的画廊上偶发。
根因:pageState.snapshotLocked 用浅拷贝把 Task 交给 UI:
t := *s.task // Pages map 仍是同一块内存
快照被 EventsEmit 做 JSON 序列化时,其他页 worker 正在写同一个 map,
撞上就抛 Go runtime 的致命错误:
fatal error: concurrent map iteration and map write
这是 runtime 级 fatal error,不可 recover,直接杀进程 —— 所以没有任何
报错提示。图片越多、页并发越高,并发写窗口越长,越容易触发。
同一问题还有第二处:Store.UpsertTask 存的是原值(含活的 map),
而 Flush() 会在后台 json.Marshal 整个 state,同样会撞上。
修复:
- snapshotLocked 深拷贝 Pages map 与 FailedList slice
- UpsertTask 存前先 clone(),让 store 完全拥有自己的数据
- 另加一个兜底 recover,防止将来再有 panic 直接杀掉进程
测试:
- TestSnapshotDoesNotSharePagesMap:验证快照隔离(修复前失败)
- TestSnapshotRaceWindowClosed:持续写原 task + 持续序列化快照,
精准复现崩溃窗口 —— 实测修复前 fatal error 崩进程,修复后通过
- 大画廊压测:200 页 × 页并发 20,以及 5 画廊 × 60 页 × 并发 16
测试从 62 增至 67。
This commit is contained in:
@@ -491,9 +491,27 @@ func (s *pageState) syncCountsLocked() {
|
||||
s.task.FailedList = append([]FailedItem(nil), s.failedL...)
|
||||
}
|
||||
|
||||
// snapshotLocked 生成任务快照。
|
||||
//
|
||||
// ⚠️ 必须深拷贝 Pages / FailedList —— 这两个是 map 和 slice,
|
||||
// 浅拷贝会把底层内存一起交出去。而快照是要被送到 UI 做 JSON 序列化的,
|
||||
// 其他页 worker 此刻还在写同一份数据 —— 共享内存会导致
|
||||
// fatal error: concurrent map read and map write(不可 recover,整个进程闪退)。
|
||||
// 症状:图片多的画廊 + 高页并发时偶发,因为并发写窗口更长。
|
||||
func (s *pageState) snapshotLocked() Task {
|
||||
s.syncCountsLocked()
|
||||
t := *s.task
|
||||
|
||||
// 深拷贝 map
|
||||
pages := make(map[string]string, len(s.task.Pages))
|
||||
for k, v := range s.task.Pages {
|
||||
pages[k] = v
|
||||
}
|
||||
t.Pages = pages
|
||||
|
||||
// 深拷贝 slice
|
||||
t.FailedList = append([]FailedItem(nil), s.task.FailedList...)
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user