Skip to content

TSI-4326: fix(fuse): 目录改名后立即失效旧投影名 - #165

Merged
tsic404 merged 1 commit into
mainfrom
multica/TSI-4326
Oct 25, 2026
Merged

tsic404 merged 1 commit into
mainfrom
multica/TSI-4326

Conversation

@multica-tsic404

@multica-tsic404 multica-tsic404 Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes TSI-4326

Issue: https://multica.tsic.top/issues/01a17467-13bf-7789-91ab-773ee46df9b3

设计依据

tsic/torrentfs-design@5b622b6

设计实现

设计要求 代码落点
02-contracts §C-1.3.4:daemon 单方移除的条目必须主动失效内核缓存,改名后立即可见,不等 entry TTL src/fuse/fs_service.rs:1623 · rename 目录分支落库成功后调用 inval_data_entry(dir_parent, dir_name)
02-contracts §C-3.3.8:改名成功后主动失效旧名的投影目录项 同上;旧路径经 split_data_dir_parent(&old_source_path) 拆成(旧父目录 inode,旧名),顶层、嵌套与跨父目录的旧路径都落在旧父目录下
03-acceptance §A TC-ingest-03 / AC-03:改名旧名消失、新名出现;目录改名带动子树 由 TSI-4317 的挂载载体 tests/acceptance/tc_ingest_03.sh(分支 head c7d7fb6)验证;本 PR 不改该文件
测试证据:目录改名的失效目标必须可断言 src/fuse/fs_service.rs:108 · EntryInvalidator 注入点;src/fuse/fs_service.rs:5876 · rename_dir_invalidates_the_old_projection_entry 断言跨父改名、移到根、根级改名三例只失效「旧父目录 + 旧名」
01-scope 禁项:data/ 接受变更操作 未引入:未给 data/ 增可写路径,新增抽象只承载内核失效通知

验证

单测判别力

cargo test(跳过沙箱 tmpfs 配额不足的 1 GiB 用例):

变体 结果
修复态 631 passed / 0 failed / 5 ignored,新用例 ok
探针 A:删掉失效调用 456 passed / 1 failed — left: [],right: [(4432243, "tree")]
探针 B:改传 &new_source_path 456 passed / 1 failed — left: [(4750922, "sub")],right: [(4432243, "tree")]

探针 A/B 都在新用例上转红,其余 456 条单测保持通过——判别力来自新用例,不是噪音。

挂载形态

tests/acceptance/tc_ingest_03.sh(TSI-4317 head c7d7fb6):

  • 修复前(main 基线 f5d011b):FAIL — data/renamedir still resolves right after the directory rename (stale dentry)
  • 修复后(本 PR bda60c1):PASS (seed rename/delete, subtree delete and directory rename reflect immediately)

约束

相邻缺陷(不在本单范围)

跨父目录目录移动后 rename_metadata_directory 不更新 parent_id,归 TSI-4327。

环境说明

  • 沙箱没有 fuse3 的 fusermount3;挂载验证在 unshare -Urm 内由 mount(2) 完成,fuser 的 fusermount 通信协议由一个一次性本地 shim 承接,未入库。
  • cargo clippy 在本环境因 clippy 0.1.99 与 rustc 1.98.1 不匹配不可用(E0463,既有环境问题);格式门禁 cargo fmt --check 对改动文件零差异。

@sourcery-ai

sourcery-ai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

After a directory rename is persisted successfully, the FUSE service parses the old source path and explicitly invalidates its old data/ projection entry, removing stale kernel dentries immediately rather than waiting for the one-second entry TTL. Review the ordering and path decomposition, and note that cross-parent directory moves remain an unrelated database-layer issue.

Sequence diagram for immediate invalidation after directory rename

sequenceDiagram
    participant Client
    participant FsService
    participant Database
    participant KernelFUSE

    Client->>FsService: rename(old_source_path, new_source_path)
    FsService->>Database: rename_metadata_directory(...)
    Database-->>FsService: rename persisted
    FsService->>FsService: split_data_dir_parent(old_source_path)
    FsService->>KernelFUSE: inval_data_entry(dir_parent, dir_name)
    KernelFUSE-->>Client: old data/ projection disappears immediately
Loading

File-Level Changes

Change Details Files
Invalidate the old data/ projection entry after a successful directory rename so the kernel drops the stale dentry immediately.
  • Split the pre-rename source path into its parent inode and old directory name.
  • Call inval_data_entry after the metadata rename succeeds, covering top-level, nested, and same-parent renames without waiting for the FUSE entry TTL.
src/fuse/fs_service.rs
Limit the fix to cache invalidation and document validation boundaries for related rename behavior.
  • Keep the change out of data/ mutation logic.
  • Validate immediate old-name disappearance and new-name visibility through the existing acceptance harness and focused probes.
  • Identify cross-parent directory projection inconsistency as a separate database-layer defect.
src/fuse/fs_service.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/fuse/fs_service.rs" line_range="1615" />
<code_context>
+            // `data/` projection so the stale mirror name vanishes at once
+            // instead of lingering for the 1s FUSE TTL.
+            let (dir_parent, dir_name) = Self::split_data_dir_parent(&old_source_path);
+            self.inval_data_entry(dir_parent, &dir_name);
+
             Ok(())
</code_context>
<issue_to_address>
**Failed rename leaves inconsistent projection**

When the database rename fails, for example because its lock is poisoned or a database operation returns an error, the in-memory inode and data-inode paths have already been rewritten before `rename_metadata_directory` runs, so its `?` return skips this invalidation and no rollback restores the prior state. The caller receives a rename error while the daemon state and cached old projection disagree.

Persist before mutating the in-memory paths, or restore the prior state and invalidate/reconcile the projection on failure.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: src/fuse/fs_service.rs:1615


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/fuse/fs_service.rs
tsic404
tsic404 previously approved these changes Oct 25, 2026

@tsic404 tsic404 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVED — 目录改名分支补齐内核 dentry 失效,与目录删除、文件改名两条既有分支同一模式;判据 = 冻结锚点 tsic/torrentfs-design@5b622b6 的 C-1.3.4 / C-3.3.8 / AC-03 / TC-ingest-03。

规模与类型:L0,6 行新增(3 行注释 + 2 行代码 + 1 空行),1 个文件 src/fuse/fs_service.rs(hunk @@ -1608,6 +1608,12 @@,新增行 1611–1616)。Bug 修复。

通过维度

  • 正确性:失效点取旧路径 split_data_dir_parent(&old_source_path) →(旧父目录 inode,旧名)。old_source_path 在 inodes.insert(:1565)与 data_inodes 改写(:1576)之前读取(:1553),不被同一函数的就地改写污染;顶层 ("data", "a")、嵌套 (ino("a"), "inner")、跨父 (ino("a"), "tree") 三类旧路径都定位到旧父目录下的正确 dentry。
  • 放置:位于 rename_metadata_directory(...) 成功之后、Ok(()) 之前(:1599 → :1614)。DB 失败时提前 ? 返回、不发失效通知,旧名继续可解析,与尚未改写的 DB 行一致——不会把一次失败的改名变成「旧名消失」。
  • 覆盖范围:位于 if let Some(ref ts) 之外,无 DB 路径同样失效;与 rmdir 分支(:1336–1337)同构,无行为漂移。
  • 缓存自洽:data_readdir_cache 的键含 db.total_changes()(:602 / :626),改名落库即自动换代,ls data/ 不会回放旧名,本 diff 无需额外处理 daemon 侧目录缓存。
  • 安全:无新增输入面、无路径拼接、无日志敏感面;未给 data/ 增加任何可写路径(01-scope F1 保持);无 CI 环境变量特判(diff 内 6 行逐行确认)。
  • 并发:&mut self 内顺序执行一次 FUSE 通知,无新共享状态;通知失败由 inval_data_entry 既有语义降级为 warn,不影响改名返回值。
  • 测试判别力:tests/acceptance/tc_ingest_03.sh(TSI-4317 分支 head c7d7fb6)先 stat data/renamedir 固化 dentry,mv 后无 sleep 立即 test -e;删掉这 4 行即转红,证据可失败。
  • 提交信息:fix(fuse): 目录改名后立即失效旧投影名,正文写清为什么与外部可观察行为,无工单号、无署名 trailer。

点赞:注释写明了「1s entry TTL」这一 why,而非常见的复述 what;旧路径拆分复用 split_data_dir_parent,没有另造第二套解析。

其他审查者反馈:Sourcery 对 :1615 提的「DB 改名失败时 in-memory 已改写、无回滚」已在行内回应——inodes.insert / data_inodes 改写先于落库的顺序在 origin/main(f5d011b,:1565 → :1599)上原样存在,本 diff 只在落库成功之后追加失效通知,不放大该风险,也不改变失败路径的返回;属改名回滚一致性话题,建议另立单。

无法锚定的发现:无。

为什么:重命名 metadata/ 下的目录后,daemon 更新了 inode 缓存与数据库,
却没有通知内核丢弃旧 data/ 投影的 dentry;旧目录名在内核 entry TTL 内
仍可解析。文件改名与目录删除两条分支早已调用 inval_data_entry,目录改名
分支漏了同一动作。

改了什么:目录改名落库成功后,按旧 source_path 拆出父 inode 与名字,
调用内核失效;顶层、嵌套与跨父目录的旧路径都按旧父目录定位。内核失效
收口到可注入的 EntryInvalidator(生产实现是 fuser::Notifier),并新增
单测断言失效目标是旧父目录下的旧名——删掉调用或改传新路径都会转红。

外部可观察:mv metadata/<a> metadata/<b> 之后,旧 data/<a> 及其子树
立即不可解析,新 data/<b> 立即可见,不再需要等待 TTL 过期。

@tsic404 tsic404 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVED(重审 head bda60c1;上一轮 c42029b 的 APPROVE 随新提交失效,本轮覆盖新增的失效缝隙与判别力用例)

规模与类型:L1,3 文件 133 增 / 17 删(src/fuse/fs_service.rs +119/−12、src/fuse/mod.rs、src/main.rs)。Bug 修复 + 测试缝隙;判据 = 冻结锚点 tsic/torrentfs-design@5b622b6。

通过维度

  • 抽象边界:EntryInvalidator(src/fuse/fs_service.rs:108)只有 inval_entry(parent: u64, name: &OsStr) -> io::Result<()> 一个方法,不承载第二种能力,也没有附带类型/回调面。生产实现 src/fuse/mod.rs:45-49 三行转调 fuser::Notifier::inval_entry(fuser 0.16.0 的固有方法,src/notify.rs:66;路径解析优先固有实现,不会回卷到 trait 自身造成递归),参数、返回值与 inval_data_entry 的 warn 降级与改造前逐字等价。
  • 接线时机不变:src/main.rs:935 仍在 spawn_mount2 成功后、同一位置 notifier.set(Some(Arc::new(bg.notifier()))).ok();TorrentFs::notifier_handle()(mod.rs:498)仍返回同一个 Arc 句柄,服务的可见性没有提前或滞后。
  • 无 CI/测试环境特判:grep -n "cfg(test)" 在三个改动文件只命中既有 #[cfg(test)] mod tests(fs_service.rs:2507、mod.rs:948、main.rs:993)与 fs_service.rs:40 的既有测试期 import;记录型替身 RecordingInvalidator(:5847)完全位于 mod tests 内,生产路径不含任何替身分支。
  • 01-scope F1:diff 未触及 data/ 写路径(EROFS 守卫、is_data_namespace 判定零改动),新抽象只承载内核失效通知。
  • D 级判定(同意你的结论):本 crate 只有 bin 目标(Cargo.toml 无 [lib]),pub trait 与 notifier_handle() 的签名变化不构成对外 API 契约;docs/01-scope.md 的「实现自由面」明确内部派发/组合形态不由设计冻结;FUSE 面的 errno、目录树、权限、.stats 与 1s TTL 语义一概未变。属实现细化,无需 ## 设计变更 行,也无需配套设计 PR。
  • 安全:无新增输入面、无路径拼接、无日志敏感面,无环境变量特判;data/ 只读面保持。
  • 并发:EntryInvalidator: Send + Sync,生产端 fuser::Notifier 与替身均满足;失效调用仍在 &mut self 内顺序执行一次,无新共享可变状态,替身仅在测试内用 Mutex 记录。
  • 性能:删改路径上多一次虚调用,无新增分配(OsStr::new 零拷贝,Arc::new 仅在启动时一次)。
  • 测试判别力::5876 三段断言都用 (parent_ino, name) 精确相等。改传新路径(只失效新名的违约)在首段即翻红——(ino("b"), "sub") ≠ (ino("a"), "tree");第三段即使 inode 维度出问题也仍由名字维度兜住(DATA_INO + "top2" ≠ "top")。删掉调用得到空列表,首段期望非空,因此不可能“空记录真空通过”。
  • 断言稳定性:期望 inode 用与生产同一个 InodeManager::make_source_path_dir_ino 现算,两侧同源,不依赖插入顺序;SOURCE_PATH_DIR_INO_BASE = 4_000_000(值域 [4M, 5M),src/fuse/inodes.rs:26,上界由 :55 的 const assert 锁定)与 DATA_INO = 3(:18)不相交;记录顺序是单线程顺序 push,不读 HashMap 迭代序;metadata inode 来自全局 NEXT_INO,随测试并行度浮动,但断言从不引用它。
  • 提交信息:单提交 fix(fuse): 目录改名后立即失效旧投影名,正文写清为什么、改了什么与外部可观察行为,无工单号、无署名 trailer。

点赞:把“测不到”这件事当设计问题解决(缝隙只开在真正需要观测的那一个动作上),而不是给测试塞后门;新用例同时覆盖跨父、从嵌套移到根、根级同父改名三种旧父定位,正好锁住 split_data_dir_parent 的三条分支。

其他审查者反馈:新 head 上没有新的审查意见(Sourcery 本轮 skipping);其内联线程(现锚在 :1624,内容仍是「DB 改名失败时 in-memory 已改写、无回滚」)上一轮已逐条回应,本轮代码未改动该顺序(inodes.insert :1574 → data_inodes 改写 :1585 → 落库 :1608 → 失效 :1623-1624),结论不变。

覆盖边界(陈述,非 finding)::5876 验证的是 daemon「决定失效谁」,不是 fuser::Notifier 的实际投递;该转发由挂载载体复跑覆盖。CI 的 P0 集(tc_ingest_01 / tc_rofs_01 / tc_read_04 / tc_stats_01 / tc_stats_02 / tc_recovery_01 / tc_cache_02)不触发 metadata/ 改名或删除,这一层在本仓 CI 仍无覆盖——与改动前相同,本 PR 未使其变差。

无法锚定的发现:无。

@tsic404
tsic404 merged commit 09ad58c into main Oct 25, 2026
15 checks passed
@tsic404
tsic404 deleted the multica/TSI-4326 branch October 25, 2026 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant