Files
OpenViking/tests/transaction
Kchenandchenpengfei 5f945fa8e6 fix(path_lock): (#3353)
# 并发精确路径锁下父目录幂等创建设计

## 问题

两个并发写请求分别写入不同文件时,可能需要在同一个父目录中创建精确路径锁文件。如果该父目录尚不存在,两个加锁流程都可能先观察到目录不存在,随后递归创建同一个目录。其中一个 `mkdir` 成功,另一个收到 `EEXIST`/already-exists;此时所需目录其实已经存在。

`PathLockEngine._ensure_directory_exists_async` 当前将所有 `mkdir` 异常都视为失败。因此,竞争失败一方的加锁结果为 `False`,内容写入链路最终向调用方返回 HTTP 409 `resource is busy`。

## 预期行为

并发场景下,目录创建应具有幂等语义:

- 如果 `mkdir` 抛出异常,但重新执行 `stat` 后确认目标已经是目录,则目录准备成功。
- 如果目标仍不存在、无法查询,或者目标是非目录条目,则保持原有失败行为。
- 本次修改只影响路径锁所需父目录的准备流程,不改变锁冲突、等待超时和 HTTP 错误语义。

## 实现方案

修改 `openviking/storage/transaction/path_lock.py` 中的 `PathLockEngine._ensure_directory_exists_async`:

1. 保留现有的首次 `stat` 和递归创建父目录流程。
2. 调用 `mkdir(path)` 创建当前目录。
3. 如果 `mkdir` 抛出异常,立即通过 `_is_existing_directory_async(path)` 重新查询目录状态。
4. 如果重新查询确认目标是目录,则返回成功。这样既能处理明确的 `EEXIST`,也能兼容存储后端对该错误的包装;只有在所需文件系统状态已经成立时才忽略异常。
5. 如果重新查询未确认目标是目录,则记录原始 `mkdir` 异常并返回失败。

不增加通用重试循环。如果竞争方在该路径创建的是文件而不是目录,不能将其视为成功。

## 回归测试

在 `tests/transaction/test_exact_path_lock.py` 中增加一个可稳定复现竞态的异步测试:

- 初始文件系统中存在 `/local/default/resources`,但不存在其下的共享子目录。
- 并发为共享子目录下两个不同文件获取精确路径锁,例如 `shared/a.md` 和 `shared/b.md`。
- 通过测试 AGFS 的同步点,保证两个加锁流程首次对 `shared` 执行 `stat` 时都看到目录不存在,然后才允许任一方执行 `mkdir(shared)`。
- 允许一个 `mkdir(shared)` 创建目录,另一个抛出 already-exists 异常。
- 断言两个精确路径锁都获取成功,并且创建了两个不同的锁文件。

该测试在现有实现上必须失败:竞争失败一方的 `mkdir` 异常会进入 `_ensure_directory_exists_async` 的失败分支。

## 非目标

- 不修改调用方 `upsertTextWithConflictFallback` 的行为。
- 不调整 `resource is busy` 的可重试分类。
- 不修改精确路径锁与目录树锁之间的冲突规则。
- 不为其他 AGFS 异常增加通用重试。

Co-authored-by: chenpengfei <chenpengfei@bytedance.com>
2026-07-19 19:17:11 +08:00
..