no-mistakes daemon单例锁实现解析:为什么文件锁不需要陈旧检测
no-mistakes daemon单例锁实现解析为什么文件锁不需要陈旧检测【免费下载链接】no-mistakesgit push no-mistakes项目地址: https://gitcode.com/GitHub_Trending/no/no-mistakesno-mistakes是一款让git push之后自动执行 AI 代码审查与修复守护流程的开源工具。它的核心是一个长期驻留的daemon 守护进程负责管理工作树、流水线执行与崩溃恢复。守护进程最经典的安全难题就是单例锁如何保证同一时刻只有一个 daemon 拥有同一个数据目录本文从源码角度解析no-mistakes daemon 单例锁的设计并回答一个反直觉的问题——为什么基于文件锁的实现完全不需要陈旧检测staleness check。1. 先搞懂背景daemon 为什么必须是单例no-mistakes 的工作方式是你正常git push no-mistakesGit 钩子把任务交给本地 daemondaemon 在一个独立工作树里跑流水线、管理 agent、做崩溃恢复。相关设计在 docs/src/content/docs/concepts/daemon.md 中有完整说明。正因为 daemon 启动时会执行一系列全局性、破坏性操作把上次崩溃遗留的运行标记为失败stale-run recovery清理孤立的 worktree 目录绑定 IPC socket所以同一NM_HOME下如果同时跑两个 daemon后启动的那个会把正在运行的任务的 worktree 误删、把活着的运行误判为崩溃。这就是单例锁必须存在的原因。2. 核心实现三行代码 操作系统内核单例锁的实现在 internal/daemon/lock.go入口函数acquireSingletonLock的逻辑可以概括为三步打开锁文件NM_HOME/daemon.lock路径定义见 internal/paths/paths.go尝试加锁调用tryLockFile做非阻塞排他文件锁记录持有者信息把 PID 和启动时间写入文件仅供诊断不参与安全机制平台差异封装在两个文件里平台文件系统调用关键点Linux/macOSinternal/daemon/lock_unix.goflock(LOCK_EX\|LOCK_NB)锁挂在打开文件描述上内核在进程退出/崩溃时自动释放Windowsinternal/daemon/lock_windows.goLockFileEx字节范围锁锁字节放在0xFFFFFFFF偏移避开文件头部数据区整个方案的核心只有一行系统调用其余代码都是错误处理和诊断信息。3. 为什么不需要陈旧检测传统实现比如 PID 文件方案遇到锁文件存在但持有者已死的尴尬场景通常需要自己写一套陈旧检测读 PID、判断进程是否存活、对比启动时间、超时后强制接管……这套逻辑本身就是竞态的重灾区。而 no-mistakes 的文件锁天生没有这个问题原因一句话就能说清flock / LockFileEx 这类 OS 原生文件锁由内核在持有进程死亡包括被 SIGKILL时自动释放。锁文件里留着锁这个状态就必然意味着有一个活着的进程持有它——锁永远不会陈旧。源码注释把这一点写得很直白internal/daemon/lock.go内核会在持有进程退出或死亡时自动释放锁——即使没有显式 unlock。这种自清理特性正是它不需要像 PID 文件那样做持有者是否还活着的陈旧检查的原因锁只可能被一个确实在运行的进程持有。对比一下两种方案的失效模式PID 文件进程被kill -9后 PID 文件还在 → 需要你写检测代码 → 检测代码可能误判PID 复用→ 需要超时、心跳……复杂度指数级上升OS 文件锁进程被kill -9后内核立即释放锁 → 下一个进程立刻能拿到锁 →零检测代码零竞态窗口这就是把安全性下沉给操作系统的经典收益。4. 细节一被拒绝时如何告诉用户谁在占用锁本身不提供谁持有的信息。no-mistakes 的做法是诊断信息与安全机制分离抢到锁的一方尽力best-effort把{PID, StartedAt}写入daemon.locklock.go抢锁失败的一方读回这份记录报错信息里带上pid 12345, started 2026-08-29T03:00:00Z注意注释特意强调这次写入失败也不致命——因为安全由 OS 锁保证写入只是让错误提示更友好。这种核心机制不依赖任何 best-effort 环节的设计是它值得学习的地方。5. 细节二获取时机——先锁再动任何东西锁的获取时机在 internal/daemon/daemon.go 的RunWithOptions里注释写明了约束单例锁必须在崩溃恢复之前、socket 绑定之前获取并持有一个进程的整个生命周期。这个顺序不是随意排布。设想一个没有锁的启动流程第二个 daemon 先恢复崩溃现场把活着的第一个 daemon 的运行标成失败、删掉 worktree再去绑 socket——灾难就发生了。对应的回归测试 internal/daemon/singleton_test.go 验证的正是这个场景第二个 daemon 启动必须快速失败ErrSingletonLockHeld绝不允许它走到 socket 绑定第一个 daemon 在整个过程中必须保持可达 项目根目录的 AGENTS.md 也把这个设计列为守护性约束内核在任何进程死亡时释放它因此持锁永远意味着持有者存活不需要任何陈旧启发式。6. 防御纵深锁不是唯一一层值得新手理解的是no-mistakes 并没有把安全押在单例锁一个点上而是层层设防见 docs/src/content/docs/concepts/daemon.md 与 AGENTS.md单例锁本文主角阻止第二个 daemon 启动socket 防抢占IPC 层在删除 socket 文件前先拨号探测有人在应答就拒绝接管internal/ipc/client.go进程扫描对账internal/daemon/collision.go 处理更隐蔽的情况——同一个目录用了不同路径拼写如符号链接的NM_HOME导致锁文件不同、socket 也不同。此时通过进程列表发现冲突 daemon健康的拒绝启动僵死的则杀掉并清理collision.goPID 文件注意 PID 文件是可以陈旧的它只做身份记录陈旧 socket / 陈旧 PID 文件由启动时的自愈逻辑处理各层互为独立安全层任何一层被绕过都不会直接导致数据破坏。7. 给开发者的启示 把 no-mistakes daemon 单例锁的设计提炼成三条可复用的经验能用 OS 原生机制就不要自己造。flock/LockFileEx 的进程死亡自动释放是内核保证的语义自研的陈旧检测在正确性上永远打不过它。安全机制与诊断信息分离。锁的持有者是内核事实写进文件的 PID 记录只是友好提示写失败无所谓——这让你的核心路径永远只有一个依赖。明确获取时机即正确性。单例锁保护的是破坏性全局操作所以必须先锁后做一切并用回归测试固化这个顺序。对日常使用 no-mistakes 的用户来说你几乎不需要感知它的存在如果真有两个 daemon 冲突第二个会直接报错并告诉你当前 daemon 的 PID 和启动时间而不是悄悄把第一个搞挂。这份安静但绝不失守的可靠性正是这套单例锁设计的价值所在。【免费下载链接】no-mistakesgit push no-mistakes项目地址: https://gitcode.com/GitHub_Trending/no/no-mistakes创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考