perf(server): 看板关闭全部改为并发 GitHub 更新与内部单事务(INT-174) #162
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
fjbobo/fast_ship!162
Loading…
Reference in a new issue
No description provided.
Delete branch "int-174/batch-close-concurrency"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
相关需求
INT-174 看板「关闭全部」对 GitHub Issue 逐条串行更新,最多 200 次往返
改动内容
看板「关闭全部」不再对每条 Issue 走
UpdateInternalIssue:UpdateIssue。已经成功返回的 GitHub 响应仍串行落本地,避免远端已关、本地仍开。UpdateIssue有限并发 4;本地upsertGitHubIssue/ 水位失效仍串行(适配生产SetMaxOpenConns(1))。state=closed/state_reason=completed并删除推荐;事务失败按条记入failures,对外统一为内部错误,不回泄漏驱动原文。batch-close超时 60s,给 30s 停发 PATCH 之后的串行落库留余量。修改范围
server(
IssueService.BatchCloseDoneIssues、repository 批量关单/删推荐、handler 透传 request context)、web(issueApi.batchCloseDone超时)、docs/internal(推荐删除挂载点、列表下推文档)本地验证
cd web && pnpm api-types --checkmake lintgo vet ./...零报告;web eslint 0 warning、tsc 通过make testgo test ./...全包 ok(service 66.2s);web vitest 146 files / 1768 tests passed验证方式(命令 / 无头浏览器):
go test ./internal/service/ -race -run TestIssueServiceBatchCloseDoneIssues_(200 条 mock 延迟下并发度 ≥ 3 且快于串行;取消后后续 PATCH 停止且取消前成功项已落库;内部关单删推荐;混源 GitHub 失败不影响内部已关)启动命令:无(无界面变化,未启
make dev)验证提交 SHA:
a34a3c62837d61fa9c2834eadbe35d7b40a6751a应用可以启动(
make dev),主要使用流程已验证(server / web 按改动范围)异常或空状态已验证(如适用)
make dev未启动:本改动无新页面、无布局变化。server 包测试覆盖批量关闭成功/失败/取消/超上限;web 覆盖 60s timeout 入参。空结果既有TestIssueHandlerBatchCloseDone_EmptyResult。已修复的基线问题
无
端到端截图
无界面变化。关闭全部按钮、确认框文案与布局未改,只改后端关闭路径与前端请求超时。替代证据见上方
make test与-race批量关闭用例。未验证范围与原因
未对真实 GitHub API 打 200 条关闭(用 fake client 注入延迟验证并发与取消)。未在单连接池被其他长事务占住时测内部事务的 context 取消(
WithContext已接上,缺该回归夹具)。API 变更说明
无。
POST /api/projects/{id}/issues/batch-close契约不变;行为变为可取消、GitHub 有限并发、内部单事务。创建 PR 前完成
审查意见处理
$pr-review-loop自动等待审查结果并处理新意见,直到收敛第 1 轮:fast-ship-bot 无 P0/P1,4 条 P2 与 3 条 Note 归为 Consider/Noted,后续见 INT-185。Codex 超过 30 分钟无结论,跳过。
@codex review
代码审查 ·
a34a3c6· 第 1 轮发现 P2 4 条。
cliproxyapi/deepseek-v4.1-flash · pi
thinking=low
P2 server/internal/repository/issue.go:62
问题:
BatchCloseInternalTx只按id IN ids AND source='internal'更新,不返回 RowsAffected,也没有把state纳入条件;调用方随后按len(issues)计成功并无条件DeleteByIssueIDsTx。影响:列表查询到事务之间被并发删除或改 source 的 Issue 实际 0 行更新,但响应仍记为成功并删掉它的推荐,看板刷新后该 Issue 仍是 open,重试也补不回推荐。建议:返回受影响行数,只按实际命中计成功,未命中的 id 记失败且跳过推荐删除;或在 WHERE 里加state='open',行数不符即报错回滚。P2 server/internal/service/issue_list.go:127
问题:30s 整批截止在
FindByID/ListFiltered之前创建,而这两次查询不走 ctx。影响:生产是SetMaxOpenConns(1)+busy_timeout(30000),连接被后台同步等长事务占住时查询可能耗掉大部分预算,查询返回后 ctx 已过期,内部与 GitHub 两侧会整体按「请求已取消或超时」记失败;旧实现没有整批截止,会等锁后继续关闭。建议:把context.WithTimeout移到列表查询之后,或让FindByID/ListFiltered走WithContext(ctx),让截止只覆盖真正的关闭阶段。P2 server/internal/service/issue_list.go:278
问题:整批 30s 的 ctx 同时传给每条
UpdateIssue,截止瞬间最多batchCloseGitHubConcurrency条在途 PATCH 被取消,但 GitHub 侧可能已经处理成功;本地只写回「成功返回」的响应。影响:会出现远端已关、本地仍 open 且计入 failed 的条目(数量约等于并发数),与本次「避免远端已关、本地仍开」的目标不一致,只能等下次同步或重试自愈。建议:在途 PATCH 不要与整批判定共用同一个 ctx(context.WithoutCancel+ 单请求超时),或在取消/失败后按 issue number 回读远端状态再决定是否落库。P2 web/src/lib/api/issues.ts:89
问题:60s 只约束浏览器等待;服务端 30s 截止后主循环仍逐条执行
upsertGitHubIssue+markCollectionsStaleAfterPush,这两步没有 ctx 或总超时。影响:单连接池被占或落库慢时请求会超过 60s,浏览器 abort 并提示「批量关闭失败」,服务端仍在继续关单;onSuccess不执行导致看板缓存不刷新,用户重试会再发一轮 PATCH。建议:给落库阶段也设总截止(或关闭完成后异步落库并尽快返回),并在超时文案里说明可能已部分完成。Note(3 条)
upsertGitHubIssue直接解引用res.updated,没有 nil/ID 为 0 的守卫;当前测试替身在未配置updatedIssuesByNumber时会返回 (nil, nil),真实 client 遇到 200 空响应也会返回零值 issue。影响:nil 会在访问内嵌gh.Issue字段时 panic;零值 issue 会被当成新 Issue 写入github_issue_id=0的脏行并计成功。建议:err == nil && res.updated == nil(或GetID()==0)按失败记录并跳过 upsert。ElapsedMs < n*delay(200×20ms=4s)断言并发收益,而其中包含 200 次串行 SQLite upsert,耗时会随 CI 负载和 -race 明显波动。影响:负载高时会偶发失败,掩盖真实回归。建议:改为断言updateIssueMax达到上限、并在途请求数有界(或直接对 worker 数量做单测),不要用绝对墙钟阈值。issue_pull_request.go的判定),必要时降低并发或加退避。复审说明:推送新提交后会自动重新审查。
本轮未发现 P0/P1;问题集中在批量关闭的边界与错误语义。影响:单连接池繁忙时整批可能被 30s 截止判失败,本地落库与远端关闭在极端时序下会短时不一致,前端 60s 超时也可能先于服务端完成。
@ -47,0 +59,4 @@if len(ids) == 0 {return nil}return tx.Model(&model.Issue{}).P2:问题:
BatchCloseInternalTx只按id IN ids AND source='internal'更新,不返回 RowsAffected,也没有把state纳入条件;调用方随后按len(issues)计成功并无条件DeleteByIssueIDsTx。影响:列表查询到事务之间被并发删除或改 source 的 Issue 实际 0 行更新,但响应仍记为成功并删掉它的推荐,看板刷新后该 Issue 仍是 open,重试也补不回推荐。建议:返回受影响行数,只按实际命中计成功,未命中的 id 记失败且跳过推荐删除;或在 WHERE 里加state='open',行数不符即报错回滚。@ -121,0 +124,4 @@if ctx == nil {ctx = context.Background()}ctx, cancel := context.WithTimeout(ctx, batchCloseDoneTimeout)P2:问题:30s 整批截止在
FindByID/ListFiltered之前创建,而这两次查询不走 ctx。影响:生产是SetMaxOpenConns(1)+busy_timeout(30000),连接被后台同步等长事务占住时查询可能耗掉大部分预算,查询返回后 ctx 已过期,内部与 GitHub 两侧会整体按「请求已取消或超时」记失败;旧实现没有整批截止,会等锁后继续关闭。建议:把context.WithTimeout移到列表查询之后,或让FindByID/ListFiltered走WithContext(ctx),让截止只覆盖真正的关闭阶段。@ -189,0 +275,4 @@results <- githubCloseNetResult{issue: issue, err: errs.ErrIssueReadOnly}return}updated, err := client.UpdateIssue(ctx, issue.GitHubMeta.Number, updateReq)P2:问题:整批 30s 的 ctx 同时传给每条
UpdateIssue,截止瞬间最多batchCloseGitHubConcurrency条在途 PATCH 被取消,但 GitHub 侧可能已经处理成功;本地只写回「成功返回」的响应。影响:会出现远端已关、本地仍 open 且计入 failed 的条目(数量约等于并发数),与本次「避免远端已关、本地仍开」的目标不一致,只能等下次同步或重试自愈。建议:在途 PATCH 不要与整批判定共用同一个 ctx(context.WithoutCancel+ 单请求超时),或在取消/失败后按 issue number 回读远端状态再决定是否落库。@ -84,1 +87,3 @@.post(`projects/${projectId}/issues/batch-close`, { json: data }).post(`projects/${projectId}/issues/batch-close`, {json: data,timeout: BATCH_CLOSE_TIMEOUT_MS,P2:问题:60s 只约束浏览器等待;服务端 30s 截止后主循环仍逐条执行
upsertGitHubIssue+markCollectionsStaleAfterPush,这两步没有 ctx 或总超时。影响:单连接池被占或落库慢时请求会超过 60s,浏览器 abort 并提示「批量关闭失败」,服务端仍在继续关单;onSuccess不执行导致看板缓存不刷新,用户重试会再发一轮 PATCH。建议:给落库阶段也设总截止(或关闭完成后异步落库并尽快返回),并在超时文案里说明可能已部分完成。第 1 轮审查结论已处理(HEAD
a34a3c6)。Codex 自 20:01 请求后超过 30 分钟等待上限仍无结论,本轮跳过,之后不再为它等待。fast-ship-bot 无 P0/P1,4 条 P2 与 3 条 Note 归桶如下。本 PR 按巡检指令收口合并,边界项另立后续问题,不在本 PR 再推提交(避免再开一轮审查空等)。
P2
WithoutCancel会在客户端断开后继续打 GitHub,与「断开后不再发后续更新」冲突。Note
GetID()==0守卫、墙钟断言、403/429 统一文案 — Consider,一并放进后续问题。后续:INT-185。