-
Notifications
You must be signed in to change notification settings - Fork 38
feat: add --overlay flag for shared caches #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
1233f60
74f1066
ea309cb
145be6c
e774d06
1b82433
075b06e
19b06f4
dff0766
3795386
cebec25
28fe7a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -48,6 +48,7 @@ fn is_os_junk(name: &str) -> bool { | |||||||||||||||||||||
| pub struct VfsConfig { | ||||||||||||||||||||||
| pub read_only: bool, | ||||||||||||||||||||||
| pub advanced_writes: bool, | ||||||||||||||||||||||
| pub overlay: bool, | ||||||||||||||||||||||
| pub uid: u32, | ||||||||||||||||||||||
| pub gid: u32, | ||||||||||||||||||||||
| pub poll_interval_secs: u64, | ||||||||||||||||||||||
|
|
@@ -137,7 +138,7 @@ impl VirtualFs { | |||||||||||||||||||||
| let inodes = Arc::new(RwLock::new(InodeTable::new())); | ||||||||||||||||||||||
| let negative_cache = Arc::new(RwLock::new(HashMap::new())); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let flush_manager = if !config.read_only && config.advanced_writes { | ||||||||||||||||||||||
| let flush_manager = if !config.read_only && config.advanced_writes && !config.overlay { | ||||||||||||||||||||||
| let sd = staging_dir | ||||||||||||||||||||||
| .as_ref() | ||||||||||||||||||||||
| .expect("--advanced-writes requires a staging directory"); | ||||||||||||||||||||||
|
|
@@ -514,6 +515,59 @@ impl VirtualFs { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Overlay mode: merge local directory entries into the inode table. | ||||||||||||||||||||||
| // New local files are inserted; existing remote entries with the same | ||||||||||||||||||||||
| // name are overridden (local size/mtime, marked dirty so reads come | ||||||||||||||||||||||
| // from the local file). Uses the pre-mount fd to access the shadowed dir. | ||||||||||||||||||||||
| if let Some(sd) = &self.staging_dir | ||||||||||||||||||||||
| && let Some(overlay_root) = sd.overlay_root() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| let dir_path = inodes.get(parent_ino).map(|e| e.full_path.clone()).unwrap_or_default(); | ||||||||||||||||||||||
| let local_dir = overlay_root.join(&dir_path); | ||||||||||||||||||||||
| if let Ok(entries) = std::fs::read_dir(&local_dir) { | ||||||||||||||||||||||
| let now = SystemTime::now(); | ||||||||||||||||||||||
dacorvo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
| for entry in entries.flatten() { | ||||||||||||||||||||||
| let name = entry.file_name().to_string_lossy().to_string(); | ||||||||||||||||||||||
| // Use symlink_metadata to avoid following symlinks | ||||||||||||||||||||||
| let metadata = match std::fs::symlink_metadata(entry.path()) { | ||||||||||||||||||||||
| Ok(m) => m, | ||||||||||||||||||||||
| Err(_) => continue, | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| // Skip symlinks — only regular files and directories | ||||||||||||||||||||||
| if metadata.is_symlink() { | ||||||||||||||||||||||
dacorvo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
| continue; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| let kind = if metadata.is_dir() { | ||||||||||||||||||||||
| InodeKind::Directory | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| InodeKind::File | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| let full_path = inode::child_path(&dir_path, &name); | ||||||||||||||||||||||
| let mtime = metadata.modified().unwrap_or(now); | ||||||||||||||||||||||
| let size = metadata.len(); | ||||||||||||||||||||||
| let mode = { | ||||||||||||||||||||||
| use std::os::unix::fs::PermissionsExt; | ||||||||||||||||||||||
| (metadata.permissions().mode() & 0o777) as u16 | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| // insert() returns existing inode if path already exists (e.g. remote file) | ||||||||||||||||||||||
| let ino = inodes.insert( | ||||||||||||||||||||||
| parent_ino, name, full_path, kind, size, mtime, None, mode, self.uid, self.gid, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
dacorvo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
| // Always update from local file (local overrides remote). | ||||||||||||||||||||||
| // Clear xet_hash so unlink/rename won't send stale remote ops. | ||||||||||||||||||||||
| if let Some(e) = inodes.get_mut(ino) { | ||||||||||||||||||||||
| e.size = size; | ||||||||||||||||||||||
| e.mtime = mtime; | ||||||||||||||||||||||
dacorvo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
| e.xet_hash = None; | ||||||||||||||||||||||
| e.set_dirty(); | ||||||||||||||||||||||
| if kind == InodeKind::Directory { | ||||||||||||||||||||||
| e.children_loaded = false; // will be lazy-loaded on access | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if let Some(parent) = inodes.get_mut(parent_ino) { | ||||||||||||||||||||||
| parent.children_loaded = true; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
@@ -860,6 +914,17 @@ impl VirtualFs { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // Advanced-writes: flush staging file to Hub immediately. | ||||||||||||||||||||||
| // In overlay mode, sync the local fd for durability but skip remote upload. | ||||||||||||||||||||||
| if self.staging_dir.as_ref().is_some_and(|sd| sd.is_overlay()) { | ||||||||||||||||||||||
| let files = self.open_files.read().expect("open_files poisoned"); | ||||||||||||||||||||||
| if let Some(OpenFile::Local { file, .. }) = files.get(&file_handle) { | ||||||||||||||||||||||
| file.sync_all().map_err(|e| { | ||||||||||||||||||||||
| error!("Overlay fsync failed for ino={}: {}", ino, e); | ||||||||||||||||||||||
| libc::EIO | ||||||||||||||||||||||
| })?; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
dacorvo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
| // Note: if the flush_loop is concurrently processing this inode, both may | ||||||||||||||||||||||
| // upload the same content. This is benign (idempotent commit, generation-aware | ||||||||||||||||||||||
| // clear ensures only one clears dirty). | ||||||||||||||||||||||
|
|
@@ -931,7 +996,10 @@ impl VirtualFs { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let file_entry = self.get_file_entry(ino)?; | ||||||||||||||||||||||
| let staging_path = self.staging_dir.as_ref().map(|sd| sd.path(ino)); | ||||||||||||||||||||||
| let staging_path = self | ||||||||||||||||||||||
| .staging_dir | ||||||||||||||||||||||
| .as_ref() | ||||||||||||||||||||||
| .map(|sd| sd.staging_path(ino, &file_entry.full_path)); | ||||||||||||||||||||||
dacorvo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if writable && self.advanced_writes { | ||||||||||||||||||||||
| // Staging file + async flush (supports random writes and seek) | ||||||||||||||||||||||
|
|
@@ -959,6 +1027,11 @@ impl VirtualFs { | |||||||||||||||||||||
| ) -> VirtualFsResult<u64> { | ||||||||||||||||||||||
| let staging_path = staging_path.expect("staging_dir required for advanced writes"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Overlay paths may need parent directories created | ||||||||||||||||||||||
| if let Some(parent) = staging_path.parent() { | ||||||||||||||||||||||
| let _ = std::fs::create_dir_all(parent); | ||||||||||||||||||||||
dacorvo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Serialize staging preparation per inode (prevents concurrent download races) | ||||||||||||||||||||||
| let staging_mutex = self.staging_lock(ino); | ||||||||||||||||||||||
| let _staging_guard = staging_mutex.lock().await; | ||||||||||||||||||||||
|
|
@@ -1867,7 +1940,11 @@ impl VirtualFs { | |||||||||||||||||||||
| .staging_dir | ||||||||||||||||||||||
| .as_ref() | ||||||||||||||||||||||
| .expect("staging_dir required for advanced writes") | ||||||||||||||||||||||
| .path(ino); | ||||||||||||||||||||||
| .staging_path(ino, &full_path); | ||||||||||||||||||||||
| // Overlay paths may need parent directories created | ||||||||||||||||||||||
| if let Some(parent) = staging_path.parent() { | ||||||||||||||||||||||
| let _ = std::fs::create_dir_all(parent); | ||||||||||||||||||||||
|
||||||||||||||||||||||
| let _ = std::fs::create_dir_all(parent); | |
| if let Err(e) = std::fs::create_dir_all(parent) { | |
| error!( | |
| "Failed to create staging directory {}: {}", | |
| parent.display(), | |
| e | |
| ); | |
| self.inode_table.write().expect("inodes poisoned").remove(ino); | |
| return Err(libc::EIO); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 9cc17e2: error is now propagated, inode cleaned up on failure.
Uh oh!
There was an error while loading. Please reload this page.