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:
+20
-19
@@ -350,40 +350,41 @@ func truncate(s string, n int) string {
|
||||
return string(r[:n])
|
||||
}
|
||||
|
||||
// UpsertTask 更新/插入任务
|
||||
// UpsertTask 更新/插入任务。
|
||||
//
|
||||
// ⚠️ 存进去前必须 clone() —— 否则外部(页级 worker)继续改同一个 map,
|
||||
// 而 store 的 Flush() 会在后台 json.Marshal 整个 state —— 撞上就是
|
||||
// fatal error: concurrent map write(不可 recover,进程闪退)。
|
||||
func (s *Store) UpsertTask(t Task) Task {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
t.UpdatedAt = time.Now().UnixMilli()
|
||||
for i := range s.state.Tasks {
|
||||
if s.state.Tasks[i].GID == t.GID && s.state.Tasks[i].Token == t.Token {
|
||||
if t.Pages == nil {
|
||||
t.Pages = map[string]string{}
|
||||
}
|
||||
if t.FailedList == nil {
|
||||
t.FailedList = []FailedItem{}
|
||||
}
|
||||
if t.CreatedAt == 0 {
|
||||
t.CreatedAt = s.state.Tasks[i].CreatedAt
|
||||
}
|
||||
s.state.Tasks[i] = t
|
||||
s.startFlushLocked()
|
||||
return t.clone()
|
||||
}
|
||||
}
|
||||
if t.Pages == nil {
|
||||
t.Pages = map[string]string{}
|
||||
}
|
||||
if t.FailedList == nil {
|
||||
t.FailedList = []FailedItem{}
|
||||
}
|
||||
|
||||
for i := range s.state.Tasks {
|
||||
if s.state.Tasks[i].GID == t.GID && s.state.Tasks[i].Token == t.Token {
|
||||
if t.CreatedAt == 0 {
|
||||
t.CreatedAt = s.state.Tasks[i].CreatedAt
|
||||
}
|
||||
s.state.Tasks[i] = t.clone()
|
||||
s.startFlushLocked()
|
||||
return t.clone()
|
||||
}
|
||||
}
|
||||
|
||||
if t.CreatedAt == 0 {
|
||||
t.CreatedAt = t.UpdatedAt
|
||||
}
|
||||
s.state.Tasks = append([]Task{t}, s.state.Tasks...)
|
||||
stored := t.clone()
|
||||
s.state.Tasks = append([]Task{stored}, s.state.Tasks...)
|
||||
s.startFlushLocked()
|
||||
return t.clone()
|
||||
return stored.clone()
|
||||
}
|
||||
|
||||
// RemoveTask 删单个任务
|
||||
|
||||
Reference in New Issue
Block a user