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", 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/dcache/dcache.rs b/src/fs/dcache/dcache.rs new file mode 100644 index 0000000..f802b8e --- /dev/null +++ b/src/fs/dcache/dcache.rs @@ -0,0 +1,209 @@ +//! Mescloud-specific directory cache wrapper. +//! +//! Composes [`DCache`] with inode allocation, attribute +//! caching, and filesystem-owner metadata. + +use std::ffi::OsStr; +use std::time::SystemTime; + +use tracing::warn; + +use crate::fs::r#trait::{ + CommonFileAttr, DirEntryType, FileAttr, FilesystemStats, Inode, Permissions, +}; +use crate::fs::mescloud::dcache::InodeControlBlock; + +use super::DCache; + +// ── InodeFactory ──────────────────────────────────────────────────────── + +/// Monotonically increasing inode allocator. +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 + } +} + +// ── MescloudDCache ────────────────────────────────────────────────────── + +/// Mescloud-specific directory cache. +/// +/// Wraps [`DCache`] and adds inode allocation, attribute +/// caching, `ensure_child_inode`, and filesystem metadata. +pub struct MescloudDCache { + inner: DCache, + inode_factory: InodeFactory, + fs_owner: (u32, u32), + block_size: u32, +} + +impl std::ops::Deref for MescloudDCache { + type Target = DCache; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +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 { + inner: DCache::new(root_ino, "/"), + inode_factory: InodeFactory::new(root_ino + 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), + }; + 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() + } + + // ── Attr caching ──────────────────────────────────────────────────── + + pub fn get_attr(&self, ino: Inode) -> Option { + 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.inner.get_icb_mut(ino) { + icb.attr = Some(attr); + } + } + + // ── 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. + let existing = self + .inner + .iter() + .find(|&(&_ino, icb)| icb.parent == Some(parent) && icb.path.as_os_str() == name) + .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); + } + + 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.inner.insert_icb( + 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 fs_owner(&self) -> (u32, u32) { + self.fs_owner + } + + 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.inner.inode_count() as u64, + free_inodes: 0, + available_inodes: 0, + filesystem_id: 0, + mount_flags: 0, + max_filename_length: 255, + } + } +} diff --git a/src/fs/dcache/mod.rs b/src/fs/dcache/mod.rs new file mode 100644 index 0000000..0b4d530 --- /dev/null +++ b/src/fs/dcache/mod.rs @@ -0,0 +1,16 @@ +//! Generic directory cache and inode management primitives. + +mod dcache; +pub mod bridge; +mod table; + +pub use dcache::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 88b2ee9..c2e5f27 100644 --- a/src/fs/mescloud/common.rs +++ b/src/fs/mescloud/common.rs @@ -1,154 +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 - } -} - -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) 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) -} +pub(super) use super::dcache::InodeControlBlock; // ── Error types ────────────────────────────────────────────────────────────── 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 ea797a9..42471e4 100644 --- a/src/fs/mescloud/mod.rs +++ b/src/fs/mescloud/mod.rs @@ -1,13 +1,12 @@ use std::collections::HashMap; use std::ffi::OsStr; -use std::time::SystemTime; use bytes::Bytes; 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, @@ -15,13 +14,16 @@ use crate::fs::r#trait::{ mod common; pub use common::{GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError}; -use common::{InodeControlBlock, InodeFactory}; +use common::InodeControlBlock; + +use crate::fs::dcache::MescloudDCache; mod org; pub use org::OrgConfig; use org::OrgFs; pub mod repo; +pub mod dcache; /// Per-org wrapper with inode and file handle translation. struct OrgSlot { @@ -42,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: MescloudDCache, /// Maps mesa-level org-root inodes → index into `org_slots`. org_inodes: HashMap, @@ -59,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: MescloudDCache::new(Self::ROOT_NODE_INO, fs_owner, Self::BLOCK_SIZE), org_inodes: HashMap::new(), org_slots: orgs .map(|org_conf| { @@ -86,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. @@ -128,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); } @@ -143,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!( @@ -159,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, @@ -199,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 } @@ -231,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 } @@ -259,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" ); @@ -275,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) @@ -300,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) @@ -323,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" ); @@ -362,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)) } @@ -385,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 { @@ -396,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)) } @@ -494,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" ); @@ -505,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()) } } diff --git a/src/fs/mescloud/org.rs b/src/fs/mescloud/org.rs index 5786b05..8b21a39 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,12 +8,13 @@ use mesa_dev::Mesa as MesaClient; use secrecy::SecretString; use tracing::{instrument, trace, warn}; -use super::common::{self, InodeControlBlock, InodeFactory}; +use super::common::InodeControlBlock; 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, @@ -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: MescloudDCache, /// 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: MescloudDCache::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()) } } diff --git a/src/fs/mescloud/repo.rs b/src/fs/mescloud/repo.rs index 3aaa98e..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 super::common::{self, InodeControlBlock, InodeFactory}; +use crate::fs::dcache::MescloudDCache; +use super::dcache as mescloud_dcache; pub use super::common::{ GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError, }; @@ -29,12 +30,7 @@ pub struct RepoFs { repo_name: String, ref_: String, - fs_owner: (u32, u32), - - inode_table: HashMap, - inode_factory: InodeFactory, - - next_fh: FileHandle, + dcache: MescloudDCache, open_files: HashMap, } @@ -50,44 +46,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: MescloudDCache::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 +63,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 +78,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 +112,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 +129,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: mescloud_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 +155,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 +203,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 +212,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 +260,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 +312,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()) } } 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;