Added prefetch

This commit is contained in:
2026-03-13 17:50:28 +00:00
parent f2d42751fd
commit eaf1f549b8
8 changed files with 710 additions and 52 deletions

View File

@@ -1,7 +1,7 @@
use crate::error::{ClientError, Result};
use furumi_common::proto::{
remote_file_system_client::RemoteFileSystemClient, AttrResponse, DirEntry, FileChunk,
PathRequest, ReadRequest,
PathRequest, ReadRequest, SnapshotRequest, WatchRequest,
};
use moka::future::Cache;
use std::future::Future;
@@ -15,7 +15,7 @@ use tonic::codegen::InterceptedService;
use tonic::metadata::MetadataValue;
use tonic::transport::{Channel, Endpoint, Uri};
use tonic::{Request, Status};
use tracing::{debug, info};
use tracing::{debug, info, warn};
// ── Auth interceptor ───────────────────────────────────────────
@@ -127,6 +127,7 @@ impl tower::Service<Uri> for InsecureTlsConnector {
pub struct FurumiClient {
client: GrpcClient,
attr_cache: Cache<String, AttrResponse>,
dir_cache: Cache<String, Arc<Vec<DirEntry>>>,
}
impl FurumiClient {
@@ -187,7 +188,74 @@ impl FurumiClient {
.time_to_live(Duration::from_secs(5))
.build();
Ok(Self { client, attr_cache })
let dir_cache = Cache::builder()
.max_capacity(10_000)
.time_to_live(Duration::from_secs(30))
.build();
let this = Self { client, attr_cache, dir_cache };
this.start_background_sync();
Ok(this)
}
/// Spawns background tasks that pre-warm the cache with a server snapshot and then
/// subscribe to live change events to keep it up to date.
fn start_background_sync(&self) {
let this = self.clone();
tokio::spawn(async move {
match this.load_snapshot("/", 3).await {
Ok(n) => info!("directory snapshot loaded ({} directories)", n),
Err(e) => {
warn!("GetSnapshot unavailable (old server?): {}", e);
return;
}
}
// Reconnect loop: if the watch stream drops, reconnect after a short delay.
loop {
match this.run_watch_loop().await {
Ok(()) => break, // server closed the stream cleanly
Err(e) => {
debug!("WatchChanges disconnected: {}, reconnecting in 5s", e);
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
}
});
}
/// Fetches the server's pre-built directory snapshot and populates `dir_cache`.
/// Returns the number of directories loaded.
async fn load_snapshot(&self, path: &str, depth: u32) -> Result<usize> {
let mut client = self.client.clone();
let req = tonic::Request::new(SnapshotRequest {
path: path.to_string(),
depth,
});
let mut stream = client.get_snapshot(req).await?.into_inner();
let mut count = 0;
while let Some(entry) = stream.next().await {
let entry = entry?;
self.dir_cache
.insert(entry.path, Arc::new(entry.children))
.await;
count += 1;
}
Ok(count)
}
/// Subscribes to the server's live change events and invalidates `dir_cache` entries.
/// Returns when the stream closes or on error.
async fn run_watch_loop(&self) -> Result<()> {
let mut client = self.client.clone();
let req = tonic::Request::new(WatchRequest {});
let mut stream = client.watch_changes(req).await?.into_inner();
while let Some(event) = stream.next().await {
let event = event?;
debug!("cache invalidated (change event): {}", event.path);
self.dir_cache.invalidate(&event.path).await;
}
Ok(())
}
/// Fetches file attributes from the server, utilizing an internal cache.
@@ -207,9 +275,9 @@ impl FurumiClient {
Ok(response)
}
/// Reads directory contents from the server stream.
pub async fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
debug!("read_dir: {}", path);
/// Fetches directory contents from gRPC and stores them in the cache.
/// Does not trigger prefetch — safe to call from background tasks.
async fn fetch_and_cache_dir(&self, path: &str) -> Result<Arc<Vec<DirEntry>>> {
let mut client = self.client.clone();
let req = tonic::Request::new(PathRequest {
path: path.to_string(),
@@ -219,13 +287,65 @@ impl FurumiClient {
let mut entries = Vec::new();
while let Some(chunk) = stream.next().await {
let entry = chunk?;
entries.push(entry);
entries.push(chunk?);
}
let entries = Arc::new(entries);
self.dir_cache.insert(path.to_string(), entries.clone()).await;
Ok(entries)
}
/// Reads directory contents, utilizing an internal cache.
/// On cache miss, fetches from server and spawns a background task to
/// prefetch attributes and immediate subdirectory listings.
pub async fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
if let Some(entries) = self.dir_cache.get(path).await {
debug!("read_dir (cache hit): {}", path);
return Ok((*entries).clone());
}
debug!("read_dir (cache miss): {}", path);
let entries = self.fetch_and_cache_dir(path).await?;
let self_clone = self.clone();
let path_clone = path.to_string();
let entries_clone = entries.clone();
tokio::spawn(async move {
self_clone.prefetch_children(&path_clone, &entries_clone).await;
});
Ok((*entries).clone())
}
/// Background: warms attr_cache for all children and dir_cache for immediate subdirs.
async fn prefetch_children(&self, parent: &str, entries: &[DirEntry]) {
for entry in entries {
let child_path = if parent == "/" {
format!("/{}", entry.name)
} else {
format!("{}/{}", parent, entry.name)
};
let _ = self.get_attr(&child_path).await;
}
let subdirs: Vec<_> = entries
.iter()
.filter(|e| e.r#type == 4)
.take(20)
.collect();
for subdir in subdirs {
let child_path = if parent == "/" {
format!("/{}", subdir.name)
} else {
format!("{}/{}", parent, subdir.name)
};
if self.dir_cache.get(&child_path).await.is_none() {
let _ = self.fetch_and_cache_dir(&child_path).await;
}
}
}
/// Fetches file chunk stream from the server.
pub async fn read_file(
&self,