Added prefetch
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user