From a7efc5665f4665f1b77ed1d22d2bd03e77a4663f Mon Sep 17 00:00:00 2001 From: baojun-zhang Date: Tue, 13 Oct 2026 20:45:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(cache):=20=E7=BB=9F=E4=B8=80=20cache?= =?UTF-8?q?=20key=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ragfs/src/cache/mod.rs | 2 +- crates/ragfs/src/cache/wrapper.rs | 34 +++++------------------ crates/ragfs/src/core/builder.rs | 8 +++--- crates/ragfs/src/core/mountable.rs | 37 +++++++++++--------------- crates/ragfs/src/lock/cache_provder.rs | 4 +-- docs/en/concepts/09-transaction.md | 8 +++--- docs/en/guides/14-ragfs-cache.md | 14 +++++----- docs/zh/concepts/09-transaction.md | 8 +++--- docs/zh/guides/14-ragfs-cache.md | 14 +++++----- 9 files changed, 52 insertions(+), 77 deletions(-) diff --git a/crates/ragfs/src/cache/mod.rs b/crates/ragfs/src/cache/mod.rs index 6c3ca10e45..516d1b6262 100644 --- a/crates/ragfs/src/cache/mod.rs +++ b/crates/ragfs/src/cache/mod.rs @@ -11,4 +11,4 @@ mod wrapper; pub use metrics::{CacheMetrics, CacheMetricsSnapshot}; pub use policy::{CacheDecision, CachePolicy, CacheTraversalMode, CacheTreeMode}; -pub use wrapper::{CacheNamespace, CachedFileSystem}; +pub use wrapper::CachedFileSystem; diff --git a/crates/ragfs/src/cache/wrapper.rs b/crates/ragfs/src/cache/wrapper.rs index b6ee651982..031fd61ace 100644 --- a/crates/ragfs/src/cache/wrapper.rs +++ b/crates/ragfs/src/cache/wrapper.rs @@ -26,26 +26,6 @@ use uuid::Uuid; const GREP_CACHE_FILE_CONCURRENCY: usize = 8; const GENERATION_PUT_CONCURRENCY: usize = 8; -/// Namespace prepended to every provider key owned by one wrapper. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CacheNamespace { - value: String, -} - -impl CacheNamespace { - /// Create a namespace from a mount-, account-, or application-specific value. - pub fn new(value: impl Into) -> Self { - Self { - value: value.into(), - } - } - - /// Return the namespace text. - pub fn as_str(&self) -> &str { - &self.value - } -} - /// A provider-independent read-through filesystem cache. /// /// Construction is explicit. RAGFS does not install this wrapper in the @@ -53,7 +33,7 @@ impl CacheNamespace { pub struct CachedFileSystem { backend: Box, runtime: Arc, - namespace: CacheNamespace, + service_name: String, policy: CachePolicy, metrics: Arc, operation_lock: RwLock<()>, @@ -68,22 +48,22 @@ impl CachedFileSystem { pub fn with_runtime( backend: Box, runtime: Arc, - namespace: CacheNamespace, + service_name: impl Into, policy: CachePolicy, ) -> Self { - Self::build(backend, runtime, namespace, policy) + Self::build(backend, runtime, service_name.into(), policy) } fn build( backend: Box, runtime: Arc, - namespace: CacheNamespace, + service_name: String, policy: CachePolicy, ) -> Self { Self { backend, runtime, - namespace, + service_name, policy, metrics: Arc::new(CacheMetrics::default()), operation_lock: RwLock::new(()), @@ -1010,8 +990,8 @@ impl CachedFileSystem { fn object_key(&self, kind: &str, path: &str) -> String { let normalized = normalize_path(path); format!( - "ragfs:v1:{}:{}:{:016x}", - self.namespace.as_str(), + "{}:ov:cache:v1:{}:{:016x}", + self.service_name, kind, stable_hash(normalized.as_bytes()) ) diff --git a/crates/ragfs/src/core/builder.rs b/crates/ragfs/src/core/builder.rs index 5fcb858d67..4503320813 100644 --- a/crates/ragfs/src/core/builder.rs +++ b/crates/ragfs/src/core/builder.rs @@ -16,18 +16,18 @@ use super::stats_wrapper::StatsWrappedFS; use super::errors::{Error, Result}; #[cfg(feature = "cache")] -use crate::cache::{CacheNamespace, CachePolicy}; +use crate::cache::CachePolicy; #[cfg(feature = "cache")] use crate::cache_runtime::{ CacheOperation, CacheRuntime, DynamicProviderConfig, RedisProviderConfig, }; +#[cfg(feature = "cache")] +use crate::lock::RedisPathLockProvider; use crate::lock::{ FilesystemPathLockProvider, MemoryPathLockProvider, PathLockConfig, PathLockManager, PathLockProvider, PathLockWrappedFS, }; -#[cfg(feature = "cache")] -use crate::lock::RedisPathLockProvider; #[cfg(feature = "s3")] use crate::plugins::S3FSPlugin; @@ -198,7 +198,7 @@ pub async fn build_configured_stack( .as_ref() .expect("cache provider is validated") .clone(), - CacheNamespace::new(&cache.cachefs.service_name), + cache.cachefs.service_name, cache.cachefs.policy, )) } else if let Some(runtime) = runtime.as_ref() { diff --git a/crates/ragfs/src/core/mountable.rs b/crates/ragfs/src/core/mountable.rs index 530f1d07ef..97565c0cfa 100644 --- a/crates/ragfs/src/core/mountable.rs +++ b/crates/ragfs/src/core/mountable.rs @@ -35,7 +35,7 @@ use super::types::{ BackendsConfig, FileInfo, GlobPage, GrepOptions, GrepResult, PluginConfig, TreeEntry, WriteFlag, }; #[cfg(feature = "cache")] -use crate::cache::{CacheNamespace, CachePolicy, CacheTraversalMode, CachedFileSystem}; +use crate::cache::{CachePolicy, CacheTraversalMode, CachedFileSystem}; #[cfg(feature = "cache")] use crate::cache_runtime::CacheRuntime; @@ -95,7 +95,7 @@ pub struct MountableFS { #[derive(Clone)] struct MountCacheConfig { runtime: Arc, - namespace: CacheNamespace, + service_name: String, policy: CachePolicy, } @@ -251,7 +251,7 @@ impl MountableFS { #[cfg(feature = "cache")] pub fn with_cache_runtime( runtime: Arc, - namespace: CacheNamespace, + service_name: impl Into, policy: CachePolicy, ) -> Self { Self { @@ -262,7 +262,7 @@ impl MountableFS { pathlock_manager: OnceLock::new(), cache: Some(MountCacheConfig { runtime: runtime.clone(), - namespace, + service_name: service_name.into(), policy, }), cache_runtime: Some(runtime), @@ -367,7 +367,7 @@ impl MountableFS { let storage_fs = if is_control_plugin { raw_arc.clone() } else { - self.maybe_wrap_cache(raw_arc.clone(), &normalized_path) + self.maybe_wrap_cache(raw_arc.clone()) }; #[cfg(not(feature = "cache"))] let storage_fs = raw_arc.clone(); @@ -422,7 +422,7 @@ impl MountableFS { Some(cache) => Arc::new(CachedFileSystem::with_runtime( Box::new(ArcFileSystem(arc)), cache.runtime.clone(), - mount_namespace(&cache.namespace, &normalized_path), + &cache.service_name, cache .policy .clone() @@ -491,7 +491,7 @@ impl MountableFS { } #[cfg(feature = "cache")] - fn maybe_wrap_cache(&self, fs: Arc, mount_path: &str) -> Arc { + fn maybe_wrap_cache(&self, fs: Arc) -> Arc { match &self.cache { Some(cache) => { let policy = if cache.policy.traversal_mode() == CacheTraversalMode::CachedTraversal @@ -507,7 +507,7 @@ impl MountableFS { Arc::new(CachedFileSystem::with_runtime( Box::new(ArcFileSystem(fs)), cache.runtime.clone(), - mount_namespace(&cache.namespace, mount_path), + &cache.service_name, policy, )) } @@ -877,11 +877,6 @@ impl FileSystem for ArcFileSystem { } } -#[cfg(feature = "cache")] -fn mount_namespace(base: &CacheNamespace, mount_path: &str) -> CacheNamespace { - CacheNamespace::new(format!("{}:{}", base.as_str(), mount_path)) -} - impl Default for MountableFS { fn default() -> Self { Self::new() @@ -1654,13 +1649,13 @@ mod tests { #[cfg(feature = "cache")] #[tokio::test] async fn mount_wraps_backend_with_cache_when_configured() { - use crate::cache::{CacheNamespace, CachePolicy}; + use crate::cache::CachePolicy; use crate::cache_runtime::CacheRuntime; let reads = Arc::new(AtomicU64::new(0)); let mfs = MountableFS::with_cache_runtime( CacheRuntime::memory(), - CacheNamespace::new("mount-test"), + "mount-test", CachePolicy::default(), ); mfs.register_plugin(CountingPlugin { @@ -1687,14 +1682,14 @@ mod tests { #[cfg(feature = "cache")] #[tokio::test] async fn queuefs_mount_bypasses_cache_even_when_cache_is_configured() { - use crate::cache::{CacheNamespace, CachePolicy}; + use crate::cache::CachePolicy; use crate::cache_runtime::{CacheRuntime, MemoryMockProvider}; use crate::plugins::QueueFSPlugin; let provider = Arc::new(MemoryMockProvider::new()); let mfs = MountableFS::with_cache_runtime( CacheRuntime::memory_with_provider(provider.clone()), - CacheNamespace::new("queue-cache-test"), + "queue-cache-test", CachePolicy::default().with_bypass_prefix("/queue"), ); mfs.register_plugin(QueueFSPlugin::new()).await; @@ -1832,13 +1827,13 @@ mod tests { #[cfg(feature = "cache")] #[tokio::test] async fn copy_within_mount_overwrite_invalidates_cached_destination() { - use crate::cache::{CacheNamespace, CachePolicy}; + use crate::cache::CachePolicy; use crate::cache_runtime::CacheRuntime; use crate::plugins::MemFSPlugin; let mfs = with_test_pathlock_manager(Arc::new(MountableFS::with_cache_runtime( CacheRuntime::memory(), - CacheNamespace::new("copy-cache-test"), + "copy-cache-test", CachePolicy::default(), ))) .await; @@ -1885,7 +1880,7 @@ mod tests { #[cfg(feature = "cache")] #[tokio::test] async fn encrypted_mount_caches_ciphertext_below_account_validation() { - use crate::cache::{CacheNamespace, CachePolicy}; + use crate::cache::CachePolicy; use crate::cache_runtime::{CacheRuntime, MemoryMockProvider}; use crate::core::{FsContextInner, FS_CTX}; use crate::lock::{ @@ -1897,7 +1892,7 @@ mod tests { let cache_runtime = CacheRuntime::memory_with_provider(cache_provider.clone()); let mfs = Arc::new(MountableFS::with_cache_runtime( cache_runtime.clone(), - CacheNamespace::new("encrypted-mount-test"), + "encrypted-mount-test", CachePolicy::default(), )); mfs.register_plugin(MemFSPlugin).await; diff --git a/crates/ragfs/src/lock/cache_provder.rs b/crates/ragfs/src/lock/cache_provder.rs index 543eb4ed82..f81df9607d 100644 --- a/crates/ragfs/src/lock/cache_provder.rs +++ b/crates/ragfs/src/lock/cache_provder.rs @@ -509,7 +509,7 @@ impl RedisPathLockProvider { } }; Ok(format!( - "ov:pathlock:{{{}}}:{suffix}:tokens", + "{{{}}}:ov:pathlock:{suffix}:tokens", self.service_name )) } @@ -1337,7 +1337,7 @@ return redis.call("HGET", KEYS[1], ARGV[2]) let raw = runtime .execute_script(ScriptRequest { script_id: TEST_HASH_ID.to_string(), - keys: vec![format!("ov:pathlock:{{{service_name}}}:{scope}:tokens")], + keys: vec![format!("{{{service_name}}}:ov:pathlock:{scope}:tokens")], args: vec![ RedisPathLockProvider::bytes("get"), RedisPathLockProvider::bytes(path), diff --git a/docs/en/concepts/09-transaction.md b/docs/en/concepts/09-transaction.md index 68af79bd65..f0470a5ca1 100644 --- a/docs/en/concepts/09-transaction.md +++ b/docs/en/concepts/09-transaction.md @@ -275,12 +275,12 @@ value = owner_id:time_ns:lock_type HASH keys are isolated by path scope: ```text -ov:pathlock:{namespace}:global:tokens -ov:pathlock:{namespace}:scope:_system:tokens -ov:pathlock:{namespace}:scope:account:{account}:tokens +{service_name}:ov:pathlock:global:tokens +{service_name}:ov:pathlock:scope:_system:tokens +{service_name}:ov:pathlock:scope:account:{account}:tokens ``` -All keys use `{namespace}` as the Redis Cluster hash tag. Exact acquisition +All keys use `{service_name}` as the Redis Cluster hash tag. Exact acquisition reads the target and ancestors with `HMGET`; Tree acquisition scans only its scope HASH with `HGETALL`. Global `/` and `/local` locks do not scan account or `_system` HASHes. Cross-scope batches are rejected. diff --git a/docs/en/guides/14-ragfs-cache.md b/docs/en/guides/14-ragfs-cache.md index 8419ece340..f71a3db477 100644 --- a/docs/en/guides/14-ragfs-cache.md +++ b/docs/en/guides/14-ragfs-cache.md @@ -94,12 +94,12 @@ Cache-backed PathLock only supports the built-in Redis Provider. Redis HASH keys are partitioned by logical path scope: ```text -ov:pathlock:{namespace}:global:tokens -ov:pathlock:{namespace}:scope:_system:tokens -ov:pathlock:{namespace}:scope:account:{account}:tokens +{service_name}:ov:pathlock:global:tokens +{service_name}:ov:pathlock:scope:_system:tokens +{service_name}:ov:pathlock:scope:account:{account}:tokens ``` -`{namespace}` is derived from `service_name` and used as the Redis Cluster +`{service_name}` is used as the Redis Cluster hash tag, so all PathLock keys for one deployment remain in one slot. Tree conflict checks scan only the HASH for the request scope. `/` and `/local` use the global HASH but do not scan @@ -235,7 +235,7 @@ RAGFS mainly caches three object types. File keys use a stable namespace and path hash: ```text -ragfs:v1:{namespace}:file:{hash(path)} +:ov:cache:v1:file: ``` The file value is a `CacheEnvelope` containing file content, object kind, path, and generation snapshots. After a full-read cache hit, RAGFS validates the envelope and generation before returning the content. @@ -247,7 +247,7 @@ The default policy prefers summary files such as `.abstract.md` and `.overview.m Directory key: ```text -ragfs:v1:{namespace}:dir:{hash(path)} +:ov:cache:v1:dir: ``` The directory cache stores raw backend `read_dir` entries, not permission-filtered final results. Permission, role, and agent-context filtering still happens in the OpenViking upper layer at request time. @@ -259,7 +259,7 @@ This lets one directory cache object serve `ls`, `tree`, `glob`, the file-collec Subtree generation key: ```text -ragfs:v1:{namespace}:subtree:{hash(scope)} +:ov:cache:v1:subtree: ``` `remove_all` and directory `rename` can leave descendant keys behind in the Provider. RAGFS bumps the subtree generation so old envelopes fail their generation snapshot check. Later real reads fall back to the backend and rebuild the cache. diff --git a/docs/zh/concepts/09-transaction.md b/docs/zh/concepts/09-transaction.md index 44f5772374..560df5d5d0 100644 --- a/docs/zh/concepts/09-transaction.md +++ b/docs/zh/concepts/09-transaction.md @@ -272,12 +272,12 @@ value = owner_id:time_ns:lock_type HASH key 按路径 scope 隔离: ```text -ov:pathlock:{namespace}:global:tokens -ov:pathlock:{namespace}:scope:_system:tokens -ov:pathlock:{namespace}:scope:account:{account}:tokens +{service_name}:ov:pathlock:global:tokens +{service_name}:ov:pathlock:scope:_system:tokens +{service_name}:ov:pathlock:scope:account:{account}:tokens ``` -所有 key 都使用 `{namespace}` 作为 Redis Cluster hash tag。Exact 获取使用 +所有 key 都使用 `{service_name}` 作为 Redis Cluster hash tag。Exact 获取使用 `HMGET` 读取目标和祖先;Tree 获取只对所属 scope 的 HASH 执行 `HGETALL`。`/` 和 `/local` 的 global 锁不会扫描 account 或 `_system` HASH。跨 scope batch 会被拒绝。 diff --git a/docs/zh/guides/14-ragfs-cache.md b/docs/zh/guides/14-ragfs-cache.md index 174d961c0e..da3fc48515 100644 --- a/docs/zh/guides/14-ragfs-cache.md +++ b/docs/zh/guides/14-ragfs-cache.md @@ -91,12 +91,12 @@ CacheRuntime 协调多个 OpenViking 进程的路径锁。OpenViking 会从顶 Redis HASH key 按逻辑路径 scope 拆分: ```text -ov:pathlock:{namespace}:global:tokens -ov:pathlock:{namespace}:scope:_system:tokens -ov:pathlock:{namespace}:scope:account:{account}:tokens +{service_name}:ov:pathlock:global:tokens +{service_name}:ov:pathlock:scope:_system:tokens +{service_name}:ov:pathlock:scope:account:{account}:tokens ``` -`{namespace}` 来自 `service_name`,也是 Redis Cluster hash tag,因此同一部署的 PathLock key +`{service_name}` 是 Redis Cluster hash tag,因此同一部署的 PathLock key 仍位于同一 slot。Tree 冲突检查只扫描请求所属 scope 的 HASH。`/` 和 `/local` 使用 global HASH,但不会扫描 account 或 `_system` HASH。 跨 scope batch 会被拒绝。 @@ -226,7 +226,7 @@ RAGFS 主要缓存三类对象。 文件 key 使用稳定命名空间和路径 hash: ```text -ragfs:v1:{namespace}:file:{hash(path)} +:ov:cache:v1:file: ``` 文件 value 是 `CacheEnvelope`,包含文件内容、对象类型、路径和 generation 快照。全量读取命中后,RAGFS 会先校验 envelope 和 generation,再返回内容。 @@ -238,7 +238,7 @@ ragfs:v1:{namespace}:file:{hash(path)} 目录 key: ```text -ragfs:v1:{namespace}:dir:{hash(path)} +:ov:cache:v1:dir: ``` 目录缓存保存 backend 原始 `read_dir` entries,而不是权限过滤后的最终结果。权限、角色和 agent context 仍在 OpenViking 上层实时处理。 @@ -250,7 +250,7 @@ ragfs:v1:{namespace}:dir:{hash(path)} 子树 generation key: ```text -ragfs:v1:{namespace}:subtree:{hash(scope)} +:ov:cache:v1:subtree: ``` `remove_all` 和目录 `rename` 可能让 Provider 中残留子孙 key。RAGFS 通过 bump subtree generation,让旧 envelope 的 generation 快照失效,后续真实读取会回源并重建缓存。