Skip to content

feat(ragfs): add CachedFileSystem and Redis/Mooncake/Yuanrong cache providers#2520

Merged
qin-ctx merged 33 commits into
volcengine:mainfrom
tuofang:ragfs-add-cache
Jun 17, 2026
Merged

feat(ragfs): add CachedFileSystem and Redis/Mooncake/Yuanrong cache providers#2520
qin-ctx merged 33 commits into
volcengine:mainfrom
tuofang:ragfs-add-cache

Conversation

@tuofang

@tuofang tuofang commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Related Discussion

add CachedFileSystem + CacheProvider trait and Redis/Mooncake/Yuanrong Provide

Summary

This PR adds the RAGFS cache provider architecture and introduces native cache provider adapters for Mooncake and Yuanrong.

Implemented functionality:

  • Added a shared CacheProvider abstraction used by CachedFileSystem.
  • Kept file and directory cache semantics in the common cache layer:
    • file cache keys and directory cache keys
    • cache hit / miss handling
    • backend fallback and cache fill
    • write/delete/rename invalidation
    • remove_all / directory rename subtree invalidation
    • metrics, timeout, and degraded fallback behavior
  • Added feature-gated Mooncake cache adapter:
    • MooncakeProvider
    • MooncakeClient
    • MooncakeConfig
    • native Mooncake Store setup and health check
    • blocking native calls isolated behind concurrency limits
    • provider contract support for get/put/delete/batch/close
  • Added feature-gated Yuanrong cache adapter:
    • YuanrongProvider
    • YuanrongClient
    • YuanrongConfig
    • ragfs-cache-yuanrong-sys FFI crate
    • C ABI bridge over Yuanrong C++ SDK / KVClient
    • health/get/set/delete/exists/batch/flush/close support
    • timeout, error mapping, concurrency control, key hashing, and empty-value encoding
  • Added Docker-based native test environments for provider validation.
  • Default build behavior remains unchanged: native cache providers are behind features and default RAGFS behavior does not require linking Mooncake or Yuanrong.

Testing

Mooncake validation:

  • Docker Mooncake native environment built successfully.
  • Mooncake official Rust native smoke: 1/1 passed.
  • OpenViking Mooncake native smoke: 1/1 passed.
  • Mooncake Provider contract: 8/8 passed.
  • Mooncake Provider + CachedFileSystem: 5/5 passed.
  • MemoryMock + CachedFileSystem regression: 18/18 passed.
  • RAGFS default feature unit tests: 111/111 passed.
  • RAGFS default feature doc tests: 1/1 passed.
  • Static checks for this change: passed.

Yuanrong validation:

  • Default feature provider contract: 6/6 passed.
  • Default feature CachedFileSystem tests: 3/3 passed.
  • Native feature provider contract: 5/5 passed.
  • Real ETCD + Yuanrong worker CachedFileSystem tests: 4/4 passed.
  • Real ETCD + Yuanrong worker native smoke: 1/1 passed.

Covered behavior:

  • cache hit reads
  • miss fallback and cache fill
  • write-after-read correctness
  • directory cache
  • rename/delete/remove_all invalidation
  • batch get/put/delete
  • provider unavailable fallback to backend
  • close lifecycle
  • timeout and concurrency control
  • default build without native provider linking

Note: Yuanrong native validation used the currently available Docker image, whose installed openyuanrong-datasystem version is 0.6.3

压测

测试一种后端形态:

storage backend = localfs
vector db = local
cache provider = redis

目标是在后端存储和向量库都固定为本地实现的前提下,对比 cache off / cache on(redis) 对 OpenViking 业务接口的端到端收益和额外开销。

规模:

数据集 数量 文件大小 用途
small docs 1,000 1 KB - 16 KB 高频 content/readfs/stat
medium docs 500 64 KB - 512 KB 主压测读路径
large docs 100 1 MB - 8 MB 大对象 read-through/warm read
hot set 总量 20% 混合大小 hot/cold mixed 中承担 80% 请求
cold set 总量 80% 混合大小 hot/cold mixed 中承担 20% 请求

总共1600个文件

1. Workload 矩阵

阶段 Workload 目标 Users Spawn rate 时长 备注
smoke warm_read 脚本、认证、metrics 冒烟 5 5/s 2m cache off/on 各一次
baseline cold_read cache on 首次 miss 成本 32 32/s 10m cache on 前清空 Redis
benchmark warm_read 最大命中收益 64 64/s 15m 正式主场景
benchmark hot_cold 真实热冷收益 64 64/s 15m hot 80% / cold 20%
benchmark read_write write-through 成本和写后读 64 64/s 15m read 90% / write 10%
benchmark ls_tree 元数据 cache 收益 64 64/s 15m 关注 stat/list/tree
correctness invalidation 失效正确性 4 4/s 5m cache on(redis) 下执行
supplement search_supplement local vector db 稳定性补充 16 16/s 10m 不作为缓存收益主结论
sweep warm_read 并发曲线 16/32/64/128 同 users 每档 10m cache off/on 成对跑

2. 执行摘要

  • 正式 A/B 与 sweep 共 20 次运行,累计 798,792 个请求,Locust failure=0。
  • cache on 在 cold_readwarm_readhot_coldread_writels_tree 的聚合平均延迟均有改善。
  • read_write 中读请求显著改善,但写请求 P95 与 cache off 持平,平均写延迟略有增加。
  • 128 users sweep 中 Avg/P50 仍改善,但 P95 略差、P99 持平,说明高并发尾延迟不再受缓存主导。

3. 主 Workload A/B 收益

Workload RPS 变化 Avg 改善 P50 改善 P95 改善 P99 改善 状态
cold_read +0.2% +34.3% +45.7% +3.6% +8.3% PASS
warm_read -0.0% +25.8% +40.0% +6.2% -1.8% PASS
hot_cold -0.0% +2.2% +20.0% -6.9% -5.9% PASS
read_write -0.0% +20.3% +23.3% +9.6% +8.1% PASS
ls_tree +0.1% +14.5% -7.1% +24.1% +11.0% PASS
invalidation -0.1% -21.4% -66.7% +4.2% +9.4% PASS
search_supplement +0.0% +0.0% +0.0% +0.0% +0.0% NOT TESTED

延迟“改善”使用 (off - on) / off,正数表示 cache on 更快;RPS 变化使用 (on - off) / off

4. 并发 Sweep

Users Avg 改善 P50 改善 P95 改善 P99 改善
16 +29.2% +37.9% +5.9% +0.0%
32 +28.3% +42.9% +10.0% +2.7%
64 +22.0% +25.8% +2.0% -1.6%
128 +58.8% +77.4% +22.0% +6.5%

add CachedFileSystem + CacheProvider trait and Mooncake/Yuanrong Provide
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🏅 Score: 90
🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 Multiple PR themes

Sub-PR theme: Add RAGFS cache framework

Relevant files:

  • crates/ragfs/src/cache/mod.rs
  • crates/ragfs/src/cache/wrapper.rs
  • crates/ragfs/src/cache/metrics.rs
  • crates/ragfs/src/cache/policy.rs
  • crates/ragfs/src/cache/provider.rs
  • crates/ragfs/src/cache/envelope.rs
  • crates/ragfs/src/cache/memory.rs
  • crates/ragfs/tests/cache_wrapper.rs
  • crates/ragfs/src/lib.rs

Sub-PR theme: Add Mooncake cache provider

Relevant files:

  • crates/ragfs-cache-mooncake/**/*

Sub-PR theme: Add Yuanrong cache provider

Relevant files:

  • crates/ragfs-cache-yuanrong/**/*
  • crates/ragfs-cache-yuanrong-sys/**/*

⚡ No major issues detected

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle mutex poisoning for known_keys instead of unwrapping

Handle mutex poisoning for known_keys instead of using unwrap(), consistent with the
flush method. A poisoned mutex can cause unexpected panics in async code. Replace
lock().unwrap() with lock().map_err(...) to return a CacheError::Internal when the
mutex is poisoned.

crates/ragfs-cache-mooncake/src/provider.rs [82-125]

 async fn put(&self, key: &str, value: Bytes) -> CacheResult<()> {
     self.client.put(key, &value, &self.replicate).await?;
-    self.known_keys.lock().unwrap().insert(key.to_owned());
+    self.known_keys
+        .lock()
+        .map_err(|_| CacheError::Internal("Mooncake key tracker is poisoned".into()))?
+        .insert(key.to_owned());
     Ok(())
 }
 
 async fn delete_object(&self, key: &str) -> CacheResult<()> {
     if !self.client.is_exist(key).await? {
-        self.known_keys.lock().unwrap().remove(key);
+        self.known_keys
+            .lock()
+            .map_err(|_| CacheError::Internal("Mooncake key tracker is poisoned".into()))?
+            .remove(key);
         return Ok(());
     }
     match self.client.remove(key).await {
         Ok(()) => {
-            self.known_keys.lock().unwrap().remove(key);
+            self.known_keys
+                .lock()
+                .map_err(|_| CacheError::Internal("Mooncake key tracker is poisoned".into()))?
+                .remove(key);
             Ok(())
         }
         Err(error) => match self.client.is_exist(key).await {
             Ok(false) => {
-                self.known_keys.lock().unwrap().remove(key);
+                self.known_keys
+                    .lock()
+                    .map_err(|_| CacheError::Internal("Mooncake key tracker is poisoned".into()))?
+                    .remove(key);
                 Ok(())
             }
             Ok(true) | Err(_) => Err(error),
         },
     }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion adds proper mutex poisoning handling to put and delete_object methods, consistent with the existing flush method, improving robustness and preventing unexpected panics.

Medium

@Mijamind719 Mijamind719 changed the title add RAGFS CachedFileSystem feat(ragfs): add CachedFileSystem and Redis/Mooncake/Yuanrong cache providers Jun 9, 2026
@Mijamind719 Mijamind719 self-assigned this Jun 9, 2026
@baojun-zhang

Copy link
Copy Markdown
Collaborator

感谢在 Rust 文件系统中实现分布式缓存能力,这为我们的系统整体性能提供了巨大的帮助。但是能否增加使用说明文档和配置实例。如:

@Mijamind719 Mijamind719 self-requested a review June 9, 2026 02:46
@baojun-zhang baojun-zhang requested review from qin-ctx and zhoujh01 June 9, 2026 02:51
@Mijamind719

Copy link
Copy Markdown
Collaborator

Findings

P1 crates/ragfs/src/cache/wrapper.rs:139-217
current_generation() 会把 subtree generation 缓存在本地 self.generations,之后 generations_match() 也只会读这个本地值,不会再回 provider 拉最新 generation。结果是:多个 CachedFileSystem 共享同一个 provider + namespace 时,一个实例 bump 了 generation,另一个实例仍可能继续命中旧缓存。
我在最新 head 8ac5904 上加了一条临时复现测试验证过:两个 wrapper 共享同一个 MemoryCacheProvider,second.remove_all("/tree") 之后,first.read("/tree/leaf.txt") 仍返回旧值 old,不是新值 new。这会直接破坏“分布式缓存失效”的核心语义。

P2 .cargo/config.toml:1-5
这个 PR 把整个 workspace 的 crates.io 源强制改成了阿里云镜像。这个改动会影响所有开发者和 CI 的依赖解析行为,而且和“缓存层实现”本身没有直接关系,属于 repo 级环境策略,不适合跟功能 PR 一起合入。

P2 crates/ragfs-cache-yuanrong/src/client.rs:16-76 与 crates/ragfs-cache-yuanrong-sys/native/yuanrong_bridge.cpp:24-27,170,185,211,224,238,276,351,391,415
Rust 层暴露了 sdk_concurrency,看起来允许并发调用;但 C++ bridge 在 YrClientHandle 上挂了一个全局 call_mutex,所有 health/get/set/delete/exists/mget/mset/mdelete/shutdown 都被串行化了。也就是说,同一个 YuanrongProvider 实例实际是单通道过桥,sdk_concurrency 在 native 模式下并不能换来真正的后端并发。这个至少需要在代码或文档里讲清楚,否则性能预期会被高估。

Open Questions / Assumptions

PR 标题里写了 Redis/Mooncake/Yuanrong cache providers,但我在这版 diff 里没有看到 Redis adapter 或 Redis 测试代码;如果 Redis 还没进这个 PR,标题和描述现在是 over-claim。
我上次提过的 Mooncake/Yuanrong known_keys.lock().unwrap() 问题,作者这版已经修掉了,这条我不再算 finding。

@tuofang

tuofang commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Findings

P1 crates/ragfs/src/cache/wrapper.rs:139-217 current_generation() 会把 subtree generation 缓存在本地 self.generations,之后 generations_match() 也只会读这个本地值,不会再回 provider 拉最新 generation。结果是:多个 CachedFileSystem 共享同一个 provider + namespace 时,一个实例 bump 了 generation,另一个实例仍可能继续命中旧缓存。 我在最新 head 8ac5904 上加了一条临时复现测试验证过:两个 wrapper 共享同一个 MemoryCacheProvider,second.remove_all("/tree") 之后,first.read("/tree/leaf.txt") 仍返回旧值 old,不是新值 new。这会直接破坏“分布式缓存失效”的核心语义。

P2 .cargo/config.toml:1-5 这个 PR 把整个 workspace 的 crates.io 源强制改成了阿里云镜像。这个改动会影响所有开发者和 CI 的依赖解析行为,而且和“缓存层实现”本身没有直接关系,属于 repo 级环境策略,不适合跟功能 PR 一起合入。

P2 crates/ragfs-cache-yuanrong/src/client.rs:16-76 与 crates/ragfs-cache-yuanrong-sys/native/yuanrong_bridge.cpp:24-27,170,185,211,224,238,276,351,391,415 Rust 层暴露了 sdk_concurrency,看起来允许并发调用;但 C++ bridge 在 YrClientHandle 上挂了一个全局 call_mutex,所有 health/get/set/delete/exists/mget/mset/mdelete/shutdown 都被串行化了。也就是说,同一个 YuanrongProvider 实例实际是单通道过桥,sdk_concurrency 在 native 模式下并不能换来真正的后端并发。这个至少需要在代码或文档里讲清楚,否则性能预期会被高估。

Open Questions / Assumptions

PR 标题里写了 Redis/Mooncake/Yuanrong cache providers,但我在这版 diff 里没有看到 Redis adapter 或 Redis 测试代码;如果 Redis 还没进这个 PR,标题和描述现在是 over-claim。 我上次提过的 Mooncake/Yuanrong known_keys.lock().unwrap() 问题,作者这版已经修掉了,这条我不再算 finding。

1、current_generation() 改为 provider-first。provider 有 generation 时永远以 provider 为准,commit
2、已删除,commit
3、已在代码中增加注释,说明同一个 native YuanrongProvider 实例实际会在 C++ bridge 处串行化 SDK 调用,sdk_concurrency > 1 不会带来真正的 Yuanrong 后端并发,sdk_concurrency 确实能为native client 提供并发通道,可以通过nativeclientpool实现,但是不保证并发冲突写的全局顺序,commit
4、已增加RedisProvider实现,commit

@qin-ctx qin-ctx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这轮主要看新增 cache 层和现有 RAGFS 语义的边界。provider 架构、通用缓存层、native provider 隔离方向都不错,但当前 head 仍有两个需要合入前修复的问题:

  1. 加密挂载启用 cache 后,cache 包在 EncryptionWrappedFS 外层,且 key 只按 namespace/path 区分,可能把某个 account 解密后的明文提供给另一个 account 或缺失 context 的读请求。这是安全边界问题。
  2. 同 mount 的 copy_within_mount fast path 直接写 raw backend,不会经过 CachedFileSystem::write(),因此目的文件和父目录缓存不会失效,后续读可能返回旧内容。

另外,PR 的压测/描述里提到 fs/stat 或元数据 cache 收益,但当前 stat() 直接透传 backend,建议同步修正文档或实现语义。

我没有本地跑测试;GitHub 上当前 pr_review check 是通过的。

Comment thread crates/ragfs/src/core/mountable.rs Outdated
#[cfg(feature = "cache")]
fn maybe_wrap_cache(&self, fs: Arc<dyn FileSystem>, mount_path: &str) -> Arc<dyn FileSystem> {
match &self.cache {
Some(cache) => Arc::new(CachedFileSystem::new(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这里把 cache 包在已经构造好的 inner_fs 外层,而 inner_fs 在加密挂载时已经是 EncryptionWrappedFS。结果是 cache 存储和返回的是解密后的明文,但 CachedFileSystem 的 key 只包含 namespace/kind/path hash,不包含 FsContextaccount_id、root key 或任何安全域。

具体场景:tenant A 读取 /local/shared/doc.md 时 cache miss,会先经过 EncryptionWrappedFS::read() 解密,然后把明文填入 cache;tenant B 或缺失 account context 的读请求后续命中同一个 path key,就会在 cache 层直接拿到 tenant A 的明文,绕过 EncryptionWrappedFSaccount_id 校验和解密流程。

建议把 cache 放到加密层下面,只缓存 ciphertext;或者把安全域作为 cache contract 的一部分,并保证 cache hit 仍会重新应用 account/decryption 规则。这里需要补一个 encryption + shared cache provider 的回归测试:tenant A 填充 cache 后,tenant B / missing context 不能读到 tenant A 明文。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • 单 backend 改为 Stats(Encryption(Cache(Backend))),共享 cache 仅保存 ciphertext。

  • 加密 multi-write 暂停 mount-level cache,并输出 warning,避免缓存明文。

  • 新增跨 tenant、missing context、ciphertext payload 回归测试。

commit

data
}

async fn write(&self, path: &str, data: &[u8], offset: u64, flags: WriteFlag) -> Result<u64> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这个 write() 路径会删除目的文件 cache 并失效父目录,但同 mount 的 copy_within_mount fast path 不会经过这里。Python cp() 会优先调用 copy_within_mount(),而 MountableFS::copy_raw_within_mount() 直接对 raw_backend.write() 写目的文件。

因此 cache 开启时可以复现 stale read:先缓存 /local/b.md,再把 /local/a.md copy 到 /local/b.md,raw backend 已经写入新内容,但目的文件 cache 和父目录 cache 仍有效,下一次正常读 /local/b.md 可能返回旧内容。

建议在 mount/cache 边界处理这个语义:cache 开启时不要绕过 mounted filesystem stack,或者给 copy_within_mount 增加 cache-aware invalidation/copy 能力,至少在 raw copy 后失效 dst_path 和目的父目录。请补一个 overwrite copy 后再次 read/list 的回归测试。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • wrapper.rs: 给 CachedFileSystem 增加 crate 内部的 invalidate_external_write(),用于失效被外部 raw write 绕过的目的文件 cache 和父目录 cache。

  • mountable.rs: copy_within_mount() raw copy 成功后,递归从 mount stack 中找到 CachedFileSystem,对 dst_rel_path 做 cache invalidation。

  • 同文件新增回归测试 copy_within_mount_overwrite_invalidates_cached_destination,覆盖 overwrite copy 后再次 read 和 read_dir。

commit

entries
}

async fn stat(&self, path: &str) -> Result<FileInfo> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这里的 stat() 仍然直接透传 backend,没有走 cache。PR 描述和压测 workload 里提到 fs/stat / 元数据 cache 收益,容易让读者理解为 stat 已被缓存。

如果暂时不打算缓存 stat,建议把描述和 benchmark 结论改成 list/tree/read_dir 相关收益;如果 stat 是目标能力,则需要在这里补实际缓存语义和失效测试。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

stat_ls 混合元数据 workload 包含stat/ls/tree,在 cache on 下的直接 cache 收益主要来自 ls/tree 的目录缓存;
fs/stat 本身当前仍直透 backend,已修改benchmark描述

fang added 3 commits June 15, 2026 08:55
- pass runtime cache config to the Rust binding
- cache ciphertext instead of decrypted content
- disable cache for encrypted multi-write mounts
- update tests and provider build documentation
Ensure copy_within_mount invalidates the cached destination file and
parent directory after the raw backend fast path writes data directly.
This prevents stale reads and stale directory metadata when cache is
enabled and Python cp() uses the same-mount copy optimization.

Add a regression test covering overwrite copy followed by read/list.
@tuofang tuofang requested a review from qin-ctx June 15, 2026 03:34
Comment thread crates/ragfs/src/core/mountable.rs Outdated
);
arc
} else {
self.maybe_wrap_cache(arc, &normalized_path)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这里会让 cache-enabled 的未加密 multi-write mount 变成 Stats(CachedFileSystem(MultiWriteWrappedFS)),但 as_multiwrite() 目前只会解包 StatsWrappedFSEncryptionWrappedFS,不会穿过 CachedFileSystem。因此 system_sync_status() / system_sync_retry() 会误报 mounted filesystem is not multi-writeunmount() 也会跳过 MultiWriteWrappedFS::shutdown(),同挂载 copy 也不会走 copy_within_primary()。建议给 CachedFileSystem 增加一个受控的 inner FS 访问方法,并让 as_multiwrite() 解开 cache 层;同时补 cached + unencrypted multi-write 的 status/retry/unmount/copy 回归测试。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • 给 CachedFileSystem 加一个 pub(crate) 的受控 inner 访问方法,然后让 as_multiwrite() 递归穿过 cache 层;在 mountable.rs 里让递归能穿过 ArcFileSystem 回到原始 Arc
  • 增加cached + unencrypted multi-write回归测试
    commit

fang added 6 commits June 15, 2026 16:01
Allow MountableFS::as_multiwrite() to unwrap CachedFileSystem when
discovering the underlying MultiWriteWrappedFS. This preserves
multi-write admin paths and same-mount copy behavior for cache-enabled,
unencrypted multi-write mounts.

Add a regression test covering sync status, sync retry, same-mount copy,
and unmount behavior for cached unencrypted multi-write mounts.
- add configurable tree traversal mode to cache policy
- keep default tree behavior delegated to backend
- allow cached traversal to reuse read_dir directory cache
- bypass cached traversal for multi-write backends
- add regression coverage for tree cache behavior and fallbacks
Introduce a shared cache traversal mode for recursive APIs and use it to
optionally run grep through CachedFileSystem.

- add CacheTraversalMode with backend and cached_traversal modes
- keep CacheTreeMode as a compatibility alias
- route tree and grep through cached traversal only when explicitly enabled
- reuse cached read_dir entries and full-file reads during grep traversal
- keep multi-write traversal on the backend path
- expose storage.agfs.cache.traversal_mode in Python config
- raise max cached directory entries threshold to 4096
- add regression tests for grep cache traversal and traversal config
@qin-ctx qin-ctx merged commit 2d56bd7 into volcengine:main Jun 17, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in OpenViking project Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants