gbrain 中gbrain serve与gbrain sync的并发协作:PGLite 单写者模型下的同步委派机制
【免费下载链接】gbrainGarry's Opinionated OpenClaw/Hermes Agent Brain项目地址: https://gitcode.com/gh_mirrors/gb/gbrain
导读
在 gbrain 的 PGLite 数据脑(embedded Postgres/WASM 单写者架构)上,gbrain serve常驻持有数据库连接,此时再运行gbrain sync会因单写者锁而无法打开数据库。本文基于 docs/architecture/serve-sync-concurrency.md,完整解析 gbrain 如何通过"把同步委派给 serve 进程"来化解这一冲突:包括委派决策梯、经过鉴权的持久化 IPC 协议、分片式游标推进、失败恢复与挂起诊断。读完你将掌握 serve 在线期间正确执行同步的全部规则、限制与排障手段。
核心结论:serve 在线时,sync 委派给 serve 执行
一句话版本:在 PGLite 数据脑上,即使gbrain serve正在运行,gbrain sync也可以照常执行——同步工作被委派给 serve 进程,因为 serve 本来就持有唯一的写连接。
PGLite 是单写者的嵌入式 Postgres(以 WASM 运行)。常驻持有者(resident owner)在连接关闭前一直持有数据存储外部稳定的原生锁。存活的持有者永远不会被驱逐(live holder is never displaced),失败的 IPC 也永远不会授权第二次打开。这意味着任何试图绕开持有者直接打开 PGLite 数据存储的做法,在 gbrain 中都是不被允许的。
从源码看,这一约束在 CLI 连接层就得到了贯彻:gbrain sync在连接引擎之前会先探测存活持有者,若发现持有者是存活的gbrain serve,则走委派路径而不是直接报错(参见 src/commands/sync-delegate.ts 中maybeDelegateSyncToServe的注释:LiveServeLockError 自 #2348 起确立"存活持有者不可被取代")。该模块明确自述:"PGLite brain 上存活的gbrain serve在其生命周期内持有单写者锁,因此gbrain sync无法打开数据脑——与其失败,不如把同步在 serve 内部通过 resolve-IPC 套接字执行,CLI 侧只负责轮询进度并打印结果。"
委派如何工作:四步机制
CLI 在打开数据存储之前先解析所选数据脑,并通过经过鉴权的持久化 IPC(authenticated persistence IPC)将工作委派给被观察到的常驻持有者。整体分四步:
- CLI 使用其持久化的本地 CLI 注册凭据发起委派。HTTP 与 stdio 常驻进程都暴露该监听器,但 hook secret 或 stdio 注册不能授予 CLI 权限。选中的 PGLite 挂载(mount)使用各自的数据存储与注册。也就是说,委派权限只属于"可信的本地管理通道"CLI 通道,stdio 被视为不可信的内存调用方,无法借道获取 CLI 权威(见 docs/guides/concurrent-writes.md 的"Local registrations and canonical ownership"一节)。
- 在受管激活(managed activation)之前,持有者在其既有连接上运行仅导入的
performSync;激活之后,它通过持久化日志推进有界的受管同步分片(bounded managed-sync slices)。客户端在结果为writer_yield或writer_pending时反复重试这些分片。 - 受管同步保留其不可变(immutable)的发现清单与游标。若客户端退出或丢失确认,已接受的页面请求仍可完成;用相同选项重跑即可从剩余游标处继续。
- 嵌入(embeddings)被延后执行,因为委派绕过了直接 CLI 的内联成本门槛(inline cost gate)。持有者使用其配置的 provider 与密钥来排空嵌入;
--no-embed会抑制该调度。
从源码看,委派侧的选项传递经过了严格的默认拒绝(default-deny)白名单:sync-delegate.ts定义了可过线转发的布尔旗标表(WIRE_BOOL_FLAGS:--full、--dry-run、--no-pull、--no-embed、--no-extract、--no-schema-pack、--skip-failed、--retry-failed、--include-gitignored)、CLI 自行消费的取值旗标(VALUE_FLAGS:--source、--timeout、--hard-deadline)以及仅在委派外有意义的旗标(IGNORED_FLAGS:--yes、--no-delegate、--no-hard-deadline)。任何不在表内的 argv 令牌都会触发明确点名该旗标的拒绝——正如源码注释所说:"一个被静默丢弃的--exclude会执行错误的同步;拒绝是唯一安全默认。"
决策梯(decision ladder)
maybeDelegateSyncToServe的决策过程在源码中清晰可读,共六级:
- 0. 显式退出:出现
--no-delegate或环境变量GBRAIN_SYNC_NO_DELEGATE=1→ 不委派,走常规连接路径。 - 1. 非宿主数据脑(挂载)不委派:套接字、secret 与锁都属于宿主数据目录,挂载脑的同步必须走常规路径。
- 2. 无存活持有者,或持有者是非 serve 进程→ 不委派(维持旧行为:死 PID 回收 / 有界等待)。
- 3. 存活 serve + 白名单之外的 argv 令牌→ 默认拒绝并点名该旗标,退出判卷(exit verdict)为 1。
- 4. 存活 serve + 套接字应答→ 委派:打印横幅、以 1 秒周期轮询
sync_status、首次 Ctrl-C 触发sync_abort(第二次 Ctrl-C 直接硬退出)、打印同步结果。 - 5. 存活 serve 但无套接字 / 陈旧 serve / 未授权→ 给出带修复建议的礼貌拒绝,退出判卷 1,绝不抛原始堆栈。
每种拒绝都给出三条出路:去掉触发拒绝的旗标(如果是旗标导致的)、停止 serve、或传--no-delegate。轮询侧也有容错设计:MAX_POLL_FAILURES = 60次连续失败才判定套接字死亡,且每 5 次失败会用只读 PID 探测区分"serve 忙"(长同步 WASM 语句会阻塞事件循环,瞬时超时是正常的)与"serve 已消失"。
委派超时与客户端死亡防护
客户端总是发送其解析出的硬性截止时间(interactive 默认 3600s,非 TTY 默认同样映射为 3600s),serve 侧每个调用都有界。--no-hard-deadline请求无同步截止时间。关键在于:即使客户端进程死亡,任务也始终有界——这是唯一一种"无界"编码(0 秒)之外的保障,见deriveDelegatedTimeoutSeconds的实现。serve 侧还对超时做了硬上限钳制:DELEGATED_SYNC_TIMEOUT_MAX_SECONDS = 86_400(24 小时),与同步硬截止时间的量级一致(src/core/context/sync-ipc.ts)。
IPC 线协议:窄类型、secret 门控、fail-closed
委派 IPC 是三种窄请求类型(wire shapes 定义于 src/core/context/sync-ipc.ts):
sync_start { options, clientToken }→{ ok, jobId }或{ ok:false, error }sync_status { jobId }→ 任务状态 + 进度 + 最终结果sync_abort { jobId }→ 开始协作式中止(typed partial)
该模块刻意保持"叶节点"设计:纯类型 + 纯函数,不导入 resolve-ipc 或任何引擎模块,避免循环依赖。信任姿态与 turn_context 一致——窄类型请求、secret 门控、原始 SQL 绝不越过线缆。服务端验证器validateDelegatedSyncOptions对未知键一律拒绝(这是防止服务端专用 SyncOpts 字段从套接字触达的关键)、类型不符拒绝、sourceId必须符合规范形状、timeoutSeconds必须是非负整数。noEmbed有一个微妙语义:它不会到达performSync——委派的同步任务永远以 noEmbed 运行(因为 #2139 成本门槛位于runSync,委派路径从不经过它);该旗标只是记录"用户拒绝了嵌入",从而抑制 serve 的延后嵌入排空(缺省时由 serve 的后续 sweep 排空)。
clientToken是客户端生成的一次性幂等令牌:丢失确认后的重试若命中busy且令牌匹配,则是在附着(attach)到自己的任务;若令牌匹配到保留的终态任务,则返回{ok, jobId, completed:true}而不是重复执行。
委派与 MCP 流量的共存:共享数据存储与公平调度
MCP 流量与委派同步共享持有者的数据存储。受管同步在有界批次之间让步(yield),但导入工作仍可能影响读延迟。这背后的调度策略在 src/core/persistence/sync-run.ts 的performManagedSync中有具体实现:
- 公平性双向保证:前台请求优先获得服务;sync 在每 25 个前台提交或连续 1 秒前台服务后,即使新交互请求持续到达,也能赚到一个有界批次(
creditedPages = 25,信用在 250ms 后清零)。 - 一次只准入一页(one page admitted ahead of the scan),扫描在每最多 25 页或 250ms 之间让出。
- 批次让步:每个 slice 完成后返回
partial+writer_yield,客户端据此重复下一 slice;若某个写入请求在 5 秒等待后仍未达终态,则返回partial+writer_pending。这两者正是委派客户端持续循环的条件。 - 游标持久化:游标头(header)与不可变清单(manifest)分离存储——推进一页时只重写游标头,绝不重写整个发现文件列表。清单存于私有
managed-sync操作的op_checkpoints(singleton JSON-array envelope),清单本身以managed-sync-manifest独立存储。saveCursor通过乐观并发(completed_keys比较)在事务内更新,防止多个持有者循环互相覆盖。
受管同步的调度规则、支持的导入类型与检查点规则详见 canonical writer enforcement;注册、回执与恢复详见 concurrent writes。值得强调的是:数据存储所有权与gbrain-sync:*源租约是两回事——原生锁阻止第二个 PGLite 持有者出现,源租约则协调同步工作本身;租约过期或 PID 元数据都不授权文件系统所有权的接管。
限制一览表
| 场景 | 行为 |
|---|---|
不支持的旗标(--all、--watch、--workers、--break-lock以及任何未分类项) | 点名该旗标直接拒绝。受支持的选项包括--repo、--source、--exclude、--src-subpath、--include-hidden、--json;接受某个旗标并不等于绕过受管模式限制。 |
serve --http或 stdio MCP | 两者都暴露经过鉴权的持久化 IPC,供本地 CLI 注册使用。 |
| 陈旧或不可用的常驻 IPC | 拒绝连接而不是另开一个引擎;升级/重启常驻进程,或恢复后用相同选项重试。 |
| 挂载数据脑(mounted brains) | 选中的 PGLite 挂载委派给它们自己的持有者。Postgres 不需要 PGLite IPC,但规范 worktree 所有权仍然适用。 |
| 客户端退出选择 | --no-delegate或GBRAIN_SYNC_NO_DELEGATE=1禁用委派;但它并不允许打开已被持有的 PGLite 数据存储。 |
| 截止时间 | 客户端发送其解析出的硬截止时间(interactive 默认 3600s);serve 每个调用都有界。--no-hard-deadline请求无同步截止时间。 |
| 受管导入 | 使用--no-pull。Git pull/rebase、代码/图片导入器与忽略文件遍历仍被拒绝,参见 canonical writer 指南。 |
| serve 在同步中途关闭 | 持有者中止当前分片并等待其工作完成后才断开。已接受的页面请求与受管游标保持其持久状态。 |
需要特别注意的是表格最后一行与源码的一致性:serve 的关闭不是"丢弃"同步——中止的分片是协作式的,游标落盘,下次用相同选项重跑即从断点继续。
旧版兼容协议
较旧的共享 secret 协议sync_start/sync_status/sync_abort仍作为未激活数据脑的兼容路径保留。它的旗标集更窄,且拒绝受管数据脑。GBRAIN_SERVE_SYNC_IPC=0可禁用该旧协议,但它不能替代撤销持久化 CLI 注册。换句话说:禁用旧协议只是关掉一扇门,撤销注册才是撤销权威本身。sync-delegate.ts的messages表对unsupported_kind的解释也印证了这一点:"serve 已禁用同步委派(GBRAIN_SERVE_SYNC_IPC=0或启动失败)"。
如果 serve 在同步中途死亡
当进程死亡时,内核会释放其原生锁。后继者必须先获取该锁,并协调持久化请求与恢复状态,然后才能发布(publish)。遗留的.gbrain-lock元数据仅为兼容性与诊断保留;删除它不能授权接管——这与sync-delegate.ts的 crash 提示一致:serve 死亡后,其同步锁行可能需要最多 60 秒才能变为可回收;若重跑报告死 PID 锁,可用gbrain sync --break-lock清除。但请记住,--break-lock在委派模式下是被拒绝的旗标,只有 serve 死亡、锁真正归属死 PID 时才适用。
恢复的实操路径:
- 重复相同的
gbrain sync选项以恢复受管游标。不可变清单 + 持久游标保证断点续传。 - 意外的文件字节会使受影响的根(root)保持阻塞以待修复,而无关的根可以继续推进。
- 在尝试任何管理性修复之前,先检查持有者与恢复状态:
gbrain sources writer status --probe --json诊断同步挂起
手动排障:按文件追踪
若同步卡死(无进展、高 CPU),带上 per-file begin 追踪重跑,卡住的文件名就会被点名:
GBRAIN_SYNC_TRACE=1 gbrain sync --no-pull --no-embed --yes最后一条[sync] begin import: <path>且没有对应完成记录的,就是挂起发生时正在处理的文件。在--workers >1/--all下,卡住的文件位于"有 begin 行但没有匹配完成行"的集合中。源码层面,GBRAIN_SYNC_TRACE在 src/commands/sync.ts 中于每文件导入前输出[sync] begin import: ${path}(注释明确说明这是为挂起排障设计的;大脑库巨大时可用环境变量按需开启,避免每文件一行刷屏)。
schema-pack 正则导致的卡死
如果怀疑是 schema-pack 正则导致的(某个包带有灾难性回溯的inference.regex),禁用该包完成同步,之后再重跑抽取:
gbrain sync --no-schema-pack --no-pull --no-embed --yesgbrain schema lint会将经典嵌套量词 ReDoS 形态((a+)+、(a*)*、……)标记为警告。--no-schema-pack在sync.ts中从 v0.41.37.0 (#1569) 起作为逃生舱口存在:跳过包加载,页面退回到遗留前缀分型(legacy prefix typing),并会向 stderr 打印明确提示。
自动化排障:进度感知的停顿看门狗
手动诊断有一个自动化表亲:进度感知的停顿看门狗(progress-aware stall watchdog)。如果导入排空在GBRAIN_SYNC_STALL_ABORT_SECONDS(默认 900 秒,以文件导入进度为键,而非锁心跳)内没有前进,运行会以reason: 'stall_timeout'中止,并释放 per-source 锁,使下一次gbrain sync从检查点恢复。sync-lock.ts的类型定义确认了stall_timeout是官方partial结果原因之一。
两条重要边界:
- 看门狗在文件之间触发——卡在某个文件导入内部的挂起会一直跑到墙钟硬截止时间。
0可禁用看门狗。
完整的同步可恢复性旋钮表位于 CLAUDE.md 的 "Sync resumability + lock tuning" 一节。
与周边机制的边界
理解本机制还需要区分三组容易混淆的概念(详见 docs/architecture/canonical-writers.md 与 docs/guides/concurrent-writes.md):
- 受管激活(managed activation)是显式边界:激活后,页面正文、frontmatter、可见性、标签、别名、事实、takes、时间线行、源身份/路径与同步检查点都要求协调者权威。SQL 触发器覆盖 pages、tags、slug 别名、自由文本别名、facts、takes、时间线条目与 sources;物理嵌入/索引遥测只是投影。
- 锁 ≠ 所有权:PGLite 只有一个进程持有者;Postgres 允许多个经过鉴权的入口进程,但每个规范文件系统根只有一个指定宿主持有者。陈旧心跳是诊断信息,绝不授权接管所有权。
- 注册是持久的、独立的委托主体:CLI 通道是可信本地管理;stdio 是不可信内存调用方。撤销在重启后依然有效;丢失凭据文件或收到拒绝响应,都不会静默创建一个替代委托主体。stdin 凭据与旧共享 secret 同步都无法获取 CLI 权威。
总结
gbrain serve与gbrain sync的并发不是靠"抢占锁"解决的,而是靠"把同步搬进锁持有者内部"解决的:CLI 通过 secret 门控的窄类型 IPC,把有界分片交给持有单写者连接的 serve 执行;不可变清单 + 持久游标 + 幂等令牌保证任意时刻中断都能断点续传;默认拒绝的旗标白名单保证委派永远不做"错误的同步"。理解这套委派模型与它的限制表、恢复流程和看门狗,是在 PGLite 数据脑上安全运行常驻 serve 与定期同步的前提。
【免费下载链接】gbrainGarry's Opinionated OpenClaw/Hermes Agent Brain项目地址: https://gitcode.com/gh_mirrors/gb/gbrain
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考