fix(android): 去掉诊断记录的对象锁,消除本地诊断数据库死锁导致心跳停止 #386

Open
opened 2026-10-13 09:14:35 +08:00 by ila · 5 comments
Owner

基本信息

  • 类型:缺陷修复(仅 Android Agent:去掉诊断记录上的对象锁,消除本地诊断数据库死锁)。涉及并发,属高风险类别,方案已由用户确认。
  • 状态:已建单,开始实施。
  • 建单:用户「只删这一行,建单并做」;Gitea MCP 不可用,回退 REST。
  • 来源:2026-10-13 用户反馈采购1 的 Agent 显示「连接异常」,要求分析原因。抓取线程堆栈确认是死锁后,用户问:「我只需要agent正常使用,不会死锁,怎样才能最小化有效修改呢」;了解到其他卡死原因无法完全排除后,决定:「只删这一行,建单并做」。
  • 关联:#370、#373、#378(引入共用诊断数据库的诊断记录、失败快照和同步诊断)、#385(同批 Agent 修复,一起发版)。

已核验事实(2026-10-13)

现场(采购1,设备 #8,PKG110,Agent 0.9.69-373-fix1;只读查询与 adb 检查)

  • 服务端最后一次心跳是 10-12 19:10:50,当时 busy=1、正在执行任务 #3004;19:11:48 设备转为离线,之后没有任何心跳。
  • 任务 #3004 于 19:11:42 在服务端已是 completed(说明当时网络正常);Agent 常驻通知一直停在 19:11:56 的「任务 #3004 失败」。
  • Agent 进程从 10-12 15:06 起持续运行,前台服务存在,Wi-Fi 正常且已通过系统联网验证。
  • 2026-10-13 09:0x 经用户同意,用 jdb 短暂连接(约 10 秒)导出线程堆栈,之后已恢复运行:
    线程 位置 持有 等待
    失败快照清理(maintainFailureSnapshots,每 60 秒) PurchaseFailureSnapshotStore.transaction 已 beginTransaction,事务内 execute → helper.writableDatabase(PurchaseFailureSnapshotStore.kt:9) 写连接 AgentDiagnosticStore 对象锁
    诊断记录(diagnosticExecutor) AgentDiagnosticStore.record()(@Synchronized)→ beginTransaction(:179) AgentDiagnosticStore 对象锁 写连接(SQLiteConnectionPool.waitForConnection)
    同步/心跳(executor) SyncDiagnosticRecorder.begin → AndroidSyncSql → diagnosticStore.writableDatabase(AgentForegroundService.kt:150) — AgentDiagnosticStore 对象锁
  • 结论:诊断记录与失败快照清理的加锁顺序相反,形成死锁。同步线程随之永久阻塞,working 标记一直不被清除,后续心跳全部跳过,因此 Admin 显示离线,而手机界面看起来正常。
  • 未查明:#3004 在服务端已完成后,Agent 为什么仍按失败处理(需另查,不在本单范围)。

代码(origin/main d4cf2bb,即 0.9.70,仍有同样问题)

  • persistence/AgentDiagnosticStore.kt:169 @Synchronized fun record(...):方法内只用到局部变量和 db,写入、回读和清理在同一个事务中完成,没有需要对象锁保护的共享字段;唯一调用方在单线程执行器中(AgentForegroundService.kt:157)。
  • 共用该数据库的另外两条路径:PurchaseFailureSnapshotStore.transaction/execute/query,以及 SyncDiagnosticStore(AndroidSyncSql.transaction → open())。这两条只在事务内短暂调用 writableDatabase/readableDatabase,数据库已打开时会立即返回,不会持有对象锁等待写连接。
  • AgentDiagnosticStore 中没有其他 @Synchronized 或 synchronized(this)。

目标

消除 Agent 本地诊断数据库的死锁,避免同步/心跳线程永久阻塞。

非目标

  • 不缓存数据库对象,不改 PurchaseFailureSnapshotStore、SyncDiagnosticStore、FailureSnapshotRepository。
  • 不加同步卡住检测或看门狗(用户:「只删这一行」)。
  • 不改表结构、数据库版本、服务端和 API。
  • 不调查 #3004 被当作失败的原因,不处理系统清理导致的离线。

方案

删除 AgentDiagnosticStore.record() 上的 @Synchronized。不再有线程「持有对象锁并等待写连接」,消除加锁顺序倒置。多个事务由 SQLite 写连接自动排队,数据一致性不受影响。

设计证据

无界面变化,不需要原型。

子项目影响、依赖与并行

  • 仅 Android:persistence/AgentDiagnosticStore.kt 一行。
  • 无依赖;与 #385 文件不重叠,建议一起合并并发布 0.9.71。

验收标准

  • Android 单元测试与 Debug 构建通过。
  • 测试机真机(装机需用户授权;不动采购1、采购2):正常执行一批采集任务,心跳连续,诊断记录照常写入。
  • 如实记录局限:死锁是偶发的,电脑上的单元测试无法使用 Android SQLite 复现,真机也无法按需触发;正确性主要依据上述堆栈与加锁顺序分析。

风险与回退

  • 风险低:去掉的是多余的对象锁,record() 的调用方本来就是单线程。
  • 其他原因导致的卡死(尚未审查的锁、系统清理等)不在本单范围,用户已知悉。
  • 回退:恢复这一行。

文档影响

无长期文档影响:内部并发缺陷修复,不改变入口、配置、接口、数据结构或业务规则。

## 基本信息 - 类型:缺陷修复(仅 Android Agent:去掉诊断记录上的对象锁,消除本地诊断数据库死锁)。**涉及并发,属高风险类别**,方案已由用户确认。 - 状态:**已建单,开始实施**。 - 建单:用户「只删这一行,建单并做」;Gitea MCP 不可用,回退 REST。 - 来源:2026-10-13 用户反馈采购1 的 Agent 显示「连接异常」,要求分析原因。抓取线程堆栈确认是死锁后,用户问:「我只需要agent正常使用,不会死锁,怎样才能最小化有效修改呢」;了解到其他卡死原因无法完全排除后,决定:「只删这一行,建单并做」。 - 关联:#370、#373、#378(引入共用诊断数据库的诊断记录、失败快照和同步诊断)、#385(同批 Agent 修复,一起发版)。 ## 已核验事实(2026-10-13) ### 现场(采购1,设备 #8,PKG110,Agent 0.9.69-373-fix1;只读查询与 adb 检查) - 服务端最后一次心跳是 10-12 19:10:50,当时 busy=1、正在执行任务 #3004;19:11:48 设备转为离线,之后没有任何心跳。 - 任务 #3004 于 19:11:42 在服务端已是 `completed`(说明当时网络正常);Agent 常驻通知一直停在 19:11:56 的「任务 #3004 失败」。 - Agent 进程从 10-12 15:06 起持续运行,前台服务存在,Wi-Fi 正常且已通过系统联网验证。 - 2026-10-13 09:0x 经用户同意,用 jdb 短暂连接(约 10 秒)导出线程堆栈,之后已恢复运行: | 线程 | 位置 | 持有 | 等待 | |---|---|---|---| | 失败快照清理(`maintainFailureSnapshots`,每 60 秒) | `PurchaseFailureSnapshotStore.transaction` 已 `beginTransaction`,事务内 `execute` → `helper.writableDatabase`(`PurchaseFailureSnapshotStore.kt:9`) | 写连接 | `AgentDiagnosticStore` 对象锁 | | 诊断记录(`diagnosticExecutor`) | `AgentDiagnosticStore.record()`(`@Synchronized`)→ `beginTransaction`(`:179`) | `AgentDiagnosticStore` 对象锁 | 写连接(`SQLiteConnectionPool.waitForConnection`) | | 同步/心跳(`executor`) | `SyncDiagnosticRecorder.begin` → `AndroidSyncSql` → `diagnosticStore.writableDatabase`(`AgentForegroundService.kt:150`) | — | `AgentDiagnosticStore` 对象锁 | - 结论:诊断记录与失败快照清理的加锁顺序相反,形成死锁。同步线程随之永久阻塞,`working` 标记一直不被清除,后续心跳全部跳过,因此 Admin 显示离线,而手机界面看起来正常。 - 未查明:#3004 在服务端已完成后,Agent 为什么仍按失败处理(需另查,不在本单范围)。 ### 代码(`origin/main` `d4cf2bb`,即 0.9.70,仍有同样问题) - `persistence/AgentDiagnosticStore.kt:169` `@Synchronized fun record(...)`:方法内只用到局部变量和 `db`,写入、回读和清理在同一个事务中完成,没有需要对象锁保护的共享字段;唯一调用方在单线程执行器中(`AgentForegroundService.kt:157`)。 - 共用该数据库的另外两条路径:`PurchaseFailureSnapshotStore.transaction`/`execute`/`query`,以及 `SyncDiagnosticStore`(`AndroidSyncSql.transaction` → `open()`)。这两条只在事务内短暂调用 `writableDatabase`/`readableDatabase`,数据库已打开时会立即返回,不会持有对象锁等待写连接。 - `AgentDiagnosticStore` 中没有其他 `@Synchronized` 或 `synchronized(this)`。 ## 目标 消除 Agent 本地诊断数据库的死锁,避免同步/心跳线程永久阻塞。 ## 非目标 - 不缓存数据库对象,不改 `PurchaseFailureSnapshotStore`、`SyncDiagnosticStore`、`FailureSnapshotRepository`。 - 不加同步卡住检测或看门狗(用户:「只删这一行」)。 - 不改表结构、数据库版本、服务端和 API。 - 不调查 #3004 被当作失败的原因,不处理系统清理导致的离线。 ## 方案 删除 `AgentDiagnosticStore.record()` 上的 `@Synchronized`。不再有线程「持有对象锁并等待写连接」,消除加锁顺序倒置。多个事务由 SQLite 写连接自动排队,数据一致性不受影响。 ## 设计证据 无界面变化,不需要原型。 ## 子项目影响、依赖与并行 - 仅 Android:`persistence/AgentDiagnosticStore.kt` 一行。 - 无依赖;与 #385 文件不重叠,建议一起合并并发布 0.9.71。 ## 验收标准 - [ ] Android 单元测试与 Debug 构建通过。 - [ ] 测试机真机(装机需用户授权;**不动采购1、采购2**):正常执行一批采集任务,心跳连续,诊断记录照常写入。 - [ ] 如实记录局限:死锁是偶发的,电脑上的单元测试无法使用 Android SQLite 复现,真机也无法按需触发;正确性主要依据上述堆栈与加锁顺序分析。 ## 风险与回退 - 风险低:去掉的是多余的对象锁,`record()` 的调用方本来就是单线程。 - 其他原因导致的卡死(尚未审查的锁、系统清理等)不在本单范围,用户已知悉。 - 回退:恢复这一行。 ## 文档影响 无长期文档影响:内部并发缺陷修复,不改变入口、配置、接口、数据结构或业务规则。
Author
Owner

2026-10-13 开始实施(Claude Code)

  • 用户:「只删这一行,建单并做」。改动只有一行,由 Claude Code 直接实施,不派子 Agent。
  • 分支 fix/386-diagnostic-deadlock,worktree D:/OPC/goauto-worktrees/issue-386,基线 origin/main d4cf2bb。
  • Gitea MCP 不可用,回退 REST。
### 2026-10-13 开始实施(Claude Code) - 用户:「只删这一行,建单并做」。改动只有一行,由 Claude Code 直接实施,不派子 Agent。 - 分支 `fix/386-diagnostic-deadlock`,worktree `D:/OPC/goauto-worktrees/issue-386`,基线 `origin/main` `d4cf2bb`。 - Gitea MCP 不可用,回退 REST。
Author
Owner

2026-10-13 实现与合并(Claude Code)

实现

  • 分支 fix/386-diagnostic-deadlock,提交 71165b1:只删除 AgentDiagnosticStore.kt 中 record() 上的 @Synchronized(1 行)。
  • 分支单独验证:testDebugUnitTest assembleDebug BUILD SUCCESSFUL,815 个用例,0 失败。

合并与装机

待验证与局限

  • 测试机正常执行一批采集任务:心跳连续,诊断记录照常写入。
  • 死锁是偶发的,无法在单元测试(JVM 上不能用 Android SQLite)或真机上按需复现;正确性依据正文中的线程堆栈与加锁顺序分析。

状态:待验收。

### 2026-10-13 实现与合并(Claude Code) ### 实现 - 分支 `fix/386-diagnostic-deadlock`,提交 `71165b1`:只删除 `AgentDiagnosticStore.kt` 中 `record()` 上的 `@Synchronized`(1 行)。 - 分支单独验证:`testDebugUnitTest assembleDebug` BUILD SUCCESSFUL,815 个用例,0 失败。 ### 合并与装机 - 用户:「推送分支,合并#385和#386,装包到测试机」。 - main:`d4cf2bb..328afde` - `7c0d37b` merge #385 - `afa03ef` merge #386 - `328afde` 版本升到 0.9.71(versionCode 84) - 合并结果重新运行 `testDebugUnitTest assembleDebug`:BUILD SUCCESSFUL,822 个用例,0 失败,0 错误,0 跳过。 - APK 用 debug keystore 改签,证书 SHA-256 为 `3eab2527dc3ce592fe22f2f8cc31e4d0fa07dd1fa54b6209948ea6ce9cd7c4f6`;APK SHA-256 为 `10ad5a2b898230897b15a54f73212ae56a11fcf2f73beffc0392321699d7cf00`。 - 测试机 `3B668T01GF400000`(PLY110)装机前核对了序列号,`install -r` 成功:0.9.70(83)→ 0.9.71(84),2026-10-13 09:29:28。**采购1、采购2 未动。** ### 待验证与局限 - [ ] 测试机正常执行一批采集任务:心跳连续,诊断记录照常写入。 - 死锁是偶发的,无法在单元测试(JVM 上不能用 Android SQLite)或真机上按需复现;正确性依据正文中的线程堆栈与加锁顺序分析。 状态:待验收。
Author
Owner

2026-10-13 装采购1(Claude Code)

  • 用户:「最新apk安装到192.168.0.3:39779」(采购1,序列号 3B65BD02H7F00000,PKG110)。
  • 装前核对:
    • 采购1 在线空闲,没有待执行或执行中的采集/采购任务;
    • 已装的 0.9.69-373-fix1 签名证书 SHA-256 为 bf86d746…766dec,与测试机用的 debug keystore(3eab2527…)不同;
    • 改用与之一致的本机 keystore 对同一个 328afde 构建重新签名,证书核对一致;APK SHA-256 为 3004cda3784182381e98ce9953cabcdba4017eb535c8578a2b317b3a7303e6ab。
  • install -r 成功:0.9.69-373-fix1(82)→ 0.9.71(84),2026-10-13 09:35:01。没有卸载,本地凭据和数据保留。
  • 装后 Agent 进程已被系统拉起,但前台服务尚未启动(09:35:14 时心跳仍是装前的记录),需要在手机上打开 Agent;如果无障碍显示未连接,需把 GoAuto 关闭后重新打开。
  • 采购2 未动。
### 2026-10-13 装采购1(Claude Code) - 用户:「最新apk安装到192.168.0.3:39779」(采购1,序列号 `3B65BD02H7F00000`,PKG110)。 - 装前核对: - 采购1 在线空闲,没有待执行或执行中的采集/采购任务; - 已装的 0.9.69-373-fix1 签名证书 SHA-256 为 `bf86d746…766dec`,**与测试机用的 debug keystore(`3eab2527…`)不同**; - 改用与之一致的本机 keystore 对同一个 `328afde` 构建重新签名,证书核对一致;APK SHA-256 为 `3004cda3784182381e98ce9953cabcdba4017eb535c8578a2b317b3a7303e6ab`。 - `install -r` 成功:0.9.69-373-fix1(82)→ 0.9.71(84),2026-10-13 09:35:01。没有卸载,本地凭据和数据保留。 - 装后 Agent 进程已被系统拉起,但前台服务尚未启动(09:35:14 时心跳仍是装前的记录),需要在手机上打开 Agent;如果无障碍显示未连接,需把 GoAuto 关闭后重新打开。 - 采购2 未动。
Author
Owner

2026-10-13 Wiki:装机签名对照(Claude Code)

  • 用户:「写进记忆和 Wiki 运维页」。
  • Deployment-and-Operations 在「2026-10-09 集成发布与 Android 签名连续性」一节后新增「Agent 装机签名对照(2026-10-13 核对)」,内容包括:
    • 采购1、采购2 与测试机的证书和签名文件对照表;
    • 本机 Gradle 调试产物已是采购机签名,测试机需要改签;
    • 装前核对序列号、签名和空闲状态;
    • 不得卸载重装。
  • 线上 revision 37630e95b7e03248546467826525f45921033f74,回读一致。
  • 执行一次 sync、一次 sync --check 均通过,镜像提交 59a483f 已推送 main(328afde..59a483f)。
  • 补充更正:装采购1 时的重新签名是多余的。Gradle 产物本身已是采购机证书 bf86…,结果不受影响。
### 2026-10-13 Wiki:装机签名对照(Claude Code) - 用户:「写进记忆和 Wiki 运维页」。 - Deployment-and-Operations 在「2026-10-09 集成发布与 Android 签名连续性」一节后新增「Agent 装机签名对照(2026-10-13 核对)」,内容包括: - 采购1、采购2 与测试机的证书和签名文件对照表; - 本机 Gradle 调试产物已是采购机签名,测试机需要改签; - 装前核对序列号、签名和空闲状态; - 不得卸载重装。 - 线上 revision `37630e95b7e03248546467826525f45921033f74`,回读一致。 - 执行一次 `sync`、一次 `sync --check` 均通过,镜像提交 `59a483f` 已推送 main(`328afde..59a483f`)。 - 补充更正:装采购1 时的重新签名是多余的。Gradle 产物本身已是采购机证书 `bf86…`,结果不受影响。
Author
Owner

2026-10-13 Claude Code:采购2 装机 0.9.71

经用户授权(「把最新版apk安装到采购2,192.168.0.9:33585」),在采购2(设备 #7,PLY110,序列号核对为 3B663A00JW500000)上覆盖安装 0.9.71(versionCode 84,main 328afde 构建;此后 main 的 Android 代码没有变化)。

  • 装前核对:已安装 APK 与待装 APK 的签名证书都是 bf86d746…766dec;线上该设备没有待执行或执行中的采集任务和采购任务,心跳正常。
  • adb install -r 成功(不卸载),装后版本为 0.9.71/84;APK SHA-256 3004cda3…03e6ab。
  • 装后 Agent 进程未运行,需要采购员手动打开 Agent;如状态 Tab 显示无障碍「未连接」,按提示在系统设置中把 GoAuto 无障碍关闭后重新打开。
  • 真机验证(无障碍暂停领取、心跳连续)仍待用户在设备上确认。
### 2026-10-13 Claude Code:采购2 装机 0.9.71 经用户授权(「把最新版apk安装到采购2,192.168.0.9:33585」),在采购2(设备 #7,PLY110,序列号核对为 `3B663A00JW500000`)上覆盖安装 0.9.71(versionCode 84,main `328afde` 构建;此后 main 的 Android 代码没有变化)。 - 装前核对:已安装 APK 与待装 APK 的签名证书都是 `bf86d746…766dec`;线上该设备没有待执行或执行中的采集任务和采购任务,心跳正常。 - `adb install -r` 成功(不卸载),装后版本为 `0.9.71`/84;APK SHA-256 `3004cda3…03e6ab`。 - 装后 Agent 进程未运行,需要采购员手动打开 Agent;如状态 Tab 显示无障碍「未连接」,按提示在系统设置中把 GoAuto 无障碍关闭后重新打开。 - 真机验证(无障碍暂停领取、心跳连续)仍待用户在设备上确认。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#386