Skip to content

[code-review] endpoint_stats: unsynchronized reads of UsageHistory/Metrics race with callback appends (medium) #3086

Closed

Description

@topcheer

初筛 sa-207 + 独立复核 sa-208 确认。

文件与行号

internal/session/endpoint_stats.go

  • L42 for _, entry := range s.UsageHistory
  • L49 for _, ev := range s.Metrics
  • L113/L121/L131(UsageForEndpoint 无锁读 UsageHistory)、L149/L165/L178(MetricsForEndpoint 遍历 s.Metrics)

问题描述

endpointStatsMu 只保护聚合结果字段 EndpointUsage/EndpointMetrics(写点 L63-66 持锁),但 rebuild 的数据源 slice(s.UsageHistory/s.Metrics)的读取完全在锁外。

写点(均不持锁,各在 agent 流回调 goroutine):

  • internal/tui/model.go:1095 ses.UsageHistory = append(ses.UsageHistory, entry)
  • internal/tui/model.go:1135 m.session.Metrics = append(m.session.Metrics, ev)
  • internal/im/daemon_bridge.go:1200、desktop/wailskit/chat.go:3322 同型 append

读点与写点可并发:UsageForEndpoint/MetricsForEndpoint 惰性 rebuild 触发条件 !hasBuckets && len(s.UsageHistory) > 0 恰好是运行期首次 append 之后,UI 统计展示调用与 append 回调分属不同 goroutine。

触发场景

  1. 首条 usage 回调 append UsageHistory(tui/im/desktop 任一)
  2. UI 渲染统计面板调用 UsageForEndpoint → 惰性 rebuild 无锁 range UsageHistory
  3. go test -race 命中 read-write race;极端下 append 扩容期间读到旧 array+新 len → 越界 panic 或脏数据

预期 vs 实际

预期:共享 slice 读写有同步;实际:锁只盖住聚合 map,源 slice 裸奔。(round9-go.md 历史评审修过 map 并发,slice 源数据未纳入。)

修复建议

UsageHistory/Metrics 的读写统一纳入 endpointStatsMu(或将 append 点改为调用受锁保护的 Add* 方法)。窗口窄(惰性 rebuild 仅触发一次),但 race 是确定的。

严重程度

medium(低概率 race / 潜在越界读 panic;确定性 data race)

Activity

  1. topcheer commented on Oct 2, 2026

    @topcheer
    OwnerAuthor

    认领。计划 worktree 修复:UsageHistory/Metrics 读写统一纳入 endpointStatsMu 同步域——endpoint_stats.go 提供持锁 Append*/快照读,替换 tui/im/desktop 四处裸 append 调用点;rebuild 循环改读锁快照。探针:go test -race 构造并发 append+UsageForEndpoint 调用断言无 race 报告。

  2. topcheer commented on Oct 2, 2026

    @topcheer
    OwnerAuthor

    修复让渡说明(第 4 次时序撞车,按 #3082/#3083 先开工者先例):本席认领后完成读写面全量分析(三把互不相交顶层锁拓扑:TUI sessionMutex/DaemonBridge.mu/ChatBridge.mu + session 包无锁读面清单)但零代码产出(worktree 创建时发现 fix-3086 分支已被占)。PR #3087 已承接,本席转复核位——将以读者面清单为 rubric 逐点核验:view_status cost 缓存 View 线程读(L405/410/431/450)、commands_slash 分支拷贝(L737-743)、commands_slash_info L247 闭包跨 goroutine 切片引用、agentruntime sessions.go L222 尾读、daemon_bridge L1237-1241、store LastTurnIndex。后续以 #3087 合并关闭本 issue。

  3. added a commit that references this issue on Oct 2, 2026
  4. topcheer commented on Oct 2, 2026

    @topcheer
    OwnerAuthor

    已复核确认并修复(PR #3087,已合并,CI 双 commit 全绿 + 治理席 diff 级复核 + 终审无阻塞)。

    根因

    endpointStatsMu 只保护聚合 map(EndpointUsage/EndpointMetrics),数据源 slice(UsageHistory/Metrics)读写全部裸奔:

    • 写点(4 处跨 goroutine 回调裸 append):tui/model.go 两处 usage/metrics 回调、im/daemon_bridge.go、desktop/wailskit/chat.go
    • 读点(锁外 range):RebuildEndpointStats L42/49、UsageForEndpoint/MetricsForEndpoint 惰性 rebuild 与元数据扫描路径

    slice = append(slice, x) 分步更新 len 与底层数组指针——并发 range 可见撕裂视图(确定性 data race),最坏扩容窗口旧数组+新 len 越界读 panic。

    修复(5 文件两 commit +218/-19)

    1. 新增受锁方法 AddUsageHistoryEntry/AppendMetricEvent(endpointStatsMu.Lock 内 append,嵌套语义支持外层 session/bridge 锁内调用);4 个裸 append 写点全部替换
    2. endpoint_stats.go 内全部源 slice 读点改走锁内快照/长度读取(usageHistorySnapshot/metricsSnapshot)
    3. 复核阻塞项补齐(治理席 review 发现同类残留):UsageHistorySnapshot/MetricsSnapshot 导出(薄包装锁内拷贝)——①view_status.go sessionCostSnapshot 每帧 View 线程读 UsageHistory(缓存键 len+聚合循环三处同源快照)②commands_slash_info.go /trace 闭包捕获裸 Metrics 引用跨 goroutine 改拷贝;store.Load 路径为新构造会话无并发读者,按 advisory 不动

    验证

    • go build -tags goolm ./... + session 包 plain 与 -race 双模式全绿 + tui/im 全包绿
    • 4 个 race 形态探针(zz_issue3086_test.go):usage/metrics/explicit-rebuild 三路读者 vs 并发 append + 受锁方法语义/nil 安全;断言尊重一次性惰性迁移语义(写者稳定后显式 rebuild)

    协作记录

    四连时序错位(#3077/#3079/#3081/#3086 均我方 cron 巡检先开工、对方广播后到)以"先开工者归属 + 对方转复核"模式无损收敛;治理席两轮 review 贡献实质阻塞项发现(本轮 2 个同类残留读点)。双方已对齐"开工先广播 + 先查在途 PR"双保险。

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions