Skip to content

refactor: share one BeeCount Cloud provider instance - #439

Open
tedzhouhk wants to merge 3 commits into
TNT-Likely:mainfrom
tedzhouhk:agent/unify-beecount-cloud-provider
Open

refactor: share one BeeCount Cloud provider instance#439
tedzhouhk wants to merge 3 commits into
TNT-Likely:mainfrom
tedzhouhk:agent/unify-beecount-cloud-provider

Conversation

@tedzhouhk

@tedzhouhk tedzhouhk commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

修改内容

  • BeeCount Cloud 的 authServiceProvider 复用 beecountCloudProviderInstance,使 UI 与 SyncEngine 使用同一个认证实例
  • 增加 UI auth 与 SyncEngine auth 实例一致性的回归测试
  • 按 review 建议移除未接入导航的 devices_page.dart 改动;本 PR 目前只改 sync_providers.dart 和对应测试

3.7.1 实测现象

3.7.1 已包含 #434。Android 真机启用 2FA 后,从 App 完成一次登录,最初同步正常;过一会儿再次出现 CloudSyncUser not authenticated,随后 App 持续重新发起登录。

服务端日志中的脱敏时间线:

20:03:03  2fa.verify.success
20:03:05  app auth.refresh -> 200
20:03:06  app WebSocket accepted
20:03:08  app auth.login -> requires_2fa
20:14:08  app auth.login -> requires_2fa
20:14:38  app auth.login -> requires_2fa
20:15:08  app auth.login -> requires_2fa

后面三次请求严格相隔 30 秒,与 App 的 _silentRecoveryCooldown 一致。因此这不是 2FA 或 refresh token 几分钟过期:2FA 验证和 refresh 均已成功,App 内另一个 auth 实例随后又进入了无 session 的静默恢复路径;开启 2FA 后静默邮密登录不能完成,于是进入 30 秒循环。

这说明 #434 合并后,独立的 authServiceProvider 仍有用户可观察的影响,不只是理论上的多实例风险。

方向选择

当前实测直接指向长生命周期的 UI auth 实例与 SyncEngine auth 实例分离,因此本 PR 采用较小的“消除这一个多余实例”方案。它不引入 #440 的进程级静态锁或完整跨实例 session 接管逻辑。

本 PR 也不再增加 provider dispose() 生命周期变更,避免非 autoDisposesyncEngineProvider.family 持有已释放实例。读取配置改为等待 activeCloudConfigProvider.future;现有三个 watch 页面都有 .when(error: ...) 分支,直接读取 .future 的登录/配置路径也已有异常处理。

验证

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

flutter test --concurrency=1 test/providers/beecount_cloud_auth_provider_test.dart
00:13 +1: All tests passed!

该测试确认 UI auth 与 SyncEngine provider 的 auth 为同一实例。3.7.1 已完成原始故障的真机复现;#439 构建版的真机复测尚未完成,完成后会补充结果。

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

感谢拆分,#434 已经合并了。这个 PR 我先提一件事,可能会影响你要不要保留其中一部分改动。

devices_page.dart 是一个从未接入的页面

我查了一下,这个页面全仓没有任何人引用

  • 没有任何文件 import
  • 没有任何地方构造 DevicesPage((除了它自己的构造函数声明)
  • 它的两个 l10n key cloudCollabDevicesPageTitle / cloudCollabDevicesPageSubtitle 只在这个文件内部使用
  • 云同步相关页面里也没有残留的入口或注释

从历史看,它是在 dfd5fc5(feat: BeeCount Cloud V2 双向同步 + 跨设备实时协同)那个大提交里进来的,之后一次都没有被修改过。所以是「写完了但一直没挂进导航」的孤儿页面。

也就是说,这个 PR 里对 devices_page.dart 的那 22 行改动落在不可达代码上,跑不到、也没法验证。

建议

devices_page.dart 从本 PR 摘掉,只保留 sync_providers.dartauthServiceProvider 复用共享实例那部分 + 对应测试。这样评审面就只有一个主题,我也好判断那两个行为变化。

设备页本身怎么处理我另外记了 todo,会单独决定 —— 它调的 listDevices() / revokeDevice() 服务端能力其实都是齐的,属于「后端和 UI 都做好了、就是没挂入口」,所以未必是删,也可能是补个入口。但不管哪种都该单独一个 PR,不该混在这次重构里。

关于保留部分,我仍然想确认的两点

这两条是我在 #434 里提到、希望单独评估的行为变化,麻烦你在这个 PR 里说明一下考虑:

  1. ref.onDispose(() => unawaited(provider.dispose())) 之后旧 provider 真的会被 dispose,而 activeCloudConfigProvider 有 7 处 invalidate。syncEngineProvider 是非 autoDispose 的 Provider.family,旧条目会继续持有已 dispose 的 provider。

    我自己的判断是实际后果不严重:provider.auth 会抛 CloudConfigurationException,退化成一条错误日志,而不是像以前那样悄悄再跑一轮重复同步;而且顺手把旧 WS 停了,算改善。但想听你的确认,尤其有没有在切换云服务配置的场景下实测过。

  2. authServiceProvider 改成 await ref.watch(activeCloudConfigProvider.future) 之后,如果 loadActive() 抛错会变成 AsyncError,而不是以前的 NoopAuthService()。它只读 SharedPreferences,概率很低,但 mine_page / cloud_sync_page 都在 watch 这个 provider,想确认一下它们的 .when 分支能兜住。

另外顺带说一句:_getCloudProvider() 里把 AppLocalizations.of(context) 提到 await 之前,顺手修掉了一个 BuildContext 跨异步间隙,这个改得对 —— 如果设备页最后决定保留,这一处值得留着。

@TNT-Likely

Copy link
Copy Markdown
Owner

补充一下上一条评论。除了摘掉 devices_page.dart,我还想请你补上问题描述,否则我倾向关掉这个 PR。

我需要的信息

对剩下的 authServiceProvider 复用共享实例这一项,麻烦说明:

  1. 现象 —— 用户或你实际观察到了什么?(报错、日志、复现步骤)
  2. 根因 —— 是从实际故障反推的,还是从代码推导出「理论上可能」的?
  3. 怎么验证 —— 真机上复现过原始故障、并确认修复后消失了吗?

#434 之所以能合,是因为这三条都齐了:issue #433 有明确报错文案、有用户报告、我按你的描述真机复现出来了、根因定位到两个登录入口的时序差异。这个 PR 目前没有对应的现象描述。

具体想问的是:#434 合并之后,authServiceProvider 那个独立实例还会造成什么可观察的问题?

我的理解是配置页、同步信息页和 SyncEngine 现在都走共享实例了,authServiceProvider 的独立实例只剩下 UI 登录态展示(mine_page / cloud_sync_page / beecount_cloud_sync_page 在 watch 它)。而那几处在登录成功后都有 ref.invalidate(authServiceProvider) 兜着,重建时会从 prefs 读到 session。所以我暂时想不出用户能看到什么异常 —— 如果你有具体场景,请说出来。

需要一起定的方向问题

这个 PR 和 #440同一个问题的两种解法

两个都做是重复投资。我倾向本 PR 这条路 —— 更小、而且让架构朝「全 App 唯一实例」收敛,#440 那些跨实例守卫在单实例下就没有存在意义了。

所以如果你要保这个 PR,请顺带说明你怎么看这个方向选择;如果你认为必须走 #440 那条,也请说明为什么消除多实例不够。

上一条提到的两点仍然需要确认

  1. ref.onDispose(() => unawaited(provider.dispose())) 之后旧 provider 真的会被 dispose,而 activeCloudConfigProvider 有 7 处 invalidate,syncEngineProvider 是非 autoDispose 的 Provider.family,旧条目会继续持有已 dispose 的 provider。有没有在切换云服务配置的场景下实测过?

  2. 改成 await ref.watch(activeCloudConfigProvider.future) 之后,loadActive() 抛错会变成 AsyncError 而不是 NoopAuthService()mine_page / cloud_sync_page.when 分支能兜住吗?

所以

  • 拿不出实际现象、也说不出这一项独立的必要性 → 建议关掉
  • 有具体场景 → 摘掉 devices_page.dart、补上说明,我评估后合

不是否定你的分析质量。只是在没有实测故障支撑的情况下改认证和 provider 生命周期,回归风险我没法评估。

@tedzhouhk

tedzhouhk commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

已按建议处理,并补上 #434 合并后的实测现象:

  1. devices_page.dart 已恢复为主线版本,提交为 0052464。当前 PR diff 只剩:
    • lib/providers/sync_providers.dart
    • test/providers/beecount_cloud_auth_provider_test.dart
  2. 先前的 provider dispose() 生命周期改动已经移除,不会让缓存的 syncEngineProvider.family 持有已释放实例。
  3. activeCloudConfigProvider.future 的异常会进入现有 watch 页面的 .when(error: ...);直接 await 的登录/配置路径也已有异常处理。
  4. 已在 PR 描述补充 3.7.1 真机复现证据。简要时间线是:
    • 2FA verify 成功;
    • App refresh 返回 200;
    • WebSocket 正常建立;
    • 数秒后 App 再次发起 login 并拿到 2FA challenge;
    • 随后每 30 秒重复一次 login,间隔与 _silentRecoveryCooldown 完全一致。

这说明 #434 合并后,独立的 authServiceProvider 仍然有可观察故障,而不只是理论风险。针对这个现象,我同意优先采用本 PR 的小方案:让 UI auth 复用 SyncEngine 的 provider;#440 的完整跨实例锁与接管逻辑暂不纳入。

定向测试已在 Flutter 3.27.3、1.5GB 内存、2 CPU、单并发下通过:

flutter test --concurrency=1 test/providers/beecount_cloud_auth_provider_test.dart
00:13 +1: All tests passed!

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