Beads 独占锁协议(Exclusive Lock Protocol)完全指南:让外部工具安全接管 Dolt 数据库的同步管理
2026/9/13 6:48:19 网站建设 项目流程

Beads 独占锁协议(Exclusive Lock Protocol)完全指南:让外部工具安全接管 Dolt 数据库的同步管理

【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads

Beads 的独占锁协议(Exclusive Lock Protocol)允许外部工具以"独占"方式声明对某个 beads 数据库的管理权,从而阻止后台 Dolt 服务器在同步周期内干扰外部工具的操作。本指南以 engdocs/EXCLUSIVE_LOCK.md 为骨架,结合仓库源码深入讲解锁文件格式、服务器判定逻辑、过期锁检测原理以及 Go/Shell 集成方式,帮助你在确定性执行系统、CI/CD 管道或自定义自动化工具中安全、可预期地接入 Beads 的数据库管理。

协议定位:解决什么问题

Beads 是一个为编码 Agent 提供"记忆升级"的数据库系统,其数据层基于 Dolt(一个支持 git 语义的 SQL 数据库)。当后台 Dolt 服务器持续运行并周期性执行同步(sync)操作时,如果外部工具需要在一段时间内完全掌控数据库状态,二者就会产生冲突。独占锁协议正是在这个场景下设计的协作机制:外部工具写入一个锁文件,Dolt 服务器在每次同步周期开始时检查该文件,一旦发现锁存在,就跳过与该数据库相关的所有同步操作,把舞台完全交给外部工具。

协议官方列举了三类典型使用场景:

  • 确定性执行系统(例如 VibeCoder):需要完整控制数据库状态,不允许服务器在关键操作中途介入;
  • CI/CD 流水线:执行原子化 issue 更新时,不希望服务器同步操作造成干扰;
  • 自定义自动化工具:自行管理 git 同步工作流,需要暂时"接管"数据库。

需要特别强调的是,这是一个协作型(cooperative)协议,而非安全机制——它依赖所有参与者自觉遵守约定,具体边界见后文"边界情况与限制"一节。

锁文件格式与字段说明

独占锁的核心是一个位于工作区内的 JSON 文件,路径固定为.beads/.exclusive-lock。其标准格式如下:

{ "holder": "vc-executor", "pid": 12345, "hostname": "dev-machine", "started_at": "2025-10-25T12:00:00Z", "version": "1.0.0" }

各字段的含义与约束如下:

字段类型是否必填说明
holderstring必填持有锁的工具名称,例如"vc-executor""ci-runner",用于日志识别
pidint必填持有锁的进程 ID,是过期锁检测的关键依据
hostnamestring必填进程所在主机名,用于区分本地锁与远程锁
started_atRFC3339 时间戳必填锁的获取时间,例如2025-10-25T12:00:00Z
versionstring可选锁持有工具自身的版本号,便于排障时定位工具版本

对应文档中的 API 参考,Go 侧将该结构定义为ExclusiveLock,其 JSON 标签与上表一一对应:

// ExclusiveLock represents the lock file format type ExclusiveLock struct { Holder string `json:"holder"` PID int `json:"pid"` Hostname string `json:"hostname"` StartedAt time.Time `json:"started_at"` Version string `json:"version"` }

在仓库中,.exclusive-lock还被纳入了运行时文件的追踪与 gitignore 管理范畴:cmd/bd/doctor/tracked_runtime.go将其列为需要追踪的运行时文件之一(见 cmd/bd/doctor/tracked_runtime.go),而cmd/bd/doctor/gitignore.go则将其列入忽略列表(见 cmd/bd/doctor/gitignore.go),这从侧面印证了该文件的"临时性 + 运行态"定位:锁文件不应被误提交进版本库,但 doctor 工具需要感知它的存在。

服务器行为:每个同步周期开头的四路判定

Dolt 服务器在每个同步周期开始时检查独占锁。依据锁文件的状态,服务器会走四条不同的路径:

  1. 无锁文件:服务器正常执行同步操作;
  2. 有效锁(进程存活):服务器跳过该数据库的所有操作;
  3. 过期锁(进程已死):服务器移除锁文件,然后正常继续同步;
  4. 损坏的锁(JSON 非法):服务器采取 fail-safe 策略,跳过该数据库。

这里有一个重要的时序细节:服务器只在同步周期开始时检查锁。如果锁是在某个同步周期进行中才被创建的,当前这个周期仍会执行完毕,但从下一个周期开始,数据库会被跳过。换句话说,锁的生效存在最多一个同步周期的延迟,集成方在设计时序时应当把这一点纳入考量。

服务器日志中与此相关的典型输出如下:

Skipping database (locked by vc-executor) Removed stale lock (vc-executor), proceeding with sync Skipping database (lock check failed: malformed lock file: unexpected EOF)

排障时可以查看服务器日志文件.beads/dolt/sql-server.log来确认锁相关事件的具体经过。

过期锁检测:ESRCH 判定与 fail-safe 原则

过期锁检测是协议中最微妙的部分,其判定规则如下:

  • hostname 与当前机器一致(不区分大小写)PID 在本机不存在(对 PID 发起探测返回 ESRCH)时,判定为过期锁;
  • 服务器只在能确切证明进程已死(ESRCH)时才移除锁。如果服务器对目标 PID 的探测因权限不足而返回 EPERM,它会把锁当作有效锁处理并跳过数据库——这种 fail-safe 设计避免了误删其他用户持有的锁;
  • 远程锁(hostname 与本机不同)永远被假定为有效,因为服务器无法验证远程进程的状态,所以远程主机上残留的过期锁不会被自动清理,必须人工手动移除。

从源码层面看,仓库在internal/linear/synclock.go中实现了同源的进程存活判定函数IsProcessAlive(见 internal/linear/synclock.go):在 Unix 平台上通过向目标 PID 发送 signal 0 来探测,pid <= 0时直接返回 false;Windows 平台则使用OpenProcess实现等价判定。这与文档中"ESRCH 判死、EPERM 存疑"的语义一致——signal 0 探测本质上就是"只探测、不打扰"的进程存在性检查。

当过期锁被成功移除时,服务器会记录日志:Removed stale lock (holder-name), proceeding with sync

集成示例:Go 与 Shell 双语言实操

创建锁(Go)

官方推荐的 Go 创建方式通过types.NewExclusiveLock构造锁对象,再以缩进 JSON 形式写入.beads/.exclusive-lock

import ( "encoding/json" "os" "path/filepath" "github.com/steveyegge/beads/internal/types" ) func acquireLock(beadsDir, holder, version string) error { lock, err := types.NewExclusiveLock(holder, version) if err != nil { return err } data, err := json.MarshalIndent(lock, "", " ") if err != nil { return err } lockPath := filepath.Join(beadsDir, ".exclusive-lock") return os.WriteFile(lockPath, data, 0644) }

NewExclusiveLock(holder, version string) (*ExclusiveLock, error)会为当前进程自动填充pidhostnamestarted_at等运行时字段;写文件时使用0644权限即可(该文件本身不承载安全职责)。

释放锁(Go)

释放锁即是删除锁文件:

func releaseLock(beadsDir string) error { lockPath := filepath.Join(beadsDir, ".exclusive-lock") return os.Remove(lockPath) }

创建锁(Shell)

对于不依赖 Go 工具链的脚本场景,可以直接用 Shell 拼装 JSON 并利用$$(当前 PID)、$(hostname)date等内建能力生成锁文件:

#!/bin/bash BEADS_DIR=".beads" LOCK_FILE="$BEADS_DIR/.exclusive-lock" # Create lock cat > "$LOCK_FILE" <<EOF { "holder": "my-tool", "pid": $$, "hostname": "$(hostname)", "started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "version": "1.0.0" } EOF # Do work... bd create "My issue" -p 1 bd update bd-42 --claim # Release lock rm "$LOCK_FILE"

注意started_at使用date -u生成 UTC 时间并格式化为 RFC3339,与协议要求一致。

推荐模式:善用清理句柄

锁的正确释放直接关系到后续同步能否恢复,因此官方强烈建议使用清理句柄(defer / trap)保证锁在异常退出时也能被释放:

func main() { beadsDir := ".beads" // Acquire lock if err := acquireLock(beadsDir, "my-tool", "1.0.0"); err != nil { log.Fatal(err) } // Ensure lock is released on exit defer func() { if err := releaseLock(beadsDir); err != nil { log.Printf("Warning: failed to release lock: %v", err) } }() // Do work with beads database... }

即使工具进程崩溃导致锁未能释放,服务器侧的过期锁检测(ESRCH 判死)也能兜底清理本地残留锁,这构成了一套"主动释放为主、服务器兜底清理为辅"的完整闭环。

边界情况与限制

多个写入者、无服务器时

独占锁协议只能阻止 Dolt 服务器干扰,它不提供以下保证:

  • 多个外部工具之间的互斥;
  • 事务隔离或 ACID 保证;
  • 对直接文件系统操作的防护。

如果需要协调多个工具,必须自行实现锁机制(例如参考仓库中internal/linear/synclock.go基于 flock 的内核锁实现,它通过lockfile.FlockExclusiveBlocking/FlockExclusiveNonBlocking提供真正的进程互斥语义,并额外发布pid=/started=元数据用于争用诊断,见 internal/linear/synclock.go)。独占锁协议与之不同,它只是一个"标记文件 + 约定"层面的协作协议。

Git Worktrees

Dolt 原生支持 git worktree。独占锁协议与 worktree 支持是相互独立的机制,互不影响。

远程主机

如前文所述,来自远程主机的锁永远被假定有效,因此远程过期锁不会自动清理,必须人工删除。这在"多机共享同一个数据库目录"的部署形态下需要格外留意。

锁文件损坏

如果锁文件因写入中断等原因变成非法 JSON,服务器会 fail-safe——跳过该数据库,你需要手动修复或删除锁文件才能恢复同步。

集成验证:五分钟跑通全流程

文档给出了一个可复现的验收流程,用于确认你的集成是否生效:

  1. 启动 Dolt 服务器:执行bd dolt start
  2. 创建锁:用你的工具生成.beads/.exclusive-lock
  3. 验证服务器跳过:检查服务器日志,应出现Skipping database消息;
  4. 释放锁:删除.beads/.exclusive-lock
  5. 验证服务器恢复:检查服务器日志,确认恢复正常同步周期。

这套流程既可用于开发阶段的联调,也可作为 CI 中对该协议进行回归验证的冒烟测试脚本。

安全考量:协作协议 ≠ 安全机制

  • 锁文件不安全:任何进程都可以创建、修改或删除它;
  • PID 复用理论上可能引发误判(概率极低,尤其在叠加 hostname 校验之后);
  • 这是一个协作协议,不是安全机制——不要用它承载任何安全边界。

API 参考速览

协议对外暴露的核心 Go API 如下:

// NewExclusiveLock creates a lock for the current process func NewExclusiveLock(holder, version string) (*ExclusiveLock, error) // Validate checks if the lock has valid field values func (e *ExclusiveLock) Validate() error // ShouldSkipDatabase checks if database should be skipped due to lock func ShouldSkipDatabase(beadsDir string) (skip bool, holder string, err error) // IsProcessAlive checks if a process is running func IsProcessAlive(pid int, hostname string) bool

其中IsProcessAlive的进程存活探测在仓库中已有同语义实现(internal/linear/synclock.go中的IsProcessAlive(pid int) bool,Unix 下基于 signal 0),可直接作为理解协议底层判活逻辑的参考实现。

结语

独占锁协议以"一个 JSON 文件 + 一个服务器检查约定"的极简形态,解决了外部工具与后台 Dolt 服务器之间的接管权交接问题。其设计精髓在于三点:协作而非强制(不承诺互斥与安全)、fail-safe 而非激进攻略(EPERM 存疑时宁可不清理)、可观测(完善的服务器日志与工具字段)。无论你是构建确定性执行系统、CI 流水线还是自定义自动化工具,只要遵守"创建锁 → 执行操作 → 释放锁(含清理句柄兜底)"的规范流程,就能稳定地将 Beads 数据库纳入自己的编排体系。更深入的工作流指引可继续查阅仓库根目录的 AGENTS.md 与 README.md,以及 examples/ 目录下的集成示例。

【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询