Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/ragfs/src/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
34 changes: 7 additions & 27 deletions crates/ragfs/src/cache/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,14 @@ 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<String>) -> 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
/// default mount path, so existing filesystem behavior remains unchanged.
pub struct CachedFileSystem {
backend: Box<dyn FileSystem>,
runtime: Arc<CacheRuntime>,
namespace: CacheNamespace,
service_name: String,
policy: CachePolicy,
metrics: Arc<CacheMetrics>,
operation_lock: RwLock<()>,
Expand All @@ -68,22 +48,22 @@ impl CachedFileSystem {
pub fn with_runtime(
backend: Box<dyn FileSystem>,
runtime: Arc<CacheRuntime>,
namespace: CacheNamespace,
service_name: impl Into<String>,
policy: CachePolicy,
) -> Self {
Self::build(backend, runtime, namespace, policy)
Self::build(backend, runtime, service_name.into(), policy)
}

fn build(
backend: Box<dyn FileSystem>,
runtime: Arc<CacheRuntime>,
namespace: CacheNamespace,
service_name: String,
policy: CachePolicy,
) -> Self {
Self {
backend,
runtime,
namespace,
service_name,
policy,
metrics: Arc::new(CacheMetrics::default()),
operation_lock: RwLock::new(()),
Expand Down Expand Up @@ -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())
)
Expand Down
8 changes: 4 additions & 4 deletions crates/ragfs/src/core/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
37 changes: 16 additions & 21 deletions crates/ragfs/src/core/mountable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -95,7 +95,7 @@ pub struct MountableFS {
#[derive(Clone)]
struct MountCacheConfig {
runtime: Arc<CacheRuntime>,
namespace: CacheNamespace,
service_name: String,
policy: CachePolicy,
}

Expand Down Expand Up @@ -251,7 +251,7 @@ impl MountableFS {
#[cfg(feature = "cache")]
pub fn with_cache_runtime(
runtime: Arc<CacheRuntime>,
namespace: CacheNamespace,
service_name: impl Into<String>,
policy: CachePolicy,
) -> Self {
Self {
Expand All @@ -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),
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -491,7 +491,7 @@ impl MountableFS {
}

#[cfg(feature = "cache")]
fn maybe_wrap_cache(&self, fs: Arc<dyn FileSystem>, mount_path: &str) -> Arc<dyn FileSystem> {
fn maybe_wrap_cache(&self, fs: Arc<dyn FileSystem>) -> Arc<dyn FileSystem> {
match &self.cache {
Some(cache) => {
let policy = if cache.policy.traversal_mode() == CacheTraversalMode::CachedTraversal
Expand All @@ -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,
))
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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::{
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions crates/ragfs/src/lock/cache_provder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ impl RedisPathLockProvider {
}
};
Ok(format!(
"ov:pathlock:{{{}}}:{suffix}:tokens",
"{{{}}}:ov:pathlock:{suffix}:tokens",
self.service_name
))
}
Expand Down Expand Up @@ -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),
Expand Down
8 changes: 4 additions & 4 deletions docs/en/concepts/09-transaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 7 additions & 7 deletions docs/en/guides/14-ragfs-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)}
<service_name>:ov:cache:v1:file:<hash(path)>
```

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.
Expand All @@ -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)}
<service_name>:ov:cache:v1:dir:<hash(path)>
```

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.
Expand All @@ -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)}
<service_name>:ov:cache:v1:subtree:<hash(scope)>
```

`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.
Expand Down
8 changes: 4 additions & 4 deletions docs/zh/concepts/09-transaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 会被拒绝。
Expand Down
Loading
Loading