Repository navigation
TSI-4326: fix(fuse): 目录改名后立即失效旧投影名 - #165
Conversation
Reviewer's guide (collapsed on small PRs)Reviewer's GuideAfter a directory rename is persisted successfully, the FUSE service parses the old source path and explicitly invalidates its old Sequence diagram for immediate invalidation after directory renamesequenceDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
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
tsic404
left a comment
There was a problem hiding this comment.
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 过期。
c42029b to
bda60c1
Compare
tsic404
left a comment
There was a problem hiding this comment.
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 未使其变差。
无法锚定的发现:无。
Closes TSI-4326
Issue: https://multica.tsic.top/issues/01a17467-13bf-7789-91ab-773ee46df9b3
设计依据
tsic/torrentfs-design@5b622b6
设计实现
src/fuse/fs_service.rs:1623·rename目录分支落库成功后调用inval_data_entry(dir_parent, dir_name)split_data_dir_parent(&old_source_path)拆成(旧父目录 inode,旧名),顶层、嵌套与跨父目录的旧路径都落在旧父目录下tests/acceptance/tc_ingest_03.sh(分支 headc7d7fb6)验证;本 PR 不改该文件src/fuse/fs_service.rs:108·EntryInvalidator注入点;src/fuse/fs_service.rs:5876·rename_dir_invalidates_the_old_projection_entry断言跨父改名、移到根、根级改名三例只失效「旧父目录 + 旧名」data/接受变更操作data/增可写路径,新增抽象只承载内核失效通知验证
单测判别力
cargo test(跳过沙箱 tmpfs 配额不足的 1 GiB 用例):left: [],right: [(4432243, "tree")]&new_source_pathleft: [(4750922, "sub")],right: [(4432243, "tree")]探针 A/B 都在新用例上转红,其余 456 条单测保持通过——判别力来自新用例,不是噪音。
挂载形态
tests/acceptance/tc_ingest_03.sh(TSI-4317 headc7d7fb6):main基线f5d011b):FAIL — data/renamedir still resolves right after the directory rename (stale dentry)bda60c1):PASS (seed rename/delete, subtree delete and directory rename reflect immediately)约束
tests/acceptance/tc_ingest_03.sh/test_tc_ingest_03.rs(归属 TSI-4317: test(acceptance): 补齐种子接入投影的六条验收载体 #160)。#[cfg(test)]生产路径特判:EntryInvalidator是生产抽象,测试实现放在既有#[cfg(test)] mod tests。data/可写面未变(01-scope F1)。相邻缺陷(不在本单范围)
跨父目录目录移动后
rename_metadata_directory不更新parent_id,归 TSI-4327。环境说明
fusermount3;挂载验证在unshare -Urm内由mount(2)完成,fuser 的fusermount通信协议由一个一次性本地 shim 承接,未入库。cargo clippy在本环境因 clippy 0.1.99 与 rustc 1.98.1 不匹配不可用(E0463,既有环境问题);格式门禁cargo fmt --check对改动文件零差异。