Skip to content

[verify] bootStack({ databaseFile }) 的数据活不过进程内重启 —— 表在、行没了,挡住冷启动类 e2e #4518

Closed

Description

@os-zhuang

在做 #4470(让验证 harness 走真实持久化路径)时发现的。它挡住了 #4470「最小形态」的第三条,所以单独记录 —— packages/qa/dogfood/test/flow-durable-suspend.dogfood.test.ts 的文件头、以及 @objectstack/verify 的 changeset 都引用了这条。

问题

#4470 给 bootStack 加了 databaseFile,本意是让两次顺序启动共享同一个 SQLite 文件,从而构成一次真正的冷启动(ADR-0019 的挂起 run 必须活过的那种重启)。实际行为是:第二次 bootStack 打开同一个文件,表结构在,行没了。

关键点是这与挂起 run 无关 —— 普通业务记录同样不保留。所以归因在 harness/驱动这一层,不在挂起存储。

复现

const dir = mkdtempSync(join(tmpdir(), 'probe-'));
const dbFile = join(dir, 'v.sqlite');

const a = await bootStack(someStack, { automation: true, databaseFile: dbFile });
const ta = await a.signIn();
await a.apiAs(ta, 'POST', '/data/suspend_note', { name: 'persisted-me', status: 'new' });
await a.stop();                       // kernel shutdown

const b = await bootStack(someStack, { automation: true, databaseFile: dbFile });
const tb = await b.signIn();
await b.apiAs(tb, 'GET', '/data/suspend_note?limit=10');

实测输出:

PROBE hot row status         200                      ← 第一个进程里读得到
PROBE file exists            true 1073152             ← 文件确实在,1MB
PROBE cold automation_run    {"records":[],"total":0}
PROBE cold suspend_note      {"records":[],"total":0} ← 普通记录也没了

两次运行文件大小完全一致(1073152),像是在某个早期时点(schema sync 之后)写过一次就再没更新。表能查到 200(不是 no such table),说明第二次启动确实读到了那个文件 —— 只是里面没有后来写入的行。

线索

  • packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.ts —— persist 默认 'on-disconnect':只在驱动 disconnect 时把 sql.js 的内存镜像刷回磁盘(外加一个 process.once('beforeExit') 的兜底 flush)。
  • packages/runtime/src/default-datasource-plugin.ts —— destroy() 会 connection.disconnect('default', { asDefault: true }),看起来应该触发那次 flush。
  • packages/verify/src/harness.ts —— stop() 关掉 http server 后调用 kernel.shutdown();日志里确实出现 Graceful shutdown started / ✅ Graceful shutdown complete。

所以从代码看这条链路本该 flush,但结果表明没有(或者 flush 发生得太早)。没有继续深挖,因为这已经在 #4470 的范围之外了。

注意 beforeExit 兜底对进程内重启是无效的:进程根本没退出。objectstack dev 这类「跑到被杀」的用法因此不受影响,这也可能是它一直没被发现的原因。

影响

  1. 挡住 [verify] 端到端验证从构造上绕开了挂起 run 的持久化路径(harness 写死 suspendedRunStore: 'memory') #4470 的第三条最小形态 —— 「冷启动一个新 kernel,resume 能继续走对分支」目前无法在 dogfood 层断言。已在测试文件头标注为 KNOWN GAP,没有用放松断言的方式假装通过。
  2. 任何想验证「跨进程状态存活」的 e2e 都会撞上同一堵墙 —— 而这正是持久化类缺陷([automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420 那一类)最需要被覆盖的维度。

期望

bootStack({ databaseFile }) 之后的 stop() 应当保证数据已落盘,使下一次 bootStack 是一次真实的冷启动。可能的方向(未验证,留给认领者判断):

  • 查清 stop() → kernel.shutdown() → 插件 destroy() → 驱动 disconnect() 这条链在 harness 里是否真的走全了;
  • 或让 harness 在 stop() 里显式调用驱动已有的 flush();
  • 或在 databaseFile 模式下改用更即时的 persist 策略(如 debounced:*)。

修好之后,flow-durable-suspend.dogfood.test.ts 最自然的下一个用例就是它最初被写出来的那个:挂起 → 关停 → 冷启动 → resume 走对分支。

参考

Activity

  1. self-assigned this
    on Aug 2, 2026
  2. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 1 轮(/pm-dispatch 会话)
    分支:claude/issue-4518-bootstack-durable-flush
    Worktree:objectstack-issue-4518
    开发 agent 即将开工;如另有会话已在做本 issue,请以更早的认领评论为准,本认领让行。


    Generated by Claude Code

  3. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    PM 验收:ACCEPT → PR #4564(等 CI 全绿后加入 merge queue)。

    根因不在 issue 预判的三个方向,而在 driver-sqlite-wasm 的 Knex 方言:_query 按「是否返回行」选执行分支、只在无行分支标脏,而 ObjectQL 的写入形状正是 INSERT … RETURNING *——几乎全部业务写入都走了不标脏的分支,on-write 与 on-disconnect 两种策略共用同一个 dirty 标志,一起丢写。修复为结构性收敛:「语句是否改库」只有一个判定点(statementMutatesDatabase,同时看 method 与 SQL 文本),执行与持久化两段分离;markDirty() 去参数化,消除双判定点。stop() 返回 ⇒ 落盘,自此是结构保证。dogfood 的 KNOWN GAP 冷启动套件解封:挂起 → 关停 → 冷启动 → resume 走对分支 → 第三次启动仍可读。回滚验证 10 条测试变红。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions