From 4a699291c95bd588759c2858290c8c6589ce10a6 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 17:26:09 -0800 Subject: [PATCH 01/11] Add DCache struct for shared dcache management Co-Authored-By: Claude Opus 4.6 --- src/fs/mescloud/dcache.rs | 274 ++++++++++++++++++++++++++++++++++++++ src/fs/mescloud/mod.rs | 2 + 2 files changed, 276 insertions(+) create mode 100644 src/fs/mescloud/dcache.rs diff --git a/src/fs/mescloud/dcache.rs b/src/fs/mescloud/dcache.rs new file mode 100644 index 0000000..f064d78 --- /dev/null +++ b/src/fs/mescloud/dcache.rs @@ -0,0 +1,274 @@ +//! Reusable directory cache for mescloud filesystem implementations. + +use std::collections::HashMap; +use std::ffi::OsStr; +use std::time::SystemTime; + +use tracing::{trace, warn}; + +use crate::fs::r#trait::{ + CommonFileAttr, DirEntryType, FileAttr, FileHandle, FilesystemStats, Inode, Permissions, +}; + +use super::common::InodeControlBlock; + +/// Monotonically increasing inode allocator. +pub(super) struct InodeFactory { + next_inode: Inode, +} + +impl InodeFactory { + fn new(start: Inode) -> Self { + Self { next_inode: start } + } + + fn allocate(&mut self) -> Inode { + let ino = self.next_inode; + self.next_inode += 1; + ino + } +} + +/// Shared directory cache state. +/// +/// Owns inode allocation, reference counting, attribute caching, and file handle allocation. +/// Designed for composition — each filesystem struct embeds a `DCache` field. +pub(super) struct DCache { + inode_table: HashMap, + inode_factory: InodeFactory, + next_fh: FileHandle, + fs_owner: (u32, u32), + block_size: u32, +} + +impl DCache { + /// Create a new DCache. Initializes root ICB (rc=1), caches root dir attr. + pub fn new(root_ino: Inode, fs_owner: (u32, u32), block_size: u32) -> Self { + let now = SystemTime::now(); + + let mut inode_table = HashMap::new(); + inode_table.insert( + root_ino, + InodeControlBlock { + rc: 1, + parent: None, + path: "/".into(), + children: None, + attr: None, + }, + ); + + let mut dcache = Self { + inode_table, + inode_factory: InodeFactory::new(root_ino + 1), + next_fh: 1, + fs_owner, + block_size, + }; + + let root_attr = FileAttr::Directory { + common: dcache.make_common_file_attr(root_ino, 0o755, now, now), + }; + dcache.cache_attr(root_ino, root_attr); + dcache + } + + // ── Inode allocation ──────────────────────────────────────────────── + + /// Allocate a new inode number. + pub fn allocate_inode(&mut self) -> Inode { + self.inode_factory.allocate() + } + + /// Allocate a file handle (increments `next_fh` and returns the old value). + pub fn allocate_fh(&mut self) -> FileHandle { + let fh = self.next_fh; + self.next_fh += 1; + fh + } + + // ── ICB access ────────────────────────────────────────────────────── + + pub fn get_icb(&self, ino: Inode) -> Option<&InodeControlBlock> { + self.inode_table.get(&ino) + } + + pub fn get_icb_mut(&mut self, ino: Inode) -> Option<&mut InodeControlBlock> { + self.inode_table.get_mut(&ino) + } + + pub fn contains(&self, ino: Inode) -> bool { + self.inode_table.contains_key(&ino) + } + + /// Insert an ICB directly (for ensure_org_inode / ensure_repo_inode patterns). + pub fn insert_icb(&mut self, ino: Inode, icb: InodeControlBlock) { + self.inode_table.insert(ino, icb); + } + + /// Insert an ICB only if absent (for translate_*_ino_to_* patterns). + /// Returns a mutable reference to the (possibly pre-existing) ICB. + pub fn entry_or_insert_icb( + &mut self, + ino: Inode, + icb: impl FnOnce() -> InodeControlBlock, + ) -> &mut InodeControlBlock { + self.inode_table.entry(ino).or_insert_with(icb) + } + + pub fn inode_count(&self) -> usize { + self.inode_table.len() + } + + // ── Attr caching ──────────────────────────────────────────────────── + + pub fn get_attr(&self, ino: Inode) -> Option { + self.inode_table.get(&ino).and_then(|icb| icb.attr) + } + + pub fn cache_attr(&mut self, ino: Inode, attr: FileAttr) { + if let Some(icb) = self.inode_table.get_mut(&ino) { + icb.attr = Some(attr); + } + } + + // ── Reference counting ────────────────────────────────────────────── + + /// Increment rc. Panics (via unwrap) if inode doesn't exist. + pub fn inc_rc(&mut self, ino: Inode) -> u64 { + let icb = self + .inode_table + .get_mut(&ino) + .unwrap_or_else(|| unreachable!("inc_rc: inode {ino} not in table")); + icb.rc += 1; + icb.rc + } + + /// Decrement rc by `nlookups`. Returns `Some(evicted_icb)` if the inode was evicted. + pub fn forget(&mut self, ino: Inode, nlookups: u64) -> Option { + match self.inode_table.entry(ino) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + if entry.get().rc <= nlookups { + trace!(ino, "evicting inode"); + Some(entry.remove()) + } else { + entry.get_mut().rc -= nlookups; + trace!(ino, new_rc = entry.get().rc, "decremented rc"); + None + } + } + std::collections::hash_map::Entry::Vacant(_) => { + warn!(ino, "forget on unknown inode"); + None + } + } + } + + // ── Child inode management ────────────────────────────────────────── + + /// Ensure a child inode exists under `parent` with the given `name` and `kind`. + /// Reuses existing inode if present. Does NOT bump rc. + pub fn ensure_child_inode( + &mut self, + parent: Inode, + name: &OsStr, + kind: DirEntryType, + ) -> (Inode, FileAttr) { + // Check existing child by parent + name. + if let Some((&existing_ino, _)) = self + .inode_table + .iter() + .find(|&(&_ino, icb)| icb.parent == Some(parent) && icb.path.as_os_str() == name) + { + if let Some(attr) = self.inode_table.get(&existing_ino).and_then(|icb| icb.attr) { + return (existing_ino, attr); + } + + warn!(ino = existing_ino, parent, name = ?name, ?kind, + "ensure_child_inode: attr missing on existing inode, rebuilding"); + let attr = self.make_attr_for_kind(existing_ino, kind); + self.cache_attr(existing_ino, attr); + return (existing_ino, attr); + } + + let ino = self.inode_factory.allocate(); + self.inode_table.insert( + ino, + InodeControlBlock { + rc: 0, + path: name.into(), + parent: Some(parent), + children: None, + attr: None, + }, + ); + + let attr = self.make_attr_for_kind(ino, kind); + self.cache_attr(ino, attr); + (ino, attr) + } + + // ── Attr construction ─────────────────────────────────────────────── + + pub fn make_common_file_attr( + &self, + ino: Inode, + perm: u16, + atime: SystemTime, + mtime: SystemTime, + ) -> CommonFileAttr { + CommonFileAttr { + ino, + atime, + mtime, + ctime: SystemTime::UNIX_EPOCH, + crtime: SystemTime::UNIX_EPOCH, + perm: Permissions::from_bits_truncate(perm), + nlink: 1, + uid: self.fs_owner.0, + gid: self.fs_owner.1, + blksize: self.block_size, + } + } + + fn make_attr_for_kind(&self, ino: Inode, kind: DirEntryType) -> FileAttr { + let now = SystemTime::now(); + match kind { + DirEntryType::Directory => FileAttr::Directory { + common: self.make_common_file_attr(ino, 0o755, now, now), + }, + DirEntryType::RegularFile + | DirEntryType::Symlink + | DirEntryType::CharDevice + | DirEntryType::BlockDevice + | DirEntryType::NamedPipe + | DirEntryType::Socket => FileAttr::RegularFile { + common: self.make_common_file_attr(ino, 0o644, now, now), + size: 0, + blocks: 0, + }, + } + } + + // ── Filesystem stats ──────────────────────────────────────────────── + + pub fn statfs(&self) -> FilesystemStats { + FilesystemStats { + block_size: self.block_size, + fragment_size: u64::from(self.block_size), + total_blocks: 0, + free_blocks: 0, + available_blocks: 0, + total_inodes: self.inode_table.len() as u64, + free_inodes: 0, + available_inodes: 0, + filesystem_id: 0, + mount_flags: 0, + max_filename_length: 255, + } + } +} + +pub fn blocks_of_size(block_size: u32, size: u64) -> u64 { + size.div_ceil(u64::from(block_size)) +} diff --git a/src/fs/mescloud/mod.rs b/src/fs/mescloud/mod.rs index ea797a9..5927053 100644 --- a/src/fs/mescloud/mod.rs +++ b/src/fs/mescloud/mod.rs @@ -17,6 +17,8 @@ mod common; pub use common::{GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError}; use common::{InodeControlBlock, InodeFactory}; +mod dcache; + mod org; pub use org::OrgConfig; use org::OrgFs; From 66b6065e7deea1df723a3690686fbb6a9a77240d Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 17:27:11 -0800 Subject: [PATCH 02/11] Migrate RepoFs to DCache Co-Authored-By: Claude Opus 4.6 --- src/fs/mescloud/repo.rs | 155 ++++++++-------------------------------- 1 file changed, 28 insertions(+), 127 deletions(-) diff --git a/src/fs/mescloud/repo.rs b/src/fs/mescloud/repo.rs index 3aaa98e..2e558cd 100644 --- a/src/fs/mescloud/repo.rs +++ b/src/fs/mescloud/repo.rs @@ -14,7 +14,7 @@ use crate::fs::r#trait::{ LockOwner, OpenFile, OpenFlags, }; -use super::common::{self, InodeControlBlock, InodeFactory}; +use super::dcache::{self, DCache}; pub use super::common::{ GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError, }; @@ -29,12 +29,7 @@ pub struct RepoFs { repo_name: String, ref_: String, - fs_owner: (u32, u32), - - inode_table: HashMap, - inode_factory: InodeFactory, - - next_fh: FileHandle, + dcache: DCache, open_files: HashMap, } @@ -50,44 +45,14 @@ impl RepoFs { ref_: String, fs_owner: (u32, u32), ) -> Self { - let now = SystemTime::now(); - - let mut inode_table = HashMap::new(); - inode_table.insert( - Self::ROOT_INO, - InodeControlBlock { - rc: 1, - parent: None, - path: "/".into(), - children: None, - attr: None, - }, - ); - - let mut fs = Self { + Self { client, org_name, repo_name, ref_, - fs_owner, - inode_table, - inode_factory: InodeFactory::new(Self::ROOT_INO + 1), - next_fh: 1, + dcache: DCache::new(Self::ROOT_INO, fs_owner, Self::BLOCK_SIZE), open_files: HashMap::new(), - }; - - let root_attr = FileAttr::Directory { - common: common::make_common_file_attr( - fs.fs_owner, - Self::BLOCK_SIZE, - Self::ROOT_INO, - 0o755, - now, - now, - ), - }; - common::cache_attr(&mut fs.inode_table, Self::ROOT_INO, root_attr); - fs + } } /// The name of the repository this filesystem is rooted at. @@ -97,7 +62,7 @@ impl RepoFs { /// Get the cached attr for an inode, if present. pub(crate) fn inode_table_get_attr(&self, ino: Inode) -> Option { - self.inode_table.get(&ino).and_then(|icb| icb.attr) + self.dcache.get_attr(ino) } /// Build the repo-relative path for an inode by walking up the parent chain. @@ -112,7 +77,7 @@ impl RepoFs { let mut components = Vec::new(); let mut current = ino; while current != Self::ROOT_INO { - let icb = self.inode_table.get(¤t)?; + let icb = self.dcache.get_icb(current)?; components.push(icb.path.clone()); current = icb.parent?; } @@ -146,7 +111,7 @@ impl Fs for RepoFs { #[instrument(skip(self), fields(repo = %self.repo_name))] async fn lookup(&mut self, parent: Inode, name: &OsStr) -> Result { debug_assert!( - self.inode_table.contains_key(&parent), + self.dcache.contains(parent), "lookup: parent inode {parent} not in inode table" ); @@ -163,49 +128,23 @@ impl Fs for RepoFs { mesa_dev::models::Content::Dir { .. } => DirEntryType::Directory, }; - let (ino, _) = common::ensure_child_inode( - &mut self.inode_table, - &mut self.inode_factory, - self.fs_owner, - Self::BLOCK_SIZE, - parent, - name, - kind, - ); + let (ino, _) = self.dcache.ensure_child_inode(parent, name, kind); let now = SystemTime::now(); let attr = match content { mesa_dev::models::Content::File { size, .. } => FileAttr::RegularFile { - common: common::make_common_file_attr( - self.fs_owner, - Self::BLOCK_SIZE, - ino, - 0o644, - now, - now, - ), + common: self.dcache.make_common_file_attr(ino, 0o644, now, now), size, - blocks: common::blocks_of_size(Self::BLOCK_SIZE, size), + blocks: dcache::blocks_of_size(Self::BLOCK_SIZE, size), }, mesa_dev::models::Content::Dir { .. } => FileAttr::Directory { - common: common::make_common_file_attr( - self.fs_owner, - Self::BLOCK_SIZE, - ino, - 0o755, - now, - now, - ), + common: self.dcache.make_common_file_attr(ino, 0o755, now, now), }, }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.dcache.cache_attr(ino, attr); - let icb = self - .inode_table - .get_mut(&ino) - .unwrap_or_else(|| unreachable!("inode {ino} was just ensured")); - icb.rc += 1; - trace!(ino, path = ?file_path, rc = icb.rc, "resolved inode"); + let rc = self.dcache.inc_rc(ino); + trace!(ino, path = ?file_path, rc, "resolved inode"); Ok(attr) } @@ -215,25 +154,21 @@ impl Fs for RepoFs { ino: Inode, _fh: Option, ) -> Result { - let icb = self.inode_table.get(&ino).ok_or_else(|| { + self.dcache.get_attr(ino).ok_or_else(|| { warn!(ino, "getattr on unknown inode"); GetAttrError::InodeNotFound - })?; - icb.attr.ok_or_else(|| { - warn!(ino, "getattr on inode with no cached attr"); - GetAttrError::InodeNotFound }) } #[instrument(skip(self), fields(repo = %self.repo_name))] async fn readdir(&mut self, ino: Inode) -> Result<&[DirEntry], ReadDirError> { debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "readdir: inode {ino} not in inode table" ); debug_assert!( matches!( - self.inode_table.get(&ino).and_then(|icb| icb.attr), + self.dcache.get_attr(ino), Some(FileAttr::Directory { .. }) | None ), "readdir: inode {ino} has non-directory cached attr" @@ -267,15 +202,7 @@ impl Fs for RepoFs { let mut entries = Vec::with_capacity(collected.len()); for (name, kind) in &collected { - let (child_ino, _) = common::ensure_child_inode( - &mut self.inode_table, - &mut self.inode_factory, - self.fs_owner, - Self::BLOCK_SIZE, - ino, - OsStr::new(name), - *kind, - ); + let (child_ino, _) = self.dcache.ensure_child_inode(ino, OsStr::new(name), *kind); entries.push(DirEntry { ino: child_ino, name: name.clone().into(), @@ -284,27 +211,26 @@ impl Fs for RepoFs { } let icb = self - .inode_table - .get_mut(&ino) + .dcache + .get_icb_mut(ino) .ok_or(ReadDirError::InodeNotFound)?; Ok(icb.children.insert(entries)) } #[instrument(skip(self), fields(repo = %self.repo_name))] async fn open(&mut self, ino: Inode, _flags: OpenFlags) -> Result { - if !self.inode_table.contains_key(&ino) { + if !self.dcache.contains(ino) { warn!(ino, "open on unknown inode"); return Err(OpenError::InodeNotFound); } debug_assert!( matches!( - self.inode_table.get(&ino).and_then(|icb| icb.attr), + self.dcache.get_attr(ino), Some(FileAttr::RegularFile { .. }) | None ), "open: inode {ino} has non-file cached attr" ); - let fh = self.next_fh; - self.next_fh += 1; + let fh = self.dcache.allocate_fh(); self.open_files.insert(fh, ino); trace!(ino, fh, "assigned file handle"); Ok(OpenFile { @@ -333,7 +259,7 @@ impl Fs for RepoFs { ); debug_assert!( matches!( - self.inode_table.get(&ino).and_then(|icb| icb.attr), + self.dcache.get_attr(ino), Some(FileAttr::RegularFile { .. }) | None ), "read: inode {ino} has non-file cached attr" @@ -385,39 +311,14 @@ impl Fs for RepoFs { #[instrument(skip(self), fields(repo = %self.repo_name))] async fn forget(&mut self, ino: Inode, nlookups: u64) { debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "forget: inode {ino} not in inode table" ); - match self.inode_table.entry(ino) { - std::collections::hash_map::Entry::Occupied(mut entry) => { - if entry.get().rc <= nlookups { - trace!(ino, "evicting inode"); - entry.remove(); - } else { - entry.get_mut().rc -= nlookups; - trace!(ino, new_rc = entry.get().rc, "decremented rc"); - } - } - std::collections::hash_map::Entry::Vacant(_) => { - warn!(ino, "forget on unknown inode"); - } - } + self.dcache.forget(ino, nlookups); } async fn statfs(&mut self) -> Result { - Ok(FilesystemStats { - block_size: Self::BLOCK_SIZE, - fragment_size: u64::from(Self::BLOCK_SIZE), - total_blocks: 0, - free_blocks: 0, - available_blocks: 0, - total_inodes: self.inode_table.len() as u64, - free_inodes: 0, - available_inodes: 0, - filesystem_id: 0, - mount_flags: 0, - max_filename_length: 255, - }) + Ok(self.dcache.statfs()) } } From ff104bd4a209b7df52891ba35e799240ba45b6c2 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 17:29:24 -0800 Subject: [PATCH 03/11] Migrate OrgFs to DCache Co-Authored-By: Claude Opus 4.6 --- src/fs/mescloud/dcache.rs | 4 + src/fs/mescloud/org.rs | 244 +++++++++++--------------------------- 2 files changed, 72 insertions(+), 176 deletions(-) diff --git a/src/fs/mescloud/dcache.rs b/src/fs/mescloud/dcache.rs index f064d78..12cb4e1 100644 --- a/src/fs/mescloud/dcache.rs +++ b/src/fs/mescloud/dcache.rs @@ -252,6 +252,10 @@ impl DCache { // ── Filesystem stats ──────────────────────────────────────────────── + pub fn fs_owner(&self) -> (u32, u32) { + self.fs_owner + } + pub fn statfs(&self) -> FilesystemStats { FilesystemStats { block_size: self.block_size, diff --git a/src/fs/mescloud/org.rs b/src/fs/mescloud/org.rs index 5786b05..dc09b15 100644 --- a/src/fs/mescloud/org.rs +++ b/src/fs/mescloud/org.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::collections::hash_map::Entry; use std::ffi::OsStr; use std::time::SystemTime; @@ -9,7 +8,8 @@ use mesa_dev::Mesa as MesaClient; use secrecy::SecretString; use tracing::{instrument, trace, warn}; -use super::common::{self, InodeControlBlock, InodeFactory}; +use super::common::InodeControlBlock; +use super::dcache::DCache; pub use super::common::{ GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError, }; @@ -49,11 +49,8 @@ enum InodeRole { pub struct OrgFs { name: String, client: MesaClient, - fs_owner: (u32, u32), - inode_table: HashMap, - inode_factory: InodeFactory, - next_fh: FileHandle, + dcache: DCache, /// Maps org-level repo-root inodes → index into `repos`. repo_inodes: HashMap, @@ -103,31 +100,22 @@ impl OrgFs { // Check existing for (&ino, existing_owner) in &self.owner_inodes { if existing_owner == owner { - if let Some(icb) = self.inode_table.get(&ino) - && let Some(attr) = icb.attr - { + if let Some(attr) = self.dcache.get_attr(ino) { return (ino, attr); } let now = SystemTime::now(); let attr = FileAttr::Directory { - common: common::make_common_file_attr( - self.fs_owner, - Self::BLOCK_SIZE, - ino, - 0o755, - now, - now, - ), + common: self.dcache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.dcache.cache_attr(ino, attr); return (ino, attr); } } // Allocate new - let ino = self.inode_factory.allocate(); + let ino = self.dcache.allocate_inode(); let now = SystemTime::now(); - self.inode_table.insert( + self.dcache.insert_icb( ino, InodeControlBlock { rc: 0, @@ -139,63 +127,26 @@ impl OrgFs { ); self.owner_inodes.insert(ino, owner.to_owned()); let attr = FileAttr::Directory { - common: common::make_common_file_attr( - self.fs_owner, - Self::BLOCK_SIZE, - ino, - 0o755, - now, - now, - ), + common: self.dcache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.dcache.cache_attr(ino, attr); (ino, attr) } /// Get the cached attr for an inode, if present. pub(crate) fn inode_table_get_attr(&self, ino: Inode) -> Option { - self.inode_table.get(&ino).and_then(|icb| icb.attr) + self.dcache.get_attr(ino) } pub fn new(name: String, client: MesaClient, fs_owner: (u32, u32)) -> Self { - let now = SystemTime::now(); - - let mut inode_table = HashMap::new(); - inode_table.insert( - Self::ROOT_INO, - InodeControlBlock { - rc: 1, - parent: None, - path: "/".into(), - children: None, - attr: None, - }, - ); - - let mut fs = Self { + Self { name, client, - fs_owner, - inode_table, - inode_factory: InodeFactory::new(Self::ROOT_INO + 1), - next_fh: 1, + dcache: DCache::new(Self::ROOT_INO, fs_owner, Self::BLOCK_SIZE), repo_inodes: HashMap::new(), owner_inodes: HashMap::new(), repos: Vec::new(), - }; - - let root_attr = FileAttr::Directory { - common: common::make_common_file_attr( - fs.fs_owner, - Self::BLOCK_SIZE, - Self::ROOT_INO, - 0o755, - now, - now, - ), - }; - common::cache_attr(&mut fs.inode_table, Self::ROOT_INO, root_attr); - fs + } } /// Classify an inode by its role. @@ -230,7 +181,7 @@ impl OrgFs { } // Walk parents. let mut current = ino; - while let Some(parent) = self.inode_table.get(¤t).and_then(|icb| icb.parent) { + while let Some(parent) = self.dcache.get_icb(current).and_then(|icb| icb.parent) { if let Some(&idx) = self.repo_inodes.get(&parent) { return Some(idx); } @@ -259,7 +210,7 @@ impl OrgFs { // Check existing repos. for (&ino, &idx) in &self.repo_inodes { if self.repos[idx].repo.repo_name() == repo_name { - if let Some(icb) = self.inode_table.get(&ino) + if let Some(icb) = self.dcache.get_icb(ino) && let Some(attr) = icb.attr { trace!( @@ -278,22 +229,15 @@ impl OrgFs { ); let now = SystemTime::now(); let attr = FileAttr::Directory { - common: common::make_common_file_attr( - self.fs_owner, - Self::BLOCK_SIZE, - ino, - 0o755, - now, - now, - ), + common: self.dcache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.dcache.cache_attr(ino, attr); return (ino, attr); } } // Allocate new. - let ino = self.inode_factory.allocate(); + let ino = self.dcache.allocate_inode(); trace!( ino, repo = repo_name, @@ -301,7 +245,7 @@ impl OrgFs { ); let now = SystemTime::now(); - self.inode_table.insert( + self.dcache.insert_icb( ino, InodeControlBlock { rc: 0, @@ -317,7 +261,7 @@ impl OrgFs { self.name.clone(), repo_name.to_owned(), default_branch.to_owned(), - self.fs_owner, + self.dcache.fs_owner(), ); let mut bridge = HashMapBridge::new(); @@ -328,16 +272,9 @@ impl OrgFs { self.repo_inodes.insert(ino, idx); let attr = FileAttr::Directory { - common: common::make_common_file_attr( - self.fs_owner, - Self::BLOCK_SIZE, - ino, - 0o755, - now, - now, - ), + common: self.dcache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.dcache.cache_attr(ino, attr); (ino, attr) } @@ -357,8 +294,7 @@ impl OrgFs { /// Allocate an org-level file handle and map it through the bridge. fn alloc_fh(&mut self, slot_idx: usize, repo_fh: FileHandle) -> FileHandle { - let fh = self.next_fh; - self.next_fh += 1; + let fh = self.dcache.allocate_fh(); self.repos[slot_idx].bridge.insert_fh(fh, repo_fh); fh } @@ -375,29 +311,29 @@ impl OrgFs { ) -> Inode { let org_ino = self.repos[slot_idx] .bridge - .backward_or_insert_inode(repo_ino, || self.inode_factory.allocate()); + .backward_or_insert_inode(repo_ino, || self.dcache.allocate_inode()); // Ensure there's an ICB in the org table. - match self.inode_table.entry(org_ino) { - Entry::Vacant(entry) => { - trace!( - org_ino, - repo_ino, - parent = parent_org_ino, - ?name, - "translate: created new org ICB" - ); - entry.insert(InodeControlBlock { - rc: 0, - path: name.into(), - parent: Some(parent_org_ino), - children: None, - attr: None, - }); - } - Entry::Occupied(_) => { - trace!(org_ino, repo_ino, "translate: reused existing org ICB"); + let icb = self.dcache.entry_or_insert_icb(org_ino, || { + trace!( + org_ino, + repo_ino, + parent = parent_org_ino, + ?name, + "translate: created new org ICB" + ); + InodeControlBlock { + rc: 0, + path: name.into(), + parent: Some(parent_org_ino), + children: None, + attr: None, } + }); + + // Log reuse case. + if icb.rc > 0 || icb.attr.is_some() { + trace!(org_ino, repo_ino, "translate: reused existing org ICB"); } org_ino @@ -416,7 +352,7 @@ impl Fs for OrgFs { #[instrument(skip(self), fields(org = %self.name))] async fn lookup(&mut self, parent: Inode, name: &OsStr) -> Result { debug_assert!( - self.inode_table.contains_key(&parent), + self.dcache.contains(parent), "lookup: parent inode {parent} not in inode table" ); @@ -429,11 +365,7 @@ impl Fs for OrgFs { // name is an owner like "torvalds" — create lazily, no API validation. trace!(owner = name_str, "lookup: resolving github owner dir"); let (ino, attr) = self.ensure_owner_inode(name_str); - let icb = self - .inode_table - .get_mut(&ino) - .unwrap_or_else(|| unreachable!("inode {ino} was just ensured")); - icb.rc += 1; + self.dcache.inc_rc(ino); Ok(attr) } else { // Children of org root are repos. @@ -448,15 +380,11 @@ impl Fs for OrgFs { &repo.default_branch, Self::ROOT_INO, ); - let icb = self - .inode_table - .get_mut(&ino) - .unwrap_or_else(|| unreachable!("inode {ino} was just ensured")); - icb.rc += 1; + let rc = self.dcache.inc_rc(ino); trace!( ino, repo = name_str, - rc = icb.rc, + rc, "lookup: resolved repo inode" ); Ok(attr) @@ -486,11 +414,7 @@ impl Fs for OrgFs { let (ino, attr) = self.ensure_repo_inode(&encoded, repo_name_str, &repo.default_branch, parent); - let icb = self - .inode_table - .get_mut(&ino) - .unwrap_or_else(|| unreachable!("inode {ino} was just ensured")); - icb.rc += 1; + self.dcache.inc_rc(ino); Ok(attr) } InodeRole::RepoOwned { idx } => { @@ -510,16 +434,12 @@ impl Fs for OrgFs { // Rebuild attr with org inode. let org_attr = self.repos[idx].bridge.attr_backward(repo_attr); - common::cache_attr(&mut self.inode_table, org_ino, org_attr); - let icb = self - .inode_table - .get_mut(&org_ino) - .unwrap_or_else(|| unreachable!("inode {org_ino} was just cached")); - icb.rc += 1; + self.dcache.cache_attr(org_ino, org_attr); + let rc = self.dcache.inc_rc(org_ino); trace!( org_ino, repo_ino, - rc = icb.rc, + rc, "lookup: resolved content inode" ); Ok(org_attr) @@ -533,20 +453,16 @@ impl Fs for OrgFs { ino: Inode, _fh: Option, ) -> Result { - let icb = self.inode_table.get(&ino).ok_or_else(|| { + self.dcache.get_attr(ino).ok_or_else(|| { warn!(ino, "getattr on unknown inode"); GetAttrError::InodeNotFound - })?; - icb.attr.ok_or_else(|| { - warn!(ino, "getattr on inode with no cached attr"); - GetAttrError::InodeNotFound }) } #[instrument(skip(self), fields(org = %self.name))] async fn readdir(&mut self, ino: Inode) -> Result<&[DirEntry], ReadDirError> { debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "readdir: inode {ino} not in inode table" ); @@ -588,8 +504,8 @@ impl Fs for OrgFs { } let icb = self - .inode_table - .get_mut(&ino) + .dcache + .get_icb_mut(ino) .ok_or(ReadDirError::InodeNotFound)?; Ok(icb.children.insert(entries)) } @@ -625,7 +541,7 @@ impl Fs for OrgFs { self.repos[idx].repo.inode_table_get_attr(entry.ino) { let org_attr = self.repos[idx].bridge.attr_backward(repo_icb_attr); - common::cache_attr(&mut self.inode_table, org_child_ino, org_attr); + self.dcache.cache_attr(org_child_ino, org_attr); } else { trace!( repo_ino = entry.ino, @@ -642,8 +558,8 @@ impl Fs for OrgFs { } let icb = self - .inode_table - .get_mut(&ino) + .dcache + .get_icb_mut(ino) .ok_or(ReadDirError::InodeNotFound)?; Ok(icb.children.insert(org_entries)) } @@ -746,7 +662,7 @@ impl Fs for OrgFs { #[instrument(skip(self), fields(org = %self.name))] async fn forget(&mut self, ino: Inode, nlookups: u64) { debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "forget: inode {ino} not in inode table" ); @@ -762,42 +678,18 @@ impl Fs for OrgFs { } } - match self.inode_table.entry(ino) { - Entry::Occupied(mut entry) => { - if entry.get().rc <= nlookups { - trace!(ino, "evicting inode"); - entry.remove(); - // Clean up repo_inodes and owner_inodes mappings. - self.repo_inodes.remove(&ino); - self.owner_inodes.remove(&ino); - // Clean up bridge mapping — find which slot, remove. - for slot in &mut self.repos { - slot.bridge.remove_inode_by_left(ino); - } - } else { - entry.get_mut().rc -= nlookups; - trace!(ino, new_rc = entry.get().rc, "forget: decremented rc"); - } - } - Entry::Vacant(_) => { - warn!(ino, "forget on unknown inode"); + if self.dcache.forget(ino, nlookups).is_some() { + // Clean up repo_inodes and owner_inodes mappings. + self.repo_inodes.remove(&ino); + self.owner_inodes.remove(&ino); + // Clean up bridge mapping — find which slot, remove. + for slot in &mut self.repos { + slot.bridge.remove_inode_by_left(ino); } } } async fn statfs(&mut self) -> Result { - Ok(FilesystemStats { - block_size: Self::BLOCK_SIZE, - fragment_size: u64::from(Self::BLOCK_SIZE), - total_blocks: 0, - free_blocks: 0, - available_blocks: 0, - total_inodes: self.inode_table.len() as u64, - free_inodes: 0, - available_inodes: 0, - filesystem_id: 0, - mount_flags: 0, - max_filename_length: 255, - }) + Ok(self.dcache.statfs()) } } From 41631dfb56919716ec08639e5f7f5bcec1d6d134 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 17:29:59 -0800 Subject: [PATCH 04/11] Cargo.lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index fe1478b..72479cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,7 +692,7 @@ dependencies = [ [[package]] name = "git-fs" -version = "0.1.1-alpha.1" +version = "0.1.2-alpha.1" dependencies = [ "async-trait", "base64", From 0972605ae402a9da503e9c7f06672edd4d5a7e22 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 17:32:24 -0800 Subject: [PATCH 05/11] Migrate MesaFS to DCache Co-Authored-By: Claude Opus 4.6 --- src/fs/mescloud/mod.rs | 178 +++++++++++------------------------------ 1 file changed, 46 insertions(+), 132 deletions(-) diff --git a/src/fs/mescloud/mod.rs b/src/fs/mescloud/mod.rs index 5927053..1577441 100644 --- a/src/fs/mescloud/mod.rs +++ b/src/fs/mescloud/mod.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; use std::ffi::OsStr; -use std::time::SystemTime; use bytes::Bytes; use mesa_dev::Mesa as MesaClient; @@ -15,9 +14,10 @@ use crate::fs::r#trait::{ mod common; pub use common::{GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError}; -use common::{InodeControlBlock, InodeFactory}; +use common::InodeControlBlock; mod dcache; +use dcache::DCache; mod org; pub use org::OrgConfig; @@ -44,11 +44,7 @@ enum InodeRole { /// Composes multiple [`OrgFs`] instances, each with its own inode namespace, /// using [`HashMapBridge`] for bidirectional inode/fh translation at each boundary. pub struct MesaFS { - fs_owner: (u32, u32), - - inode_table: HashMap, - inode_factory: InodeFactory, - next_fh: FileHandle, + dcache: DCache, /// Maps mesa-level org-root inodes → index into `org_slots`. org_inodes: HashMap, @@ -61,22 +57,8 @@ impl MesaFS { /// Create a new `MesaFS` instance. pub fn new(orgs: impl Iterator, fs_owner: (u32, u32)) -> Self { - let now = SystemTime::now(); - - let mut inode_table = HashMap::new(); - inode_table.insert( - Self::ROOT_NODE_INO, - InodeControlBlock { - rc: 1, - parent: None, - path: "/".into(), - children: None, - attr: None, - }, - ); - - let mut fs = Self { - inode_table, + Self { + dcache: DCache::new(Self::ROOT_NODE_INO, fs_owner, Self::BLOCK_SIZE), org_inodes: HashMap::new(), org_slots: orgs .map(|org_conf| { @@ -88,24 +70,7 @@ impl MesaFS { } }) .collect(), - inode_factory: InodeFactory::new(Self::ROOT_NODE_INO + 1), - fs_owner, - next_fh: 1, - }; - - let root_attr = FileAttr::Directory { - common: common::make_common_file_attr( - fs.fs_owner, - Self::BLOCK_SIZE, - Self::ROOT_NODE_INO, - 0o755, - now, - now, - ), - }; - common::cache_attr(&mut fs.inode_table, Self::ROOT_NODE_INO, root_attr); - - fs + } } /// Classify an inode by its role. @@ -130,7 +95,7 @@ impl MesaFS { return Some(idx); } let mut current = ino; - while let Some(parent) = self.inode_table.get(¤t).and_then(|icb| icb.parent) { + while let Some(parent) = self.dcache.get_icb(current).and_then(|icb| icb.parent) { if let Some(&idx) = self.org_inodes.get(&parent) { return Some(idx); } @@ -145,7 +110,7 @@ impl MesaFS { fn ensure_org_inode(&mut self, org_idx: usize) -> (Inode, FileAttr) { // Check if an inode already exists. if let Some((&existing_ino, _)) = self.org_inodes.iter().find(|&(_, &idx)| idx == org_idx) { - if let Some(icb) = self.inode_table.get(&existing_ino) + if let Some(icb) = self.dcache.get_icb(existing_ino) && let Some(attr) = icb.attr { trace!( @@ -161,28 +126,23 @@ impl MesaFS { ino = existing_ino, org_idx, "ensure_org_inode: attr missing, rebuilding" ); - let now = SystemTime::now(); + let now = std::time::SystemTime::now(); let attr = FileAttr::Directory { - common: common::make_common_file_attr( - self.fs_owner, - Self::BLOCK_SIZE, - existing_ino, - 0o755, - now, - now, - ), + common: self + .dcache + .make_common_file_attr(existing_ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, existing_ino, attr); + self.dcache.cache_attr(existing_ino, attr); return (existing_ino, attr); } // Allocate new. let org_name = self.org_slots[org_idx].org.name().to_owned(); - let ino = self.inode_factory.allocate(); + let ino = self.dcache.allocate_inode(); trace!(ino, org_idx, org = %org_name, "ensure_org_inode: allocated new inode"); - let now = SystemTime::now(); - self.inode_table.insert( + let now = std::time::SystemTime::now(); + self.dcache.insert_icb( ino, InodeControlBlock { rc: 0, @@ -201,23 +161,15 @@ impl MesaFS { .insert_inode(ino, OrgFs::ROOT_INO); let attr = FileAttr::Directory { - common: common::make_common_file_attr( - self.fs_owner, - Self::BLOCK_SIZE, - ino, - 0o755, - now, - now, - ), + common: self.dcache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.dcache.cache_attr(ino, attr); (ino, attr) } /// Allocate a mesa-level file handle and map it through the bridge. fn alloc_fh(&mut self, slot_idx: usize, org_fh: FileHandle) -> FileHandle { - let fh = self.next_fh; - self.next_fh += 1; + let fh = self.dcache.allocate_fh(); self.org_slots[slot_idx].bridge.insert_fh(fh, org_fh); fh } @@ -233,17 +185,15 @@ impl MesaFS { ) -> Inode { let mesa_ino = self.org_slots[slot_idx] .bridge - .backward_or_insert_inode(org_ino, || self.inode_factory.allocate()); + .backward_or_insert_inode(org_ino, || self.dcache.allocate_inode()); - self.inode_table - .entry(mesa_ino) - .or_insert_with(|| InodeControlBlock { - rc: 0, - path: name.into(), - parent: Some(parent_mesa_ino), - children: None, - attr: None, - }); + self.dcache.entry_or_insert_icb(mesa_ino, || InodeControlBlock { + rc: 0, + path: name.into(), + parent: Some(parent_mesa_ino), + children: None, + attr: None, + }); mesa_ino } @@ -261,7 +211,7 @@ impl Fs for MesaFS { #[instrument(skip(self))] async fn lookup(&mut self, parent: Inode, name: &OsStr) -> Result { debug_assert!( - self.inode_table.contains_key(&parent), + self.dcache.contains(parent), "lookup: parent inode {parent} not in inode table" ); @@ -277,15 +227,11 @@ impl Fs for MesaFS { trace!(org = org_name, "lookup: matched org"); let (ino, attr) = self.ensure_org_inode(org_idx); - let icb = self - .inode_table - .get_mut(&ino) - .unwrap_or_else(|| unreachable!("inode {ino} was just ensured")); - icb.rc += 1; + let rc = self.dcache.inc_rc(ino); trace!( ino, org = org_name, - rc = icb.rc, + rc, "lookup: resolved org inode" ); Ok(attr) @@ -302,16 +248,12 @@ impl Fs for MesaFS { let mesa_ino = self.translate_org_ino_to_mesa(idx, org_ino, parent, name); let mesa_attr = self.org_slots[idx].bridge.attr_backward(org_attr); - common::cache_attr(&mut self.inode_table, mesa_ino, mesa_attr); - let icb = self - .inode_table - .get_mut(&mesa_ino) - .unwrap_or_else(|| unreachable!("inode {mesa_ino} was just cached")); - icb.rc += 1; + self.dcache.cache_attr(mesa_ino, mesa_attr); + let rc = self.dcache.inc_rc(mesa_ino); trace!( mesa_ino, org_ino, - rc = icb.rc, + rc, "lookup: resolved via org delegation" ); Ok(mesa_attr) @@ -325,20 +267,16 @@ impl Fs for MesaFS { ino: Inode, _fh: Option, ) -> Result { - let icb = self.inode_table.get(&ino).ok_or_else(|| { + self.dcache.get_attr(ino).ok_or_else(|| { warn!(ino, "getattr on unknown inode"); GetAttrError::InodeNotFound - })?; - icb.attr.ok_or_else(|| { - warn!(ino, "getattr on inode with no cached attr"); - GetAttrError::InodeNotFound }) } #[instrument(skip(self))] async fn readdir(&mut self, ino: Inode) -> Result<&[DirEntry], ReadDirError> { debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "readdir: inode {ino} not in inode table" ); @@ -364,8 +302,8 @@ impl Fs for MesaFS { trace!(entry_count = entries.len(), "readdir: listing orgs"); let icb = self - .inode_table - .get_mut(&ino) + .dcache + .get_icb_mut(ino) .ok_or(ReadDirError::InodeNotFound)?; Ok(icb.children.insert(entries)) } @@ -387,7 +325,7 @@ impl Fs for MesaFS { self.org_slots[idx].org.inode_table_get_attr(entry.ino) { let mesa_attr = self.org_slots[idx].bridge.attr_backward(org_icb_attr); - common::cache_attr(&mut self.inode_table, mesa_child_ino, mesa_attr); + self.dcache.cache_attr(mesa_child_ino, mesa_attr); } mesa_entries.push(DirEntry { @@ -398,8 +336,8 @@ impl Fs for MesaFS { } let icb = self - .inode_table - .get_mut(&ino) + .dcache + .get_icb_mut(ino) .ok_or(ReadDirError::InodeNotFound)?; Ok(icb.children.insert(mesa_entries)) } @@ -496,7 +434,7 @@ impl Fs for MesaFS { #[instrument(skip(self))] async fn forget(&mut self, ino: Inode, nlookups: u64) { debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "forget: inode {ino} not in inode table" ); @@ -507,39 +445,15 @@ impl Fs for MesaFS { self.org_slots[idx].org.forget(org_ino, nlookups).await; } - match self.inode_table.entry(ino) { - std::collections::hash_map::Entry::Occupied(mut entry) => { - if entry.get().rc <= nlookups { - trace!(ino, "evicting inode"); - entry.remove(); - self.org_inodes.remove(&ino); - for slot in &mut self.org_slots { - slot.bridge.remove_inode_by_left(ino); - } - } else { - entry.get_mut().rc -= nlookups; - trace!(ino, new_rc = entry.get().rc, "forget: decremented rc"); - } - } - std::collections::hash_map::Entry::Vacant(_) => { - warn!(ino, "forget on unknown inode"); + if self.dcache.forget(ino, nlookups).is_some() { + self.org_inodes.remove(&ino); + for slot in &mut self.org_slots { + slot.bridge.remove_inode_by_left(ino); } } } async fn statfs(&mut self) -> Result { - Ok(FilesystemStats { - block_size: Self::BLOCK_SIZE, - fragment_size: u64::from(Self::BLOCK_SIZE), - total_blocks: 0, - free_blocks: 0, - available_blocks: 0, - total_inodes: self.inode_table.len() as u64, - free_inodes: 0, - available_inodes: 0, - filesystem_id: 0, - mount_flags: 0, - max_filename_length: 255, - }) + Ok(self.dcache.statfs()) } } From e2d7f6a2b5a02547ea6ae2cba1d465f8fa5ee19e Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 17:33:23 -0800 Subject: [PATCH 06/11] Remove dcache code from common.rs (now in DCache) Co-Authored-By: Claude Opus 4.6 --- src/fs/mescloud/common.rs | 138 +------------------------------------- src/fs/mescloud/dcache.rs | 10 +-- 2 files changed, 4 insertions(+), 144 deletions(-) diff --git a/src/fs/mescloud/common.rs b/src/fs/mescloud/common.rs index 88b2ee9..2e1f8c3 100644 --- a/src/fs/mescloud/common.rs +++ b/src/fs/mescloud/common.rs @@ -1,27 +1,8 @@ //! Shared types and helpers used by both `MesaFS` and `RepoFs`. -use std::{collections::HashMap, ffi::OsStr, time::SystemTime}; - use thiserror::Error; -use tracing::warn; - -use crate::fs::r#trait::{CommonFileAttr, DirEntry, DirEntryType, FileAttr, Inode, Permissions}; - -pub(super) struct InodeFactory { - next_inode: Inode, -} - -impl InodeFactory { - pub(super) fn new(start: Inode) -> Self { - Self { next_inode: start } - } - pub(super) fn allocate(&mut self) -> Inode { - let ino = self.next_inode; - self.next_inode += 1; - ino - } -} +use crate::fs::r#trait::{DirEntry, FileAttr, Inode}; pub(super) struct InodeControlBlock { /// The root inode doesn't have a parent. @@ -33,123 +14,6 @@ pub(super) struct InodeControlBlock { pub attr: Option, } -pub(super) fn blocks_of_size(block_size: u32, size: u64) -> u64 { - size.div_ceil(u64::from(block_size)) -} - -pub(super) fn make_common_file_attr( - fs_owner: (u32, u32), - block_size: u32, - ino: Inode, - perm: u16, - atime: SystemTime, - mtime: SystemTime, -) -> CommonFileAttr { - CommonFileAttr { - ino, - atime, - mtime, - ctime: SystemTime::UNIX_EPOCH, - crtime: SystemTime::UNIX_EPOCH, - perm: Permissions::from_bits_truncate(perm), - nlink: 1, - uid: fs_owner.0, - gid: fs_owner.1, - blksize: block_size, - } -} - -pub(super) fn cache_attr( - inode_table: &mut HashMap, - ino: Inode, - attr: FileAttr, -) { - if let Some(icb) = inode_table.get_mut(&ino) { - icb.attr = Some(attr); - } -} - -/// Ensure a child inode exists under `parent` with the given `name` and `kind`. -/// -/// Reuses an existing inode if one already exists for this parent+name pair. -/// Does NOT bump rc — callers that create kernel-visible references must bump rc themselves. -#[expect( - clippy::too_many_arguments, - reason = "inode creation requires all these contextual parameters" -)] -pub(super) fn ensure_child_inode( - inode_table: &mut HashMap, - inode_factory: &mut InodeFactory, - fs_owner: (u32, u32), - block_size: u32, - parent: Inode, - name: &OsStr, - kind: DirEntryType, -) -> (Inode, FileAttr) { - // Check if an inode already exists for this child under this parent. - if let Some((&existing_ino, _)) = inode_table - .iter() - .find(|&(&_ino, icb)| icb.parent == Some(parent) && icb.path.as_os_str() == name) - { - if let Some(attr) = inode_table.get(&existing_ino).and_then(|icb| icb.attr) { - return (existing_ino, attr); - } - - // Attr missing, rebuild from kind. - warn!(ino = existing_ino, parent, name = ?name, ?kind, "ensure_child_inode: attr missing on existing inode, rebuilding"); - let now = SystemTime::now(); - let attr = match kind { - DirEntryType::Directory => FileAttr::Directory { - common: make_common_file_attr(fs_owner, block_size, existing_ino, 0o755, now, now), - }, - DirEntryType::RegularFile - | DirEntryType::Symlink - | DirEntryType::CharDevice - | DirEntryType::BlockDevice - | DirEntryType::NamedPipe - | DirEntryType::Socket => FileAttr::RegularFile { - common: make_common_file_attr(fs_owner, block_size, existing_ino, 0o644, now, now), - size: 0, - blocks: 0, - }, - }; - cache_attr(inode_table, existing_ino, attr); - return (existing_ino, attr); - } - - // No existing inode — allocate without bumping rc. - let ino = inode_factory.allocate(); - let now = SystemTime::now(); - inode_table.insert( - ino, - InodeControlBlock { - rc: 0, - path: name.into(), - parent: Some(parent), - children: None, - attr: None, - }, - ); - - let attr = match kind { - DirEntryType::Directory => FileAttr::Directory { - common: make_common_file_attr(fs_owner, block_size, ino, 0o755, now, now), - }, - DirEntryType::RegularFile - | DirEntryType::Symlink - | DirEntryType::CharDevice - | DirEntryType::BlockDevice - | DirEntryType::NamedPipe - | DirEntryType::Socket => FileAttr::RegularFile { - common: make_common_file_attr(fs_owner, block_size, ino, 0o644, now, now), - size: 0, - blocks: 0, - }, - }; - cache_attr(inode_table, ino, attr); - (ino, attr) -} - // ── Error types ────────────────────────────────────────────────────────────── #[derive(Debug, Error)] diff --git a/src/fs/mescloud/dcache.rs b/src/fs/mescloud/dcache.rs index 12cb4e1..9df588f 100644 --- a/src/fs/mescloud/dcache.rs +++ b/src/fs/mescloud/dcache.rs @@ -42,7 +42,7 @@ pub(super) struct DCache { } impl DCache { - /// Create a new DCache. Initializes root ICB (rc=1), caches root dir attr. + /// Create a new `DCache`. Initializes root ICB (rc=1), caches root dir attr. pub fn new(root_ino: Inode, fs_owner: (u32, u32), block_size: u32) -> Self { let now = SystemTime::now(); @@ -101,12 +101,12 @@ impl DCache { self.inode_table.contains_key(&ino) } - /// Insert an ICB directly (for ensure_org_inode / ensure_repo_inode patterns). + /// Insert an ICB directly (for `ensure_org_inode` / `ensure_repo_inode` patterns). pub fn insert_icb(&mut self, ino: Inode, icb: InodeControlBlock) { self.inode_table.insert(ino, icb); } - /// Insert an ICB only if absent (for translate_*_ino_to_* patterns). + /// Insert an ICB only if absent (for `translate_*_ino_to_*` patterns). /// Returns a mutable reference to the (possibly pre-existing) ICB. pub fn entry_or_insert_icb( &mut self, @@ -116,10 +116,6 @@ impl DCache { self.inode_table.entry(ino).or_insert_with(icb) } - pub fn inode_count(&self) -> usize { - self.inode_table.len() - } - // ── Attr caching ──────────────────────────────────────────────────── pub fn get_attr(&self, ino: Inode) -> Option { From a3acce33e4df3169c5489e4f8bbf34417cdf78c6 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Thu, 5 Feb 2026 18:22:30 -0800 Subject: [PATCH 07/11] Extract generic DCache and MescloudDCache into top-level dcache package Generify the directory cache so it can be shared between mescloud and LocalFs. The generic DCache owns the inode table and file handle counter, while MescloudDCache wraps it with inode allocation, attr caching, and filesystem metadata. LocalFs now uses DCache directly. Also moves inode_bridge into dcache::bridge. Co-Authored-By: Claude Opus 4.6 --- src/fs/{inode_bridge.rs => dcache/bridge.rs} | 0 .../dcache.rs => dcache/mescloud.rs} | 181 ++++++++---------- src/fs/dcache/mod.rs | 16 ++ src/fs/dcache/table.rs | 107 +++++++++++ src/fs/local.rs | 104 +++++----- src/fs/mescloud/common.rs | 12 +- src/fs/mescloud/mod.rs | 9 +- src/fs/mescloud/org.rs | 8 +- src/fs/mescloud/repo.rs | 8 +- src/fs/mod.rs | 2 +- 10 files changed, 259 insertions(+), 188 deletions(-) rename src/fs/{inode_bridge.rs => dcache/bridge.rs} (100%) rename src/fs/{mescloud/dcache.rs => dcache/mescloud.rs} (58%) create mode 100644 src/fs/dcache/mod.rs create mode 100644 src/fs/dcache/table.rs diff --git a/src/fs/inode_bridge.rs b/src/fs/dcache/bridge.rs similarity index 100% rename from src/fs/inode_bridge.rs rename to src/fs/dcache/bridge.rs diff --git a/src/fs/mescloud/dcache.rs b/src/fs/dcache/mescloud.rs similarity index 58% rename from src/fs/mescloud/dcache.rs rename to src/fs/dcache/mescloud.rs index 9df588f..36463ed 100644 --- a/src/fs/mescloud/dcache.rs +++ b/src/fs/dcache/mescloud.rs @@ -1,19 +1,56 @@ -//! Reusable directory cache for mescloud filesystem implementations. +//! Mescloud-specific directory cache wrapper. +//! +//! Composes [`DCache`] with inode allocation, attribute +//! caching, and filesystem-owner metadata. -use std::collections::HashMap; use std::ffi::OsStr; use std::time::SystemTime; -use tracing::{trace, warn}; +use tracing::warn; use crate::fs::r#trait::{ - CommonFileAttr, DirEntryType, FileAttr, FileHandle, FilesystemStats, Inode, Permissions, + CommonFileAttr, DirEntry, DirEntryType, FileAttr, FilesystemStats, Inode, Permissions, }; -use super::common::InodeControlBlock; +use super::{DCache, IcbLike}; + +// ── InodeControlBlock ──────────────────────────────────────────────────── + +pub struct InodeControlBlock { + /// The root inode doesn't have a parent. + pub parent: Option, + pub rc: u64, + pub path: std::path::PathBuf, + pub children: Option>, + /// Cached file attributes from the last lookup. + pub attr: Option, +} + +impl IcbLike for InodeControlBlock { + fn new_root(path: std::path::PathBuf) -> Self { + Self { + rc: 1, + parent: None, + path, + children: None, + attr: None, + } + } + + fn rc(&self) -> u64 { + self.rc + } + + fn rc_mut(&mut self) -> &mut u64 { + &mut self.rc + } + +} + +// ── InodeFactory ──────────────────────────────────────────────────────── /// Monotonically increasing inode allocator. -pub(super) struct InodeFactory { +struct InodeFactory { next_inode: Inode, } @@ -29,43 +66,43 @@ impl InodeFactory { } } -/// Shared directory cache state. +// ── MescloudDCache ────────────────────────────────────────────────────── + +/// Mescloud-specific directory cache. /// -/// Owns inode allocation, reference counting, attribute caching, and file handle allocation. -/// Designed for composition — each filesystem struct embeds a `DCache` field. -pub(super) struct DCache { - inode_table: HashMap, +/// Wraps [`DCache`] and adds inode allocation, attribute +/// caching, `ensure_child_inode`, and filesystem metadata. +pub struct MescloudDCache { + inner: DCache, inode_factory: InodeFactory, - next_fh: FileHandle, fs_owner: (u32, u32), block_size: u32, } -impl DCache { - /// Create a new `DCache`. Initializes root ICB (rc=1), caches root dir attr. - pub fn new(root_ino: Inode, fs_owner: (u32, u32), block_size: u32) -> Self { - let now = SystemTime::now(); +impl std::ops::Deref for MescloudDCache { + type Target = DCache; + fn deref(&self) -> &Self::Target { + &self.inner + } +} - let mut inode_table = HashMap::new(); - inode_table.insert( - root_ino, - InodeControlBlock { - rc: 1, - parent: None, - path: "/".into(), - children: None, - attr: None, - }, - ); +impl std::ops::DerefMut for MescloudDCache { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} +impl MescloudDCache { + /// Create a new `MescloudDCache`. Initializes root ICB (rc=1), caches root dir attr. + pub fn new(root_ino: Inode, fs_owner: (u32, u32), block_size: u32) -> Self { let mut dcache = Self { - inode_table, + inner: DCache::new(root_ino, "/"), inode_factory: InodeFactory::new(root_ino + 1), - next_fh: 1, fs_owner, block_size, }; + let now = SystemTime::now(); let root_attr = FileAttr::Directory { common: dcache.make_common_file_attr(root_ino, 0o755, now, now), }; @@ -80,86 +117,18 @@ impl DCache { self.inode_factory.allocate() } - /// Allocate a file handle (increments `next_fh` and returns the old value). - pub fn allocate_fh(&mut self) -> FileHandle { - let fh = self.next_fh; - self.next_fh += 1; - fh - } - - // ── ICB access ────────────────────────────────────────────────────── - - pub fn get_icb(&self, ino: Inode) -> Option<&InodeControlBlock> { - self.inode_table.get(&ino) - } - - pub fn get_icb_mut(&mut self, ino: Inode) -> Option<&mut InodeControlBlock> { - self.inode_table.get_mut(&ino) - } - - pub fn contains(&self, ino: Inode) -> bool { - self.inode_table.contains_key(&ino) - } - - /// Insert an ICB directly (for `ensure_org_inode` / `ensure_repo_inode` patterns). - pub fn insert_icb(&mut self, ino: Inode, icb: InodeControlBlock) { - self.inode_table.insert(ino, icb); - } - - /// Insert an ICB only if absent (for `translate_*_ino_to_*` patterns). - /// Returns a mutable reference to the (possibly pre-existing) ICB. - pub fn entry_or_insert_icb( - &mut self, - ino: Inode, - icb: impl FnOnce() -> InodeControlBlock, - ) -> &mut InodeControlBlock { - self.inode_table.entry(ino).or_insert_with(icb) - } - // ── Attr caching ──────────────────────────────────────────────────── pub fn get_attr(&self, ino: Inode) -> Option { - self.inode_table.get(&ino).and_then(|icb| icb.attr) + self.inner.get_icb(ino).and_then(|icb| icb.attr) } pub fn cache_attr(&mut self, ino: Inode, attr: FileAttr) { - if let Some(icb) = self.inode_table.get_mut(&ino) { + if let Some(icb) = self.inner.get_icb_mut(ino) { icb.attr = Some(attr); } } - // ── Reference counting ────────────────────────────────────────────── - - /// Increment rc. Panics (via unwrap) if inode doesn't exist. - pub fn inc_rc(&mut self, ino: Inode) -> u64 { - let icb = self - .inode_table - .get_mut(&ino) - .unwrap_or_else(|| unreachable!("inc_rc: inode {ino} not in table")); - icb.rc += 1; - icb.rc - } - - /// Decrement rc by `nlookups`. Returns `Some(evicted_icb)` if the inode was evicted. - pub fn forget(&mut self, ino: Inode, nlookups: u64) -> Option { - match self.inode_table.entry(ino) { - std::collections::hash_map::Entry::Occupied(mut entry) => { - if entry.get().rc <= nlookups { - trace!(ino, "evicting inode"); - Some(entry.remove()) - } else { - entry.get_mut().rc -= nlookups; - trace!(ino, new_rc = entry.get().rc, "decremented rc"); - None - } - } - std::collections::hash_map::Entry::Vacant(_) => { - warn!(ino, "forget on unknown inode"); - None - } - } - } - // ── Child inode management ────────────────────────────────────────── /// Ensure a child inode exists under `parent` with the given `name` and `kind`. @@ -171,12 +140,14 @@ impl DCache { kind: DirEntryType, ) -> (Inode, FileAttr) { // Check existing child by parent + name. - if let Some((&existing_ino, _)) = self - .inode_table + let existing = self + .inner .iter() .find(|&(&_ino, icb)| icb.parent == Some(parent) && icb.path.as_os_str() == name) - { - if let Some(attr) = self.inode_table.get(&existing_ino).and_then(|icb| icb.attr) { + .map(|(&ino, _)| ino); + + if let Some(existing_ino) = existing { + if let Some(attr) = self.inner.get_icb(existing_ino).and_then(|icb| icb.attr) { return (existing_ino, attr); } @@ -188,7 +159,7 @@ impl DCache { } let ino = self.inode_factory.allocate(); - self.inode_table.insert( + self.inner.insert_icb( ino, InodeControlBlock { rc: 0, @@ -259,7 +230,7 @@ impl DCache { total_blocks: 0, free_blocks: 0, available_blocks: 0, - total_inodes: self.inode_table.len() as u64, + total_inodes: self.inner.inode_count() as u64, free_inodes: 0, available_inodes: 0, filesystem_id: 0, diff --git a/src/fs/dcache/mod.rs b/src/fs/dcache/mod.rs new file mode 100644 index 0000000..dc445c1 --- /dev/null +++ b/src/fs/dcache/mod.rs @@ -0,0 +1,16 @@ +//! Generic directory cache and inode management primitives. + +pub mod bridge; +pub mod mescloud; +mod table; + +pub use mescloud::MescloudDCache; +pub use table::DCache; + +/// Common interface for inode control block types usable with `DCache`. +pub trait IcbLike { + /// Create an ICB with rc=1, the given path, and no children. + fn new_root(path: std::path::PathBuf) -> Self; + fn rc(&self) -> u64; + fn rc_mut(&mut self) -> &mut u64; +} diff --git a/src/fs/dcache/table.rs b/src/fs/dcache/table.rs new file mode 100644 index 0000000..753ddf5 --- /dev/null +++ b/src/fs/dcache/table.rs @@ -0,0 +1,107 @@ +//! Generic inode table with reference counting and file handle allocation. + +use std::collections::HashMap; + +use tracing::{trace, warn}; + +use crate::fs::r#trait::{FileHandle, Inode}; + +use super::IcbLike; + +/// Generic directory cache. +/// +/// Owns an inode table and a file handle counter. Provides reference counting, +/// ICB lookup/insertion, and file handle allocation. +pub struct DCache { + inode_table: HashMap, + next_fh: FileHandle, +} + +impl DCache { + /// Create a new `DCache` with a root ICB at `root_ino` (rc=1). + pub fn new(root_ino: Inode, root_path: impl Into) -> Self { + let mut inode_table = HashMap::new(); + inode_table.insert(root_ino, I::new_root(root_path.into())); + Self { + inode_table, + next_fh: 1, + } + } + + // ── File handle allocation ────────────────────────────────────────── + + /// Allocate a file handle (increments `next_fh` and returns the old value). + pub fn allocate_fh(&mut self) -> FileHandle { + let fh = self.next_fh; + self.next_fh += 1; + fh + } + + // ── ICB access ────────────────────────────────────────────────────── + + pub fn get_icb(&self, ino: Inode) -> Option<&I> { + self.inode_table.get(&ino) + } + + pub fn get_icb_mut(&mut self, ino: Inode) -> Option<&mut I> { + self.inode_table.get_mut(&ino) + } + + pub fn contains(&self, ino: Inode) -> bool { + self.inode_table.contains_key(&ino) + } + + /// Insert an ICB directly. + pub fn insert_icb(&mut self, ino: Inode, icb: I) { + self.inode_table.insert(ino, icb); + } + + /// Insert an ICB only if absent. + /// Returns a mutable reference to the (possibly pre-existing) ICB. + pub fn entry_or_insert_icb(&mut self, ino: Inode, f: impl FnOnce() -> I) -> &mut I { + self.inode_table.entry(ino).or_insert_with(f) + } + + /// Number of inodes in the table. + pub fn inode_count(&self) -> usize { + self.inode_table.len() + } + + // ── Reference counting ────────────────────────────────────────────── + + /// Increment rc. Panics (via unwrap) if inode doesn't exist. + pub fn inc_rc(&mut self, ino: Inode) -> u64 { + let icb = self + .inode_table + .get_mut(&ino) + .unwrap_or_else(|| unreachable!("inc_rc: inode {ino} not in table")); + *icb.rc_mut() += 1; + icb.rc() + } + + /// Decrement rc by `nlookups`. Returns `Some(evicted_icb)` if the inode was evicted. + pub fn forget(&mut self, ino: Inode, nlookups: u64) -> Option { + match self.inode_table.entry(ino) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + if entry.get().rc() <= nlookups { + trace!(ino, "evicting inode"); + Some(entry.remove()) + } else { + *entry.get_mut().rc_mut() -= nlookups; + trace!(ino, new_rc = entry.get().rc(), "decremented rc"); + None + } + } + std::collections::hash_map::Entry::Vacant(_) => { + warn!(ino, "forget on unknown inode"); + None + } + } + } + + // ── Iteration ─────────────────────────────────────────────────────── + + pub fn iter(&self) -> impl Iterator { + self.inode_table.iter() + } +} diff --git a/src/fs/local.rs b/src/fs/local.rs index 006e945..8a39e48 100644 --- a/src/fs/local.rs +++ b/src/fs/local.rs @@ -1,16 +1,14 @@ //! An implementation of a filesystem that directly overlays the host filesystem. use bytes::Bytes; use nix::sys::statvfs::statvfs; -use std::{ - collections::{HashMap, hash_map::Entry}, - path::PathBuf, -}; +use std::{collections::HashMap, path::PathBuf}; use thiserror::Error; use tokio::io::{AsyncReadExt as _, AsyncSeekExt as _}; use std::ffi::OsStr; use tracing::warn; +use crate::fs::dcache::{DCache, IcbLike}; use crate::fs::r#trait::{ DirEntry, FileAttr, FileHandle, FileOpenOptions, FilesystemStats, Fs, Inode, LockOwner, OpenFile, OpenFlags, @@ -141,36 +139,42 @@ struct InodeControlBlock { pub children: Option>, } +impl IcbLike for InodeControlBlock { + fn new_root(path: PathBuf) -> Self { + Self { + rc: 1, + path, + children: None, + } + } + + fn rc(&self) -> u64 { + self.rc + } + + fn rc_mut(&mut self) -> &mut u64 { + &mut self.rc + } +} + pub struct LocalFs { - inode_table: HashMap, + dcache: DCache, open_files: HashMap, - next_fh: FileHandle, } impl LocalFs { #[expect(dead_code, reason = "alternative filesystem implementation")] pub fn new(abs_path: impl Into) -> Self { - let mut inode_table = HashMap::new(); - inode_table.insert( - 1, - InodeControlBlock { - rc: 1, - path: abs_path.into(), - children: None, - }, - ); - Self { - inode_table, + dcache: DCache::new(1, abs_path), open_files: HashMap::new(), - next_fh: 1, } } fn abspath(&self) -> &PathBuf { &self - .inode_table - .get(&1) + .dcache + .get_icb(1) .unwrap_or_else(|| unreachable!("root inode 1 must always exist in inode_table")) .path } @@ -202,10 +206,10 @@ impl Fs for LocalFs { async fn lookup(&mut self, parent: Inode, name: &OsStr) -> Result { debug_assert!( - self.inode_table.contains_key(&parent), + self.dcache.contains(parent), "parent inode {parent} not in inode_table" ); - let parent_icb = self.inode_table.get(&parent).ok_or_else(|| { + let parent_icb = self.dcache.get_icb(parent).ok_or_else(|| { warn!( "Lookup called on unknown parent inode {}. This is a programming bug", parent @@ -222,15 +226,14 @@ impl Fs for LocalFs { debug_assert!(file_attr.is_ok(), "FileAttr conversion failed unexpectedly"); let file_attr = file_attr?; - let map_entry = - self.inode_table - .entry(file_attr.common().ino) - .or_insert(InodeControlBlock { - rc: 0, - path: child_path, - children: None, - }); - map_entry.rc += 1; + let icb = self + .dcache + .entry_or_insert_icb(file_attr.common().ino, || InodeControlBlock { + rc: 0, + path: child_path, + children: None, + }); + *icb.rc_mut() += 1; Ok(file_attr) } @@ -262,10 +265,10 @@ impl Fs for LocalFs { } else { // No open path, so we have to do a painful stat on the path. debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "inode {ino} not in inode_table" ); - let icb = self.inode_table.get(&ino).ok_or_else(|| { + let icb = self.dcache.get_icb(ino).ok_or_else(|| { warn!( "GetAttr called on unknown inode {}. This is a programming bug", ino @@ -285,11 +288,11 @@ impl Fs for LocalFs { async fn readdir(&mut self, ino: Inode) -> Result<&[DirEntry], ReadDirError> { debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "inode {ino} not in inode_table" ); - let inode_cb = self.inode_table.get_mut(&ino).ok_or_else(|| { + let inode_cb = self.dcache.get_icb(ino).ok_or_else(|| { warn!( parent = ino, "Readdir of unknown parent inode. Programming bug" @@ -315,7 +318,7 @@ impl Fs for LocalFs { entries.push(Self::parse_tokio_dirent(&dir_entry).await?); } - let inode_cb = self.inode_table.get_mut(&ino).ok_or_else(|| { + let inode_cb = self.dcache.get_icb_mut(ino).ok_or_else(|| { warn!(parent = ino, "inode disappeared. TOCTOU programming bug"); ReadDirError::InodeNotFound })?; @@ -325,10 +328,10 @@ impl Fs for LocalFs { async fn open(&mut self, ino: Inode, flags: OpenFlags) -> Result { debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "inode {ino} not in inode_table" ); - let icb = self.inode_table.get(&ino).ok_or_else(|| { + let icb = self.dcache.get_icb(ino).ok_or_else(|| { warn!( "Open called on unknown inode {}. This is a programming bug", ino @@ -348,8 +351,7 @@ impl Fs for LocalFs { .map_err(OpenError::Io)?; // Generate a new file handle. - let fh = self.next_fh; - self.next_fh += 1; + let fh = self.dcache.allocate_fh(); self.open_files.insert(fh, file); Ok(OpenFile { @@ -370,7 +372,7 @@ impl Fs for LocalFs { ) -> Result { // TODO(markovejnovic): Respect flags and lock_owner. debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "inode {ino} not in inode_table" ); debug_assert!( @@ -415,25 +417,11 @@ impl Fs for LocalFs { async fn forget(&mut self, ino: Inode, nlookups: u64) { debug_assert!( - self.inode_table.contains_key(&ino), + self.dcache.contains(ino), "inode {ino} not in inode_table" ); - match self.inode_table.entry(ino) { - Entry::Occupied(mut entry) => { - if entry.get().rc <= nlookups { - entry.remove(); - } else { - entry.get_mut().rc -= nlookups; - } - } - Entry::Vacant(_) => { - warn!( - "Forget called on unknown inode {}. This is a programming bug", - ino - ); - } - } + self.dcache.forget(ino, nlookups); } async fn statfs(&mut self) -> Result { @@ -456,7 +444,7 @@ impl Fs for LocalFs { #[allow(clippy::allow_attributes)] #[allow(clippy::useless_conversion)] available_blocks: u64::from(stat.blocks_available()), - total_inodes: self.inode_table.len() as u64, + total_inodes: self.dcache.inode_count() as u64, #[allow(clippy::allow_attributes)] #[allow(clippy::useless_conversion)] free_inodes: u64::from(stat.files_free()), diff --git a/src/fs/mescloud/common.rs b/src/fs/mescloud/common.rs index 2e1f8c3..de08f1f 100644 --- a/src/fs/mescloud/common.rs +++ b/src/fs/mescloud/common.rs @@ -2,17 +2,7 @@ use thiserror::Error; -use crate::fs::r#trait::{DirEntry, FileAttr, Inode}; - -pub(super) struct InodeControlBlock { - /// The root inode doesn't have a parent. - pub parent: Option, - pub rc: u64, - pub path: std::path::PathBuf, - pub children: Option>, - /// Cached file attributes from the last lookup. - pub attr: Option, -} +pub(super) use crate::fs::dcache::mescloud::InodeControlBlock; // ── Error types ────────────────────────────────────────────────────────────── diff --git a/src/fs/mescloud/mod.rs b/src/fs/mescloud/mod.rs index 1577441..9cbf35e 100644 --- a/src/fs/mescloud/mod.rs +++ b/src/fs/mescloud/mod.rs @@ -6,7 +6,7 @@ use mesa_dev::Mesa as MesaClient; use secrecy::ExposeSecret as _; use tracing::{instrument, trace, warn}; -use crate::fs::inode_bridge::HashMapBridge; +use crate::fs::dcache::bridge::HashMapBridge; use crate::fs::r#trait::{ DirEntry, DirEntryType, FileAttr, FileHandle, FilesystemStats, Fs, Inode, LockOwner, OpenFile, OpenFlags, @@ -16,8 +16,7 @@ mod common; pub use common::{GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError}; use common::InodeControlBlock; -mod dcache; -use dcache::DCache; +use crate::fs::dcache::MescloudDCache; mod org; pub use org::OrgConfig; @@ -44,7 +43,7 @@ enum InodeRole { /// Composes multiple [`OrgFs`] instances, each with its own inode namespace, /// using [`HashMapBridge`] for bidirectional inode/fh translation at each boundary. pub struct MesaFS { - dcache: DCache, + dcache: MescloudDCache, /// Maps mesa-level org-root inodes → index into `org_slots`. org_inodes: HashMap, @@ -58,7 +57,7 @@ impl MesaFS { /// Create a new `MesaFS` instance. pub fn new(orgs: impl Iterator, fs_owner: (u32, u32)) -> Self { Self { - dcache: DCache::new(Self::ROOT_NODE_INO, fs_owner, Self::BLOCK_SIZE), + dcache: MescloudDCache::new(Self::ROOT_NODE_INO, fs_owner, Self::BLOCK_SIZE), org_inodes: HashMap::new(), org_slots: orgs .map(|org_conf| { diff --git a/src/fs/mescloud/org.rs b/src/fs/mescloud/org.rs index dc09b15..8b21a39 100644 --- a/src/fs/mescloud/org.rs +++ b/src/fs/mescloud/org.rs @@ -9,12 +9,12 @@ use secrecy::SecretString; use tracing::{instrument, trace, warn}; use super::common::InodeControlBlock; -use super::dcache::DCache; pub use super::common::{ GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError, }; use super::repo::RepoFs; -use crate::fs::inode_bridge::HashMapBridge; +use crate::fs::dcache::bridge::HashMapBridge; +use crate::fs::dcache::MescloudDCache; use crate::fs::r#trait::{ DirEntry, DirEntryType, FileAttr, FileHandle, FilesystemStats, Fs, Inode, LockOwner, OpenFile, OpenFlags, @@ -50,7 +50,7 @@ pub struct OrgFs { name: String, client: MesaClient, - dcache: DCache, + dcache: MescloudDCache, /// Maps org-level repo-root inodes → index into `repos`. repo_inodes: HashMap, @@ -142,7 +142,7 @@ impl OrgFs { Self { name, client, - dcache: DCache::new(Self::ROOT_INO, fs_owner, Self::BLOCK_SIZE), + dcache: MescloudDCache::new(Self::ROOT_INO, fs_owner, Self::BLOCK_SIZE), repo_inodes: HashMap::new(), owner_inodes: HashMap::new(), repos: Vec::new(), diff --git a/src/fs/mescloud/repo.rs b/src/fs/mescloud/repo.rs index 2e558cd..ca4baf3 100644 --- a/src/fs/mescloud/repo.rs +++ b/src/fs/mescloud/repo.rs @@ -14,7 +14,7 @@ use crate::fs::r#trait::{ LockOwner, OpenFile, OpenFlags, }; -use super::dcache::{self, DCache}; +use crate::fs::dcache::mescloud::{self as mescloud_dcache, MescloudDCache}; pub use super::common::{ GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError, }; @@ -29,7 +29,7 @@ pub struct RepoFs { repo_name: String, ref_: String, - dcache: DCache, + dcache: MescloudDCache, open_files: HashMap, } @@ -50,7 +50,7 @@ impl RepoFs { org_name, repo_name, ref_, - dcache: DCache::new(Self::ROOT_INO, fs_owner, Self::BLOCK_SIZE), + dcache: MescloudDCache::new(Self::ROOT_INO, fs_owner, Self::BLOCK_SIZE), open_files: HashMap::new(), } } @@ -135,7 +135,7 @@ impl Fs for RepoFs { mesa_dev::models::Content::File { size, .. } => FileAttr::RegularFile { common: self.dcache.make_common_file_attr(ino, 0o644, now, now), size, - blocks: dcache::blocks_of_size(Self::BLOCK_SIZE, size), + blocks: mescloud_dcache::blocks_of_size(Self::BLOCK_SIZE, size), }, mesa_dev::models::Content::Dir { .. } => FileAttr::Directory { common: self.dcache.make_common_file_attr(ino, 0o755, now, now), diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 4b3ade1..3072565 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -1,5 +1,5 @@ +pub mod dcache; pub mod fuser; -pub mod inode_bridge; pub mod local; pub mod mescloud; pub mod r#trait; From 4cf6a9bde88d25219e226c463aa314797379c02e Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Fri, 6 Feb 2026 15:04:53 -0800 Subject: [PATCH 08/11] Add mescloud/dcache.rs with InodeControlBlock and blocks_of_size Co-Authored-By: Claude Opus 4.6 --- src/fs/mescloud/dcache.rs | 40 +++++++++++++++++++++++++++++++++++++++ src/fs/mescloud/mod.rs | 1 + 2 files changed, 41 insertions(+) create mode 100644 src/fs/mescloud/dcache.rs diff --git a/src/fs/mescloud/dcache.rs b/src/fs/mescloud/dcache.rs new file mode 100644 index 0000000..8fb22d3 --- /dev/null +++ b/src/fs/mescloud/dcache.rs @@ -0,0 +1,40 @@ +//! Mescloud-specific inode control block and helpers. + +use crate::fs::dcache::IcbLike; +use crate::fs::r#trait::{DirEntry, FileAttr, Inode}; + +/// Inode control block for mescloud filesystem layers (MesaFS, OrgFs, RepoFs). +pub struct InodeControlBlock { + /// The root inode doesn't have a parent. + pub parent: Option, + pub rc: u64, + pub path: std::path::PathBuf, + pub children: Option>, + /// Cached file attributes from the last lookup. + pub attr: Option, +} + +impl IcbLike for InodeControlBlock { + fn new_root(path: std::path::PathBuf) -> Self { + Self { + rc: 1, + parent: None, + path, + children: None, + attr: None, + } + } + + fn rc(&self) -> u64 { + self.rc + } + + fn rc_mut(&mut self) -> &mut u64 { + &mut self.rc + } +} + +/// Calculate the number of blocks needed for a given size. +pub fn blocks_of_size(block_size: u32, size: u64) -> u64 { + size.div_ceil(u64::from(block_size)) +} diff --git a/src/fs/mescloud/mod.rs b/src/fs/mescloud/mod.rs index 9cbf35e..42471e4 100644 --- a/src/fs/mescloud/mod.rs +++ b/src/fs/mescloud/mod.rs @@ -23,6 +23,7 @@ pub use org::OrgConfig; use org::OrgFs; pub mod repo; +pub mod dcache; /// Per-org wrapper with inode and file handle translation. struct OrgSlot { From 046e243a724b2dde594fccb3b5ffad2695fa95dc Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Fri, 6 Feb 2026 15:09:07 -0800 Subject: [PATCH 09/11] Rename dcache/mescloud.rs to dcache/dcache.rs, import ICB from mescloud Co-Authored-By: Claude Opus 4.6 --- src/fs/dcache/{mescloud.rs => dcache.rs} | 42 ++---------------------- src/fs/dcache/mod.rs | 4 +-- 2 files changed, 5 insertions(+), 41 deletions(-) rename src/fs/dcache/{mescloud.rs => dcache.rs} (86%) diff --git a/src/fs/dcache/mescloud.rs b/src/fs/dcache/dcache.rs similarity index 86% rename from src/fs/dcache/mescloud.rs rename to src/fs/dcache/dcache.rs index 36463ed..f802b8e 100644 --- a/src/fs/dcache/mescloud.rs +++ b/src/fs/dcache/dcache.rs @@ -9,43 +9,11 @@ use std::time::SystemTime; use tracing::warn; use crate::fs::r#trait::{ - CommonFileAttr, DirEntry, DirEntryType, FileAttr, FilesystemStats, Inode, Permissions, + CommonFileAttr, DirEntryType, FileAttr, FilesystemStats, Inode, Permissions, }; +use crate::fs::mescloud::dcache::InodeControlBlock; -use super::{DCache, IcbLike}; - -// ── InodeControlBlock ──────────────────────────────────────────────────── - -pub struct InodeControlBlock { - /// The root inode doesn't have a parent. - pub parent: Option, - pub rc: u64, - pub path: std::path::PathBuf, - pub children: Option>, - /// Cached file attributes from the last lookup. - pub attr: Option, -} - -impl IcbLike for InodeControlBlock { - fn new_root(path: std::path::PathBuf) -> Self { - Self { - rc: 1, - parent: None, - path, - children: None, - attr: None, - } - } - - fn rc(&self) -> u64 { - self.rc - } - - fn rc_mut(&mut self) -> &mut u64 { - &mut self.rc - } - -} +use super::DCache; // ── InodeFactory ──────────────────────────────────────────────────────── @@ -239,7 +207,3 @@ impl MescloudDCache { } } } - -pub fn blocks_of_size(block_size: u32, size: u64) -> u64 { - size.div_ceil(u64::from(block_size)) -} diff --git a/src/fs/dcache/mod.rs b/src/fs/dcache/mod.rs index dc445c1..0b4d530 100644 --- a/src/fs/dcache/mod.rs +++ b/src/fs/dcache/mod.rs @@ -1,10 +1,10 @@ //! Generic directory cache and inode management primitives. +mod dcache; pub mod bridge; -pub mod mescloud; mod table; -pub use mescloud::MescloudDCache; +pub use dcache::MescloudDCache; pub use table::DCache; /// Common interface for inode control block types usable with `DCache`. From f227ac4d44281c7f3958502afbab829f5cf01e1f Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Fri, 6 Feb 2026 15:12:14 -0800 Subject: [PATCH 10/11] Update InodeControlBlock import in common.rs to use mescloud::dcache Co-Authored-By: Claude Opus 4.6 --- src/fs/mescloud/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fs/mescloud/common.rs b/src/fs/mescloud/common.rs index de08f1f..c2e5f27 100644 --- a/src/fs/mescloud/common.rs +++ b/src/fs/mescloud/common.rs @@ -2,7 +2,7 @@ use thiserror::Error; -pub(super) use crate::fs::dcache::mescloud::InodeControlBlock; +pub(super) use super::dcache::InodeControlBlock; // ── Error types ────────────────────────────────────────────────────────────── From c1015dd988760aa218d1b9882c857c1b774db845 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Fri, 6 Feb 2026 15:12:20 -0800 Subject: [PATCH 11/11] Update repo.rs imports to use mescloud::dcache for blocks_of_size Co-Authored-By: Claude Opus 4.6 --- src/fs/mescloud/repo.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fs/mescloud/repo.rs b/src/fs/mescloud/repo.rs index ca4baf3..7f28aff 100644 --- a/src/fs/mescloud/repo.rs +++ b/src/fs/mescloud/repo.rs @@ -14,7 +14,8 @@ use crate::fs::r#trait::{ LockOwner, OpenFile, OpenFlags, }; -use crate::fs::dcache::mescloud::{self as mescloud_dcache, MescloudDCache}; +use crate::fs::dcache::MescloudDCache; +use super::dcache as mescloud_dcache; pub use super::common::{ GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError, };