Conversation
issue LinekForge#27 的 PID lock(273bf3a)让第二个实例进入 passive 模式以避免重复触发, 但 acquirePidLock() 只在启动时调用一次。active 实例退出后,已经处于 passive 的 实例不会重新竞选——即使它们还活着、MCP 还连着,调度也静默停摆到下一个新 session 启动为止。 原实现处理了 stale 锁回收,但只在启动路径上:新实例发现旧持锁者已死时接管。 没覆盖的是反过来的顺序——已经在场的 passive 实例发现 active 死了该怎么办。 实测(2026-09-08):三个实例,active 被关闭后剩下两个 passive。此时 `/mcp` 显示 连接正常、进程在、MCP 工具可调用,但 PID 锁文件消失、engine.log 停止输出,全部 定时任务不再触发。故障唯一的可见信号是一个没人会主动去看的锁文件。 改动: - passive 实例启动 30s 间隔的重新竞选定时器,夺锁成功即接管调度 - 抽出 activate(),首次启动与事后接管共用同一段排程逻辑 - stopScheduler() 清理竞选定时器 - 导出 isPassiveMode() / retryPassivePromotion() 供测试直接驱动,不必等 interval acquirePidLock() 本身未改:持锁者仍存活时照旧返回 false。所以接管只可能发生在锁 被释放(正常退出)或变成 stale(崩溃)之后,不会退回 LinekForge#27 的重复触发。 测试 forge-engine/scheduler-pid-lock.test.ts(4 个,含一条防止修过头的反向测试): - 持锁者存活时进入 passive - 持锁者正常退出后自我提升 - 持锁者崩溃留下 stale 锁时自我提升 - 持锁者仍存活时保持 passive,且不夺锁 验证: - (cd forge-engine && bunx tsc --noEmit && bun test) → 23 pass / 0 fail - bun hub-test-harness/harness.ts → 8/8 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017pGyNVmhbA1Z1vRoxLZbFe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
动机
273bf3a(修 #27)引入 PID lock 后,第二个 engine 实例会进入 passive 模式以避免重复触发。但acquirePidLock()只在startScheduler()里调用一次——active 实例退出后,已经处于 passive 的实例不会重新竞选,即使它们还活着、MCP 还连着。调度就此静默停摆,直到下一个新 session 启动。根因:原实现处理了 stale 锁回收,但只在启动路径上——新实例发现旧持锁者已死时接管。没覆盖的是相反顺序——已经在场的 passive 实例,发现 active 死了该怎么办。
顺带一提,
isPassive这个变量在改动前是只写不读的(除stopScheduler()里重置外没有任何读取点),这大概也说明当初留了个没接上的口子。复现:
此时 B、C 仍在运行、
/mcp显示 forge-engine 连接正常、MCP 工具可正常调用,但engine-data/engine.pid已消失、engine.log停止输出、所有定时任务不再触发。故障唯一的可见信号是一个没人会主动去看的锁文件。 2026-09-08 实测遇到,停摆约 10 分钟才被发现;此前 09-03 有过一次同类症状的静默停摆(config.ts顶部注释记录为 10 小时 31 分、漏跑 12 个任务)。改动概要
activate(),首次启动与事后接管共用同一段排程逻辑stopScheduler()清理竞选定时器isPassiveMode()/retryPassivePromotion(),测试可直接驱动一次竞选,不必等 intervalforge-engine/scheduler-pid-lock.test.ts,4 个用例,用FORGE_ENGINE_DATA指向临时目录做隔离:最后一条是防止修过头——如果接管条件放松了,它会失败。
影响范围
forge-engine/的 scheduler(PID lock 之后的 passive 分支)与一个新测试文件;hub-server / hub-client / cli 不涉及。acquirePidLock()本身未改:持锁者仍存活时照旧返回false。所以接管只可能发生在锁被释放(正常退出)或变成 stale(崩溃)之后,不会退回 forge-engine: multi-instance duplicate triggers from per-session engine-channel #27 的重复触发。assignee、按实例 tag 分配)解决的是「谁该执行」,本 PR 解决的是「负责的那个死了之后谁来接」——两者正交,不冲突。加了 affinity 之后,如果被指派的实例退出,任务同样会丢,这个竞选机制仍然需要。self-test 结果
grep -rE '@im\.wechat|ou_[a-f0-9]{16,}|sk-ant-…|sk-…|[0-9]{9,10}:[A-Za-z0-9_-]{35}'对本 PR 改动文件 → 无命中。