
OpenViking 路径锁与崩溃恢复上下文数据库写操作一致性的工程实践【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenVikingOpenViking 是面向 AI Agent 的自演进上下文数据库将源数据FS与派生索引VectorDB解耦管理。本文以docs/zh/concepts/09-transaction.md为核心深入讲解 OpenViking 如何通过路径锁PathLock与持久化队列恢复两个简单原语保护rm、mv、add_resource、session.commit四类核心写操作的一致性并结合仓库中 Rust ragfs 锁子系统与 Python 队列消费者源码剖析其底层实现原理。读完本文你将掌握 OpenViking 的一致性设计哲学、EXACT/TREE/MV 三种锁模式的冲突矩阵、LockContext 的使用方式、session_commit两阶段提交的崩溃恢复流程以及相关的配置方法与源码验证路径。设计哲学索引可重建源数据不可丢OpenViking 中VikingFS 是源数据source of truthVectorDB 是派生索引。二者关系决定了整体的一致性策略宁可搜不到不要搜到坏结果。索引丢失可以从源数据重建源数据丢失则不可恢复。因此所有一致性设计都围绕一个原则任何时刻都保证源数据完整、可重试索引只是可快速重建的派生品。这解释了为什么rm要先删索引再删文件——即使中途崩溃源文件仍在重试即可安全完成。设计原则写互斥通过路径锁保证同一路径同一时间只有一个写操作默认生效所有数据操作命令自动加锁用户无需额外配置锁即保护进入LockContext时加锁退出时释放没有 undo/journal/commit 语义——锁只负责互斥不负责回滚仅 session_memory 需要崩溃恢复通过持久化session_commit队列在进程崩溃后恢复 Phase 2Queue 操作在锁外执行SemanticQueue/EmbeddingQueue 的 enqueue 是幂等的失败可重试。架构总览Service Layer (rm / mv / add_resource / session.commit) | v --[LockContext 异步上下文管理器]------- | | | 1. 创建 LockHandle | | 2. 获取路径锁轮询 超时 | | 3. 执行操作FS VectorDB | | 4. 释放锁 | | | | 异常时自动释放锁异常原样传播 | --------------------------------------- | v Storage Layer (VikingFS, VectorDB, QueueManager)锁子系统目前在仓库中分为两层实现Rust 层生产路径锁语义的真正实现位于 crates/ragfs/src/lock/mod.rs其中PathLockManager是锁语义的唯一事实来源single source of truthPathLockProvider是存储抽象内存或文件系统LockPathResolver计算锁文件路径LockTokenCodec负责锁令牌编解码。Python 侧说明也印证了这一点openviking/storage/transaction/init.py 明确指出 Path-lock management has been moved to the Rust ragfs layer. Python-side lock operations now go through RAGFSBindingClient.pathlock_* methods。Python 层通过VikingFS暴露的pathlock_acquire_exact/pathlock_acquire_tree/pathlock_acquire_batch/pathlock_release/pathlock_adopt等 API 与 Rust 层交互例如 openviking/storage/viking_fs/_ops.py 中的删除、移动逻辑以及 openviking/storage/viking_fs/_access.py 中的目录访问保护。两个核心组件组件 1PathLockEngine LockManager LockContext路径锁系统PathLockEngine实现基于文件的分布式锁支持 EXACT 和 TREE 两种锁类型使用 fencing token 防止 TOCTOU 竞争自动检测并清理过期锁。在 Rust 实现中锁类型由 crates/ragfs/src/lock/types.rs 的PathLockKind枚举定义Exact编码为字符ETree编码为T锁令牌LockToken包含owner_idUUIDv4 风格所有者的身份标识、time_ns最近一次写入/刷新的纳秒时间戳与lock_type。LockHandle是轻量的锁持有者令牌dataclass class LockHandle: id: str # 唯一标识用于生成 fencing token locks: list[str] # 已获取的锁文件路径 created_at: float # handle 创建时间 last_active_at: float # 最近一次成功 acquire/refresh 的时间LockManager是全局单例管理锁生命周期创建/释放 LockHandle后台清理泄漏的锁进程内安全网启动后由 QueueManager 恢复持久化的session_commitPhase 2 任务在 Rust 层租约模型由 crates/ragfs/src/lock/types.rs 的PathLockLease统一租约记录、OwnedPathLockLease持所有权可 refresh/release/handoff与BorrowedPathLockLease只读证明无生命周期控制权组成PathLockHandoffRef则用于跨队列/跨 worker 的锁转移序列化。LockContext是异步上下文管理器封装加锁/解锁生命周期# Conceptual example: production path locks are acquired inside the Rust ragfs layer. async with LockContext(lock_manager, [path], lock_modeexact) as handle: # 在锁保护下执行操作 ... # 退出时自动释放锁包括异常情况组件 2持久化session_commit队列崩溃恢复session.commit的 Phase 2 不再使用独立 RedoLog。Phase 1 会先把 archive 元数据持久化再把SessionCommitMsg写入持久化队列进程重启后QueueManager 会继续消费遗留的session_commit任务并恢复 Phase 2。在源码中这一队列由 openviking/storage/queuefs/queue_manager.py 定义SESSION_COMMIT SessionCommit默认最大并发max_concurrent_session_commit 8轮询间隔 1.0 秒。队列消费者是 openviking/storage/queuefs/session_commit_processor.py 中的SessionCommitProcessor其_process方法调用session.resume_queued_commit(msg)从 archive 恢复 Phase 2若返回未处理例如会话已被删除则把消息重新 enqueue 等待重试。Memory 提取是幂等的从同一个 archive 重新提取会得到相同结果。一致性问题与解决方案rm(uri)问题方案先删文件再删索引 - 文件已删但索引残留 - 搜索返回不存在的文件调换顺序先删索引再删文件。索引删除失败 - 源文件仍在重试可完成可能只执行了一部分的索引清理加锁策略根据目标类型区分删除目录lock_modetree锁目录自身及其整棵子树删除文件lock_modeexact锁文件路径本身操作流程1. 检查目标是目录还是文件选择锁模式 2. 获取锁 3. 删除 VectorDB 索引 - 搜索立刻不可见 4. 删除 FS 文件 5. 释放锁源码印证在 openviking/storage/viking_fs/_ops.py 中可以看到删除逻辑按目标类型选择锁方法——目录走pathlock_acquire_tree文件走pathlock_acquire_exact操作完成后统一pathlock_release。索引 URI 收集或 VectorDB 删除失败 - 直接抛异常锁自动释放源文件仍在。多记录删除在部分后端可能已经执行了一部分但重试可以安全补完清理。FS 删除失败 - VectorDB 已删但文件还在重试同样安全。mv(old_uri, new_uri)问题方案文件移到新路径但索引指向旧路径 - 搜索返回旧路径不存在先 copy 再更新索引失败时清理副本加锁策略通过lock_modemv自动处理移动目录源路径加 TreeLock目标路径加 ExactPathLock移动文件源路径和目标路径各加 EXACT 锁操作流程1. 检查源是目录还是文件确定 src_is_dir 2. 获取 mv 锁内部根据 src_is_dir 选择 TreeLock 或 ExactPathLock 3. Copy 到新位置源还在安全 4. 如果是目录删除副本中被 cp 带过去的锁文件 5. 更新 VectorDB 中的 URI - 失败 - 清理副本源和旧索引都在一致状态 6. 删除源 7. 释放锁源码印证mv 的批量加锁通过pathlock_acquire_batch一次获取多个锁请求见 openviking/storage/viking_fs/_ops.py每个锁请求由PathLockRequest { path, kind }描述crates/ragfs/src/lock/types.rs。add_resource问题方案文件从临时目录移到正式目录后崩溃 - 文件存在但永远搜不到首次添加与增量更新分离为两条独立路径资源已落盘但语义处理/向量化还在跑时被 rm 删除 - 处理白跑生命周期 TreeLock从落盘持续到处理完成首次添加target 不存在— 在ResourceProcessor.process_resourcePhase 3.5 中处理1. 获取 TreeLock锁 final_uri - 如果 final_uri 目录不存在先检查祖先/后代/同路径锁冲突 - 无冲突则创建 final_uri 目录并在 final_uri/.path.ovlock 写 T 锁 2. 保留 temp 作为源目录入队 SemanticMsg(uritemp, target_urifinal_uri, lifecycle_lock_handle_id...) 3. DAG 在 temp 上跑完成后把 temp 内容同步到 final_uri - final_uri 已经用于放锁文件所以不做裸 agfs.mv(temp - final_uri) 4. 清理临时目录 5. DAG 启动锁刷新循环每 lock_expire/2 秒刷新锁 token 并更新 handle 活跃时间 6. DAG 完成 所有 embedding 完成 - 释放 TreeLock如果本次调用关闭了摘要和索引没有下游 DAG 接管则在同一把 TreeLock 里把 temp 目录内容复制到final_uri清理 temp然后释放锁。这里不调用VikingFS.mv(temp, final_uri, lock_handlehandle)避免移动逻辑清理目录锁文件。此期间rm尝试获取同路径 TreeLock 会失败抛出ResourceBusyError。增量更新target 已存在— temp 保持不动1. 获取 TreeLock锁 target_uri保护已有资源 2. 入队 SemanticMsg(uritemp, target_urifinal, lifecycle_lock_handle_id...) 3. DAG 在 temp 上跑启动锁刷新循环 4. DAG 完成后触发 sync_diff_callback 或 move_temp_to_target_callback 5. callback 执行完毕 - 释放 TreeLock注意DAG callback 不在外层加锁。每个VikingFS.rm和VikingFS.mv内部各自有独立锁保护。外层锁会与内部锁冲突导致死锁。首次添加和增量更新都只持有TreeLock(resource_dir)。这里不再做ExactPathLock(resource_dir) - TreeLock(resource_dir)的锁转交避免两种锁复用同一个.path.ovlock时出现释放顺序错误。自动命名由资源层处理不属于锁服务ResourceProcessor先用exists(candidate_uri)判断候选目录是否已占用已存在则尝试_1、_2后缀。候选目录不存在时才尝试获取该目录的TreeLock且不等待如果同名正在被并发请求处理就直接尝试下一个后缀。服务重启恢复SemanticMsg 持久化在 QueueFS 中。重启后SemanticProcessor发现lifecycle_lock_handle_id对应的 handle 不在内存中会重新获取 TreeLock。这一机制在 openviking/storage/queuefs/semantic_lock.py 的SemanticLockScope.resolve中有完整实现当pathlock_adopt(lock_handoff)因LockAcquisitionError失败例如原持有者进程已崩溃、锁已过期时会从 handoff 的lock_paths中解析出 TreeLock 目录路径并通过pathlock_acquire_tree或pathlock_acquire_tree_batch重新获取锁恢复对资源的生命周期保护。派生语义文件.abstract.md / .overview.md.abstract.md和.overview.md是后台生成的派生文件不作为普通用户源文件写入。它们的并发保护分两层问题方案多个后台任务同时刷新同一个目录摘要旧结果覆盖新结果相同 dirty key 使用coalesce_version只有最新版本允许写回最新任务写回派生文件时与另一个写回交错写.abstract.md、.overview.md前获取各自的 ExactPathLock例子同一目录下并发写入a.md、b.md、c.md时前台写入分别持有ExactPathLock(a.md)、ExactPathLock(b.md)、ExactPathLock(c.md)互不阻塞。后台可能产生多个docs/摘要刷新任务但只有最新 version 能写回docs/.overview.md和docs/.abstract.md旧任务在写回前发现自己过期后直接丢弃结果。memory 目录摘要使用同一规则。比如并发更新viking://user/default/memories/preferences/theme.md viking://user/default/memories/preferences/editor.md两个文件写入各自持有 ExactPathLockpreferences/.overview.md和preferences/.abstract.md的后台刷新不再持有长时间 TreeLock而是通过coalesce_version淘汰旧任务并在最终写派生文件时短暂获取 ExactPathLock。session.commit()问题方案消息已清空但 archive 未写入 - 对话数据丢失Phase 1 无锁archive 不完整无副作用 Phase 2 持久化session_commit队列LLM 调用耗时不可控5s~60s不能放在持锁操作内。设计拆为两个阶段Phase 1 — 归档无锁 1. 生成归档摘要LLM 2. 写 archivehistory/archive_N/messages.jsonl 摘要 3. 清空 messages.jsonl 4. 清空内存中的消息列表 Phase 2 — 记忆提取 写入持久化 session_commit 队列 1. 持久化 archive 元数据并 enqueue SessionCommitMsg 2. 从归档消息提取 memoriesLLM 3. 写当前消息状态 4. 直接 enqueue SemanticQueue崩溃恢复分析崩溃时间点状态恢复动作Phase 1 写 archive 中途队列未发布archive 不完整下次 commit 从 history/ 扫描 index不受影响Phase 1 archive 完成但 messages 未清空队列未发布archive 完整 messages 仍在 数据冗余但安全Phase 2 记忆提取/写入中途session_commit任务仍在持久化队列中重启后继续消费该任务从 archive 恢复 Phase 2Phase 2 完成archive 标记为完成无需恢复LockContext 使用方式LockContext是异步上下文管理器封装锁的获取和释放# Conceptual example: production path locks are acquired inside the Rust ragfs layer. # Exact 锁写操作、语义处理 async with LockContext(lock_manager, [path], lock_modeexact): # 执行操作... pass # Tree 锁删除目录、目录生命周期保护 async with LockContext(lock_manager, [path], lock_modetree): # 执行操作... pass # MV 锁移动操作 async with LockContext(lock_manager, [src], lock_modemv, mv_dst_pathdst): # 执行操作... pass锁模式lock_mode用途行为exact文件写入、单文件删除、派生文件写回锁定指定路径与同路径锁和祖先目录 TreeLock 冲突tree删除目录、资源生命周期、目录级保护锁定子树根节点与同路径锁、后代锁和祖先 TreeLock 冲突mv移动操作目录移动源路径 TreeLock 目标路径 ExactPathLock文件移动源路径和目标路径均 ExactPathLock通过src_is_dir控制异常处理__aexit__总是释放锁不吞异常。获取锁失败时抛出LockAcquisitionError该异常定义于 openviking/storage/errors.py 同模块并在 semantic_lock 中被捕获用于恢复流程。锁类型EXACT 与 TREE 的冲突矩阵锁机制使用两种锁类型来处理不同的冲突场景同路径 EXACT同路径 TREE后代 EXACT祖先 TREEEXACT冲突冲突—冲突TREE冲突冲突冲突冲突EXACT (E)锁定一个具体路径本身。文件、目录名、尚未创建的目标路径都可以使用若祖先目录持有 TreeLock 则阻塞。TREE (T)用于删除目录、移动目录、资源生命周期保护等。逻辑上覆盖整棵子树但只在根目录写一个锁文件。获取前扫描所有后代和祖先目录确认无冲突锁。目标目录不存在时先做冲突检查无冲突才创建目录并写锁。若创建后又发现并发冲突本次加锁失败但不回滚刚创建出来的空目录。源码印证锁覆盖判定实现在 crates/ragfs/src/lock/types.rs 的request_is_covered_by中——EXACT 只覆盖规范化后完全相同的路径TREE 覆盖自身及所有后代path_is_self_or_descendantEXACT 无法覆盖 TREE 请求。这与上表完全一致。锁机制深入锁协议锁文件路径TreeLock(path) - {path}/.path.ovlock ExactPathLock(已存在目录 path) - {path}/.path.ovlock ExactPathLock(文件或未创建路径) - {parent}/.exact.ovlock.name.hash锁文件内容Fencing Token{handle_id}:{time_ns}:{lock_type}其中lock_type为EEXACT或TTREE。这与 Rust 侧LockToken { owner_id, time_ns, lock_type }的结构一一对应owner_id即 handle_id纳秒时间戳用于陈旧锁判定与 TOCTOU 冲突仲裁。获取锁流程EXACT 模式循环直到超时轮询间隔200ms 1. 检查目标路径是否被其他操作锁定 - 陈旧锁 - 移除后重试 - 活跃锁 - 等待 2. 检查所有祖先目录是否有 TREE 锁 - 陈旧锁 - 移除后重试 - 活跃锁 - 等待 3. 确保锁文件所在父目录存在如果不存在则创建目录 4. 写入 EXACT (E) 锁文件 5. TOCTOU 双重检查重新扫描目标路径和祖先目录的 TREE 锁 - 发现冲突比较 (timestamp, handle_id) - 后到者更大的 timestamp/handle_id主动让步删除自己的锁防止活锁 - 等待后重试 6. 验证锁文件归属fencing token 匹配 7. 成功 超时默认 0 不等待抛出 LockAcquisitionError获取锁流程TREE 模式循环直到超时轮询间隔200ms 1. 检查目标路径是否被其他操作锁定 - 陈旧锁 - 移除后重试 - 活跃锁 - 等待 2. 检查所有祖先目录是否有 TREE 锁 - 陈旧锁 - 移除后重试 - 活跃锁 - 等待 3. 扫描所有后代目录检查是否有其他操作持有的锁 - 目标目录不存在 - 视为无后代锁 - 陈旧锁 - 移除后重试 - 活跃锁 - 等待 4. 确保目标目录存在如果不存在则创建目录 5. 写入 TREE (T) 锁文件只写一个文件在根路径 6. TOCTOU 双重检查重新扫描后代目录和祖先目录 - 发现冲突比较 (timestamp, handle_id) - 后到者更大的 timestamp/handle_id主动让步删除自己的锁防止活锁 - 等待后重试 7. 验证锁文件归属fencing token 匹配 8. 成功 超时默认 0 不等待抛出 LockAcquisitionError源码细节Rust 侧PathLockManagercrates/ragfs/src/lock/manager.rs的轮询采用带抖动jitter的退避策略——首次轮询不低于 50ms随后以 3/2 倍数退避并叠加 ±20% 抖动上限 500ms用于分散等待者、避免惊群。文档中的 200ms 是概念上的平均轮询间隔实际实现以该退避区间为准。缺失目录创建规则锁系统允许为了放置锁文件而创建目录但创建前必须先检查冲突1. 发现祖先 TreeLock / 同路径锁 / 后代锁冲突 - 不创建目录直接失败或等待 2. 当前无冲突 - 可以创建目录并写锁 3. 写锁后再次检查时发现新冲突 - 删除自己的锁并失败或重试 4. 第 3 步不会回滚刚创建的空目录例子请求 A 正在删除 viking://resources/books A 持有 TreeLock(/resources/books) 请求 B 想添加 viking://resources/books/java-guide B 在创建 java-guide 目录前发现祖先 TreeLock B 不创建目录返回 busy如果两个请求同时创建java-guide两边都可能先看到当前无冲突但最终只有 fencing token 校验通过的一方成功持有TreeLock(java-guide)失败方会删除自己的锁已创建出来的空目录可以保留。锁过期清理陈旧锁检测PathLockEngine 检查 fencing token 中的时间戳。超过lock_expire默认 30s的锁被视为陈旧锁在加锁过程中自动移除。进程内清理LockManager 每 60 秒检查活跃的 LockHandle。仍持有锁文件且失活时间超过lock_expire的 handle 会被强制释放。孤儿锁进程崩溃后遗留的锁文件在下次任何操作尝试获取同一路径锁时通过 stale lock 检测自动移除。崩溃恢复服务启动后QueueManager 会继续消费持久化的session_commit任务场景恢复方式session_memory 提取中途崩溃从 archive 恢复 Phase 2 并继续消费session_commit任务锁持有期间崩溃锁文件留在 AGFS下次获取时 stale 检测自动清理默认 30s 过期enqueue 后 worker 处理前崩溃QueueFS SQLite 持久化worker 重启后自动拉取孤儿索引L2 按需加载时清理从源码看SessionCommitProcessor的恢复链路为QueueManager 启动轮询SessionCommit队列默认并发 8、轮询间隔 1.0s→on_dequeue把任务调度到服务事件循环 →_process调用session.resume_queued_commit(msg)完成 Phase 2处理失败会重新 enqueue处理取消则调用finalize_cancelled_commit收尾见 openviking/storage/queuefs/session_commit_processor.py。防线总结异常场景防线恢复时机操作中途崩溃锁自动过期 stale 检测下次获取同路径锁时add_resource 语义处理中途崩溃生命周期锁过期 SemanticProcessor 重启时重新获取worker 重启后session.commit Phase 2 崩溃持久化session_commit队列 重试消费重启时enqueue 后 worker 处理前崩溃QueueFS SQLite 持久化worker 重启后孤儿索引L2 按需加载时清理用户访问时配置说明路径锁默认启用无需额外配置。推荐通过storage.agfs.pathlock配置过期时间。运行时等待超时固定为0.0秒不再接受外部配置。storage.transaction仅保留为兼容旧配置lock_timeout已废弃且会被忽略lock_expire会在未显式配置新字段时自动映射redo_recovery_enabled已废弃且会被忽略。推荐写法{ storage: { agfs: { pathlock: { lock_expire_secs: 30.0 } } } }兼容旧写法{ storage: { transaction: { lock_expire: 30.0 } } }参数类型说明默认值lock_timeoutfloat已废弃且忽略。运行时等待超时固定为0.0。0.0lock_expirefloat已废弃。改用storage.agfs.pathlock.lock_expire_secs。30.0源码印证Rust 侧PathLockConfigcrates/ragfs/src/lock/manager.rs的默认值正是lock_timeout_secs: 0.0与lock_expire_secs: 30.0provider 默认为filesystem与上述配置说明完全对应。QueueFS 持久化路径锁机制依赖 QueueFS 使用 SQLite 后端确保 enqueue 的任务在进程重启后可恢复。这是默认配置无需手动设置。总结OpenViking 用路径锁 持久化队列两个简单原语替换了传统数据库复杂的 redo log / undo log 机制路径锁解决并发写互斥fencing token 解决 TOCTOU 竞争stale 检测解决进程崩溃遗留持久化session_commit队列解决耗时 LLM 阶段的两阶段提交恢复。整体设计始终遵循宁可搜不到不要搜到坏结果的原则——源数据永远优先可恢复索引作为派生品可以随时重建。相关文档架构概述 - 系统整体架构存储架构 - AGFS 和向量库会话管理 - 会话和记忆管理配置 - 配置文件说明【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考