package engine // Dream 文件锁 -- 防止多个进程同时执行 Dream 巩固. // // 升华改进(ELEVATED): 借鉴早期方案 autoDream 的 mtime-as-state 设计-- // // 锁文件 mtime = lastConsolidatedAt(上次巩固时间). // 好处:一个文件同时承担互斥锁和状态存储两职,消除状态文件和锁文件之间的竞态: // 原方案(两文件):TryAcquire 更新锁文件,成功后再写 dream_state.json. // 若进程在两步之间 crash,下次启动 state 显示"从未巩固"→ 门槛立即通过 → 重复触发 Dream. // 新方案(mtime-as-state):TryAcquire 本身就更新了 mtime,crash 后 mtime 仍保留 // "巩固于 T 时刻"的信息,防止重复触发. // // 与早期方案的区别: // // 早期方案用 PID 体内容检测死进程(因为没有真正的 flock),我们用 flock(2)(OS 自动处理死进程). // 两种机制互补:flock 处理互斥,mtime 处理持久状态. // // 跨平台:Linux/Mac 用 syscall.Flock,Windows 构建标签隔离(dream_lock_windows.go). import ( "fmt" "os" "path/filepath" "time" ) // ReadLockMtime 读取锁文件的 mtime,用作 lastConsolidatedAt. // // 精妙之处(CLEVER): mtime 即状态--不需要单独的 JSON 字段记录"上次巩固时间". // TryAcquire 成功时会将 mtime 更新为 now,这就是"巩固开始时间". // 进程 crash 后,mtime 仍然保留,下次启动能正确判断"24h 内是否已巩固过". // 对比:dream_state.json 只有在 saveStateLocked 成功后才更新, // crash-between-dream-and-save 会导致下次启动重复触发. // // 返回零值 time.Time 表示锁文件不存在(从未巩固过,触发首次 Dream). func ReadLockMtime(path string) time.Time { info, err := os.Stat(path) if err != nil { return time.Time{} // ENOENT 或其他错误:视为从未巩固 } return info.ModTime() } // FileLock 是基于文件系统的分布式锁. type FileLock struct { path string file *os.File acquired bool } // NewFileLock 创建一个文件锁实例. func NewFileLock(path string) *FileLock { return &FileLock{path: path} } // ensureDir 确保锁文件所在的目录存在. func (l *FileLock) ensureDir() error { dir := filepath.Dir(l.path) return os.MkdirAll(dir, 0755) } // TryAcquire 尝试非阻塞获取文件锁. // 返回锁文件之前的 mtime 和是否成功. // 如果锁文件不存在,priorMtime 为零值. func (l *FileLock) TryAcquire() (priorMtime time.Time, ok bool) { if l.acquired { return time.Time{}, false } if err := l.ensureDir(); err != nil { return time.Time{}, false } // 读取当前 mtime(如果文件存在) if info, err := os.Stat(l.path); err == nil { priorMtime = info.ModTime() } // 打开或创建锁文件 f, err := os.OpenFile(l.path, os.O_CREATE|os.O_RDWR, 0644) if err != nil { return time.Time{}, false } // 尝试获取排他锁(非阻塞) if err := tryFlock(f); err != nil { f.Close() return time.Time{}, false } l.file = f l.acquired = true // 更新 mtime 为当前时间(标记锁已被获取) now := time.Now() if err := os.Chtimes(l.path, now, now); err != nil { fmt.Fprintf(os.Stderr, "dream_lock: chtimes failed: %v\n", err) } return priorMtime, true } // Release 释放锁. func (l *FileLock) Release() { if !l.acquired || l.file == nil { return } unlockFlock(l.file) l.file.Close() l.file = nil l.acquired = false } // Rollback 在任务被 kill 时恢复锁文件的 mtime. // 这样下次 Dream 检查时不会认为巩固已经完成. func (l *FileLock) Rollback(priorMtime time.Time) { if priorMtime.IsZero() { // 锁文件之前不存在,删除它 os.Remove(l.path) return } if err := os.Chtimes(l.path, priorMtime, priorMtime); err != nil { fmt.Fprintf(os.Stderr, "dream_lock: rollback chtimes failed: %v\n", err) } } // IsAcquired 返回当前是否持有锁. func (l *FileLock) IsAcquired() bool { return l.acquired }