Skip to content

fix: preserve BeeCount Cloud sessions across auth instances - #440

Closed
tedzhouhk wants to merge 2 commits into
TNT-Likely:mainfrom
tedzhouhk:agent/fix-beecount-cloud-refresh-race
Closed

fix: preserve BeeCount Cloud sessions across auth instances#440
tedzhouhk wants to merge 2 commits into
TNT-Likely:mainfrom
tedzhouhk:agent/fix-beecount-cloud-refresh-race

Conversation

@tedzhouhk

@tedzhouhk tedzhouhk commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

修复内容

  • currentUserrequireAccessToken 和 refresh 前同步同一服务地址的持久化 session
  • 按服务地址串行化 App 内多个认证实例对 session 的读改写
  • refresh 成功响应落盘前校验请求使用的旧 token,丢弃过时响应并接管最新 session
  • refresh 失败清理前执行同样的 token CAS,避免删除另一实例刚轮换的新 session
  • 正确传播跨实例账号切换与退出状态
  • 退出时先原子清理本地 session 并写持久化 tombstone,再 best-effort 请求服务端登出,防止在途 refresh 复活会话

根因

_refreshInFlight 只能去重单个 BeeCountCloudAuthService 实例内的请求。App 内多个实例共用 SharedPreferences,但各自缓存内存 session;rotating refresh、2FA 登录、账号切换和退出并发时,旧实例可能删除或覆盖新 session,或者在退出后继续使用内存 token。

本问题与 #434 的配置页登录路径不同,因此按维护者建议独立评审。

Agentic review

审查过程中发现并修复:

  • 空 session 的旧实例误删新 session
  • A→B 账号切换后旧实例继续使用或恢复 A
  • 旧 refresh 成功响应覆盖新账号
  • 跨实例 logout 后旧 token 继续可用
  • Provider 重建后保存的密码自动恢复已退出会话
  • logout 与在途 refresh 并发时 session 被复活

最终复核无剩余 findings。

验证

全部测试均在 Flutter 3.27.3 容器中限制 1.5GB 内存、2 CPU、单并发运行:

flutter test --concurrency=1 packages/flutter_cloud_sync/test/beecount_cloud_auth_service_test.dart
00:10 +9: All tests passed!

flutter test --concurrency=1 packages/flutter_cloud_sync/test
00:26 +65: All tests passed!

Follow-up to #434.

@tedzhouhk
tedzhouhk marked this pull request as ready for review August 12, 2026 03:54
@TNT-Likely

Copy link
Copy Markdown
Owner

我把这个 PR 的生产代码读完了。先说结论:按目前的信息我倾向关掉,但如果你能补上下面这些,我愿意重新评估。

我需要的信息

对这个 PR 里的每一项机制,麻烦说明:

  1. 现象 —— 用户或你实际观察到了什么?(报错文案、日志片段、复现步骤)
  2. 根因 —— 是从实际故障反推的,还是从代码推导出「理论上可能」的?
  3. 为什么是这个修法 —— 有没有更小的选择?
  4. 怎么验证 —— 除了单测,有没有在真机上复现过原始故障、并确认修复后消失?

#434 之所以能合,是因为它满足了这四条:issue #433 有明确报错、有用户报告、我按你的描述真机复现了、根因定位到配置页与同步信息页两个入口的时序差异。这个 PR 目前一条都没有 —— 我看到的是完整的测试和自洽的推理,但看不到任何一个真实发生过的故障。

为什么我对这个 PR 特别谨慎

这个 PR 的前提正在消失。 它解决的全部是「多个 auth 实例互相踩」的问题:

  • _synchronizePersistedSession —— 跨实例接管另一实例的登录
  • _clearSession(expectedRefreshToken:) —— 跨实例 rotating token 竞态
  • _tryRecoveryLogin(expectedEmail:) —— 旧实例用账号 A 的凭证覆盖账号 B
  • _withSessionMutation —— 进程级互斥锁

#434 已经合并,配置页现在用共享实例登录;#439 如果合并,authServiceProvider 也复用共享实例。两个都落地之后,BeeCount Cloud 只剩一个 auth 实例,上面这四项针对的场景就不存在了。单实例内部本来就有 _refreshInFlight 去重。

换句话说:#439 和这个 PR 是同一个问题的两种解法 —— #439 是「消除多实例」,本 PR 是「让多实例互相安全」。两个都做是重复投资,而 #439 只有 20 行、还顺手让架构更清晰。这也是我更倾向 #439 那条路的原因。

唯一一项与实例数无关的

signOut() 那部分(_takeSessionForLogout + logout tombstone)不依赖多实例:单实例下也可能出现「用户点退出登录,此时一个在途的 refresh 请求返回并把 session 存回去,登录态复活」。这个竞态是真实存在的。

如果这一项有实际现象支撑(用户报告过「点了退出还是登录状态」、或你真机复现过),麻烦只把它单独提出来,我很快就能合 —— 那会是个几十行的小 PR,收益清楚。

另外,这个 PR 已经明显超出拆分范围

#434 拆出来时是约 90 行生产代码,现在是 230 行。新增的 logout tombstone、账号切换守卫、进程级静态锁都不在原来的拆分范围内。

进程级静态锁(static final Map<String, Future<void>> _sessionMutationTails)我尤其想谨慎:它给所有认证操作串行化,是个全局行为改变。在没有实测故障支撑的情况下引入这种东西,风险收益比对我来说不划算。

所以

  • 如果拿不出实际现象 → 建议关掉。这不是否定你的分析质量,而是我没法评估「修了一个可能不存在的问题」带来的回归风险
  • 如果 signOut 那条有真实现象 → 单独提一个小 PR
  • 如果你认为 refactor: share one BeeCount Cloud provider instance #439 那条路不够 → 说明为什么,我们先把方向定下来再写代码

再次感谢你这一轮的配合,#434 拆得很干净,收敛后一次就合了。

@tedzhouhk

Copy link
Copy Markdown
Contributor Author

感谢详细评估,认同这里应优先收敛到单实例方向,而不是在缺少真实故障支撑时引入跨实例协调、进程级锁和额外 session 状态机。

目前我没有观察到或复现过「退出登录后登录态复活」的真实现象,因此也不保留 signOut tombstone 这一部分。本 PR 先关闭;如果后续真机出现 logout/refresh 竞态,会带着明确复现步骤、日志和验证结果另开 issue,再提交只覆盖该竞态的小 PR。

@tedzhouhk tedzhouhk closed this Aug 15, 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.

2 participants