Skip to content

fix(storage): 修复刷新锁固定重试导致的饥饿 - #71

Merged
sunerpy merged 2 commits into
mainfrom
fix/refresh-lock-retry-starvation
Jul 27, 2026
Merged

fix(storage): 修复刷新锁固定重试导致的饥饿#71
sunerpy merged 2 commits into
mainfrom
fix/refresh-lock-retry-starvation

Conversation

@sunerpy

@sunerpy sunerpy commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

延续 #59 对数据库锁的修复思路,把同一套 deadline + 抖动退避机制补齐到刷新锁;并顺带消除流迭代的一处误报告警、补齐重试可观测性。

1. 刷新锁饥饿(fix(storage)

withRefreshLock 此前仍使用固定 retries: 10(约 7.5s 预算),而其 stale 为 15s:

  • 孤儿锁陷阱:持锁进程在刷新 token 中途被 kill 后,残留锁需满 15s 才判定过期,但等待方 7.5s 就耗尽预算抛 ELOCKED(即 TUI 的 Lock file is already being held)。
  • 无抖动:多进程竞争时按相同节奏重试,形成惊群。

修复:新增 acquireRefreshLock,结构与已发布的 acquireDatabaseLock 完全对称(deadline 循环 + isLockContention 判定 + 抖动退避 + 非竞争错误直接抛出)。REFRESH_LOCK_DEADLINE_MS = 15000,沿用 #59deadline == stale 约定。asyncBackoff 参数化后由两把锁共用,数据库锁显式传入原有 25/250ms,有效行为逐字节不变

说明:刷新锁在网络刷新期间被持有,但这不影响修复——proper-lockfile 对存活持有者每 stale/2 自动刷新 mtime,活进程不会被误判过期;等待方拿到锁后 readLatestAuth 直接采纳刚持久化的新 token,不会重复发起网络刷新。因此「等待更久」在此处是正收益。

2. 误报告警与可观测性(fix(streaming)

  • 完成元数据之后的传输关闭(ECONNRESET / terminated)本属正常收尾,此前记为 logger.warn 造成误报;现改为常规日志并标注 outcome: 'ignored_after_completion_metadata'
  • 流迭代日志补充 conversationId / account / accountId / streamAttempt / maxStreamAttempts 关联字段。
  • 按结局分类:retryingrecoveredexhaustedterminated_after_output
  • 提取 MAX_STREAM_ATTEMPTS 常量替代硬编码 3

结构化 503 响应体保持不变(retryable / phase / emittedOutput / code)。

不变量保持

Provider id 仍为 kiro-auth;未改动任何锁文件名、getRefreshLockPath / getKeepAliveLockPath、AWS wire strings;互斥语义与锁序(refresh → db)不变。

Testing

  • make ci 通过:802 pass, 0 fail, 2244 expect()(基线 797 + 新增 5)。
  • bun run build 退出 0。
  • 两个改动文件 lsp_diagnostics 均无诊断。
  • TDD 证据(已独立复现):把实现回滚到 HEAD 后跑新增的刷新锁争用测试 → 1 fail,8596ms 后抛 code: "ELOCKED",报错文件正是 .kiro-refresh-*.lock,证明旧的 7.5s 预算确实会饥饿;恢复实现后 → 1 pass(9.03s)。
  • 新增测试覆盖:刷新锁持续争用、retrying/recovered 日志关联、exhausted 日志且 503 响应不变、terminated_after_output、完成元数据后传输关闭不产生失败告警。

取舍说明:刷新锁争用测试包含约 8.5s 真实等待(超时上限 12s)。若只等 3s,旧实现也能通过、测试将失去鉴别力。代价是整套测试从约 17s 增至约 23s。

Blocked features

无。ELOCKED 致命边界(token-refresher.ts 终态 throw 及若干 batchSave)本次未触碰,与 #59 对数据库路径的处理策略保持一致。

sunerpy added 2 commits July 27, 2026 12:21
- 新增 acquireRefreshLock 采用 deadline 与抖动退避
- 刷新锁预算由固定 10 次提升至 15 秒以覆盖 stale 窗口
- 参数化 asyncBackoff 供数据库锁与刷新锁共用
- 完成元数据后的传输关闭改记常规日志不再告警
- 流迭代日志补充会话账号与尝试次数关联字段
- 按 retrying recovered exhausted 等结局分类日志
- 提取 MAX_STREAM_ATTEMPTS 替代硬编码重试上限
@sunerpy
sunerpy merged commit ec05535 into main Jul 27, 2026
3 of 4 checks passed
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.03846% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/plugin/storage/locked-operations.ts 97.05% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/core/request/request-handler.ts 97.29% <100.00%> (+0.30%) ⬆️
src/plugin/storage/locked-operations.ts 91.37% <97.05%> (+0.34%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sunerpy
sunerpy deleted the fix/refresh-lock-retry-starvation branch July 27, 2026 04:33
@github-actions github-actions Bot mentioned this pull request Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant