diff --git a/Cargo.lock b/Cargo.lock index 755898c..22cdef5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arrayvec" @@ -515,6 +515,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -690,6 +696,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "git-fs" version = "0.1.2-alpha.1" @@ -746,6 +765,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -964,6 +992,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -998,7 +1032,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1030,9 +1066,9 @@ dependencies = [ [[package]] name = "inquire" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae51d5da01ce7039024fbdec477767c102c454dbdb09d4e2a432ece705b1b25d" +checksum = "979f5ab9760427ada4fa5762b2d905e5b12704fb1fada07b6bfa66aeaa586f87" dependencies = [ "bitflags", "crossterm", @@ -1086,11 +1122,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libredox" @@ -1152,9 +1194,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mesa-dev" @@ -1399,6 +1441,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1696,21 +1748,21 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "saa" -version = "5.4.6" +version = "5.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990c7987befb5767abaf512320d695f7341533ec653eb84360b074d86f064316" +checksum = "da0ba8adb63e0deebd0744d8fc5bea394c08029159deaf680513fec1a3949144" [[package]] name = "scc" -version = "3.5.5" +version = "3.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccbc3c0af6257ff8e8b50d4e58974631aeaa2af1ea25c4e1398f7de3a7d7dd3" +checksum = "e4bd9d1727de391b6982925d830baad51692fa2aa6e337733c03d95121ca2793" dependencies = [ "saa", "sdd", @@ -2030,12 +2082,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2072,9 +2124,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -2095,9 +2147,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -2360,9 +2412,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-segmentation" @@ -2376,6 +2428,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unit-prefix" version = "0.5.2" @@ -2529,6 +2587,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -2588,6 +2655,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -2850,6 +2951,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -2882,18 +3065,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.38" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.38" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -2962,6 +3145,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/src/fs/inode_bridge.rs b/src/fs/icache/bridge.rs similarity index 100% rename from src/fs/inode_bridge.rs rename to src/fs/icache/bridge.rs diff --git a/src/fs/icache/cache.rs b/src/fs/icache/cache.rs new file mode 100644 index 0000000..675a3ba --- /dev/null +++ b/src/fs/icache/cache.rs @@ -0,0 +1,99 @@ +//! 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 ICache { + inode_table: HashMap, + next_fh: FileHandle, +} + +impl ICache { + /// Create a new `ICache` 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, + } + } + + /// 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 + } + + 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() + } + + /// 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 + } + } + } + + pub fn iter(&self) -> impl Iterator { + self.inode_table.iter() + } +} diff --git a/src/fs/icache/inode_factory.rs b/src/fs/icache/inode_factory.rs new file mode 100644 index 0000000..3f8f95e --- /dev/null +++ b/src/fs/icache/inode_factory.rs @@ -0,0 +1,18 @@ +use crate::fs::r#trait::Inode; + +/// Monotonically increasing inode allocator. +pub struct InodeFactory { + next_inode: Inode, +} + +impl InodeFactory { + pub fn new(start: Inode) -> Self { + Self { next_inode: start } + } + + pub fn allocate(&mut self) -> Inode { + let ino = self.next_inode; + self.next_inode += 1; + ino + } +} diff --git a/src/fs/icache/mod.rs b/src/fs/icache/mod.rs new file mode 100644 index 0000000..91f8bdd --- /dev/null +++ b/src/fs/icache/mod.rs @@ -0,0 +1,16 @@ +//! Generic directory cache and inode management primitives. + +pub mod bridge; +mod cache; +mod inode_factory; + +pub use cache::ICache; +pub use inode_factory::InodeFactory; + +/// Common interface for inode control block types usable with `ICache`. +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/local.rs b/src/fs/local.rs index 006e945..73e41de 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::icache::{ICache, 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, + icache: ICache, 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, + icache: ICache::new(1, abs_path), open_files: HashMap::new(), - next_fh: 1, } } fn abspath(&self) -> &PathBuf { &self - .inode_table - .get(&1) + .icache + .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.icache.contains(parent), "parent inode {parent} not in inode_table" ); - let parent_icb = self.inode_table.get(&parent).ok_or_else(|| { + let parent_icb = self.icache.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 + .icache + .entry_or_insert_icb(file_attr.common().ino, || InodeControlBlock { + rc: 0, + path: child_path, + children: None, + }); + *icb.rc_mut() += 1; Ok(file_attr) } @@ -261,11 +264,8 @@ impl Fs for LocalFs { Ok(file_attr?) } else { // No open path, so we have to do a painful stat on the path. - debug_assert!( - self.inode_table.contains_key(&ino), - "inode {ino} not in inode_table" - ); - let icb = self.inode_table.get(&ino).ok_or_else(|| { + debug_assert!(self.icache.contains(ino), "inode {ino} not in inode_table"); + let icb = self.icache.get_icb(ino).ok_or_else(|| { warn!( "GetAttr called on unknown inode {}. This is a programming bug", ino @@ -284,12 +284,9 @@ impl Fs for LocalFs { } async fn readdir(&mut self, ino: Inode) -> Result<&[DirEntry], ReadDirError> { - debug_assert!( - self.inode_table.contains_key(&ino), - "inode {ino} not in inode_table" - ); + debug_assert!(self.icache.contains(ino), "inode {ino} not in inode_table"); - let inode_cb = self.inode_table.get_mut(&ino).ok_or_else(|| { + let inode_cb = self.icache.get_icb(ino).ok_or_else(|| { warn!( parent = ino, "Readdir of unknown parent inode. Programming bug" @@ -315,7 +312,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.icache.get_icb_mut(ino).ok_or_else(|| { warn!(parent = ino, "inode disappeared. TOCTOU programming bug"); ReadDirError::InodeNotFound })?; @@ -324,11 +321,8 @@ impl Fs for LocalFs { } async fn open(&mut self, ino: Inode, flags: OpenFlags) -> Result { - debug_assert!( - self.inode_table.contains_key(&ino), - "inode {ino} not in inode_table" - ); - let icb = self.inode_table.get(&ino).ok_or_else(|| { + debug_assert!(self.icache.contains(ino), "inode {ino} not in inode_table"); + let icb = self.icache.get_icb(ino).ok_or_else(|| { warn!( "Open called on unknown inode {}. This is a programming bug", ino @@ -348,8 +342,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.icache.allocate_fh(); self.open_files.insert(fh, file); Ok(OpenFile { @@ -369,10 +362,7 @@ impl Fs for LocalFs { _lock_owner: Option, ) -> Result { // TODO(markovejnovic): Respect flags and lock_owner. - debug_assert!( - self.inode_table.contains_key(&ino), - "inode {ino} not in inode_table" - ); + debug_assert!(self.icache.contains(ino), "inode {ino} not in inode_table"); debug_assert!( self.open_files.contains_key(&fh), "file handle {fh} not in open_files" @@ -414,26 +404,9 @@ impl Fs for LocalFs { } async fn forget(&mut self, ino: Inode, nlookups: u64) { - debug_assert!( - self.inode_table.contains_key(&ino), - "inode {ino} not in inode_table" - ); + debug_assert!(self.icache.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.icache.forget(ino, nlookups); } async fn statfs(&mut self) -> Result { @@ -456,7 +429,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.icache.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..c97d76d 100644 --- a/src/fs/mescloud/common.rs +++ b/src/fs/mescloud/common.rs @@ -1,156 +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) -} -// ── Error types ────────────────────────────────────────────────────────────── +pub(super) use super::icache::InodeControlBlock; #[derive(Debug, Error)] pub enum LookupError { diff --git a/src/fs/mescloud/icache.rs b/src/fs/mescloud/icache.rs new file mode 100644 index 0000000..d6d9ab9 --- /dev/null +++ b/src/fs/mescloud/icache.rs @@ -0,0 +1,209 @@ +//! Mescloud-specific inode control block, helpers, and directory cache wrapper. + +use std::ffi::OsStr; +use std::time::SystemTime; + +use tracing::warn; + +use crate::fs::icache::{ICache, IcbLike, InodeFactory}; +use crate::fs::r#trait::{ + CommonFileAttr, DirEntry, DirEntryType, FileAttr, FilesystemStats, Inode, Permissions, +}; + +/// 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)) +} + +/// Mescloud-specific directory cache. +/// +/// Wraps [`ICache`] and adds inode allocation, attribute +/// caching, `ensure_child_inode`, and filesystem metadata. +pub struct MescloudICache { + inner: ICache, + inode_factory: InodeFactory, + fs_owner: (u32, u32), + block_size: u32, +} + +impl std::ops::Deref for MescloudICache { + type Target = ICache; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for MescloudICache { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl MescloudICache { + /// Create a new `MescloudICache`. 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 icache = Self { + inner: ICache::new(root_ino, "/"), + inode_factory: InodeFactory::new(root_ino + 1), + fs_owner, + block_size, + }; + + let now = SystemTime::now(); + let root_attr = FileAttr::Directory { + common: icache.make_common_file_attr(root_ino, 0o755, now, now), + }; + icache.cache_attr(root_ino, root_attr); + icache + } + + /// Allocate a new inode number. + pub fn allocate_inode(&mut self) -> Inode { + self.inode_factory.allocate() + } + + 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); + } + } + + /// 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) + } + + 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, + }, + } + } + + 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/mescloud/mod.rs b/src/fs/mescloud/mod.rs index cf26721..0d0e141 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::icache::bridge::HashMapBridge; use crate::fs::r#trait::{ DirEntry, DirEntryType, FileAttr, FileHandle, FilesystemStats, Fs, Inode, LockOwner, OpenFile, OpenFlags, @@ -19,13 +18,16 @@ const MESA_API_BASE_URL: &str = "https://staging.depot.mesa.dev/api/v1"; const MESA_API_BASE_URL: &str = "https://depot.mesa.dev/api/v1"; mod common; +use common::InodeControlBlock; pub use common::{GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError}; -use common::{InodeControlBlock, InodeFactory}; + +use icache::MescloudICache; mod org; pub use org::OrgConfig; use org::OrgFs; +pub mod icache; pub mod repo; /// Per-org wrapper with inode and file handle translation. @@ -47,11 +49,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, + icache: MescloudICache, /// Maps mesa-level org-root inodes → index into `org_slots`. org_inodes: HashMap, @@ -64,22 +62,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 { + icache: MescloudICache::new(Self::ROOT_NODE_INO, fs_owner, Self::BLOCK_SIZE), org_inodes: HashMap::new(), org_slots: orgs .map(|org_conf| { @@ -93,24 +77,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. @@ -135,7 +102,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.icache.get_icb(current).and_then(|icb| icb.parent) { if let Some(&idx) = self.org_inodes.get(&parent) { return Some(idx); } @@ -150,7 +117,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.icache.get_icb(existing_ino) && let Some(attr) = icb.attr { trace!( @@ -166,28 +133,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 + .icache + .make_common_file_attr(existing_ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, existing_ino, attr); + self.icache.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.icache.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.icache.insert_icb( ino, InodeControlBlock { rc: 0, @@ -206,23 +168,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.icache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.icache.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.icache.allocate_fh(); self.org_slots[slot_idx].bridge.insert_fh(fh, org_fh); fh } @@ -238,11 +192,10 @@ 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.icache.allocate_inode()); - self.inode_table - .entry(mesa_ino) - .or_insert_with(|| InodeControlBlock { + self.icache + .entry_or_insert_icb(mesa_ino, || InodeControlBlock { rc: 0, path: name.into(), parent: Some(parent_mesa_ino), @@ -266,7 +219,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.icache.contains(parent), "lookup: parent inode {parent} not in inode table" ); @@ -282,17 +235,8 @@ 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; - trace!( - ino, - org = org_name, - rc = icb.rc, - "lookup: resolved org inode" - ); + let rc = self.icache.inc_rc(ino); + trace!(ino, org = org_name, rc, "lookup: resolved org inode"); Ok(attr) } InodeRole::OrgOwned { idx } => { @@ -307,18 +251,9 @@ 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; - trace!( - mesa_ino, - org_ino, - rc = icb.rc, - "lookup: resolved via org delegation" - ); + self.icache.cache_attr(mesa_ino, mesa_attr); + let rc = self.icache.inc_rc(mesa_ino); + trace!(mesa_ino, org_ino, rc, "lookup: resolved via org delegation"); Ok(mesa_attr) } } @@ -330,20 +265,16 @@ impl Fs for MesaFS { ino: Inode, _fh: Option, ) -> Result { - let icb = self.inode_table.get(&ino).ok_or_else(|| { + self.icache.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.icache.contains(ino), "readdir: inode {ino} not in inode table" ); @@ -369,8 +300,8 @@ impl Fs for MesaFS { trace!(entry_count = entries.len(), "readdir: listing orgs"); let icb = self - .inode_table - .get_mut(&ino) + .icache + .get_icb_mut(ino) .ok_or(ReadDirError::InodeNotFound)?; Ok(icb.children.insert(entries)) } @@ -392,7 +323,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.icache.cache_attr(mesa_child_ino, mesa_attr); } mesa_entries.push(DirEntry { @@ -403,8 +334,8 @@ impl Fs for MesaFS { } let icb = self - .inode_table - .get_mut(&ino) + .icache + .get_icb_mut(ino) .ok_or(ReadDirError::InodeNotFound)?; Ok(icb.children.insert(mesa_entries)) } @@ -501,7 +432,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.icache.contains(ino), "forget: inode {ino} not in inode table" ); @@ -512,39 +443,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.icache.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.icache.statfs()) } } diff --git a/src/fs/mescloud/org.rs b/src/fs/mescloud/org.rs index 5786b05..9e3b56d 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::icache::MescloudICache; use super::repo::RepoFs; -use crate::fs::inode_bridge::HashMapBridge; +use crate::fs::icache::bridge::HashMapBridge; 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, + icache: MescloudICache, /// 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.icache.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.icache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.icache.cache_attr(ino, attr); return (ino, attr); } } // Allocate new - let ino = self.inode_factory.allocate(); + let ino = self.icache.allocate_inode(); let now = SystemTime::now(); - self.inode_table.insert( + self.icache.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.icache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.icache.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.icache.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, + icache: MescloudICache::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.icache.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.icache.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.icache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.icache.cache_attr(ino, attr); return (ino, attr); } } // Allocate new. - let ino = self.inode_factory.allocate(); + let ino = self.icache.allocate_inode(); trace!( ino, repo = repo_name, @@ -301,7 +245,7 @@ impl OrgFs { ); let now = SystemTime::now(); - self.inode_table.insert( + self.icache.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.icache.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.icache.make_common_file_attr(ino, 0o755, now, now), }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.icache.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.icache.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.icache.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.icache.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.icache.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.icache.inc_rc(ino); Ok(attr) } else { // Children of org root are repos. @@ -448,17 +380,8 @@ 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; - trace!( - ino, - repo = name_str, - rc = icb.rc, - "lookup: resolved repo inode" - ); + let rc = self.icache.inc_rc(ino); + trace!(ino, repo = name_str, rc, "lookup: resolved repo inode"); Ok(attr) } } @@ -486,11 +409,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.icache.inc_rc(ino); Ok(attr) } InodeRole::RepoOwned { idx } => { @@ -510,18 +429,9 @@ 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; - trace!( - org_ino, - repo_ino, - rc = icb.rc, - "lookup: resolved content inode" - ); + self.icache.cache_attr(org_ino, org_attr); + let rc = self.icache.inc_rc(org_ino); + trace!(org_ino, repo_ino, rc, "lookup: resolved content inode"); Ok(org_attr) } } @@ -533,20 +443,16 @@ impl Fs for OrgFs { ino: Inode, _fh: Option, ) -> Result { - let icb = self.inode_table.get(&ino).ok_or_else(|| { + self.icache.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.icache.contains(ino), "readdir: inode {ino} not in inode table" ); @@ -588,8 +494,8 @@ impl Fs for OrgFs { } let icb = self - .inode_table - .get_mut(&ino) + .icache + .get_icb_mut(ino) .ok_or(ReadDirError::InodeNotFound)?; Ok(icb.children.insert(entries)) } @@ -625,7 +531,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.icache.cache_attr(org_child_ino, org_attr); } else { trace!( repo_ino = entry.ino, @@ -642,8 +548,8 @@ impl Fs for OrgFs { } let icb = self - .inode_table - .get_mut(&ino) + .icache + .get_icb_mut(ino) .ok_or(ReadDirError::InodeNotFound)?; Ok(icb.children.insert(org_entries)) } @@ -746,7 +652,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.icache.contains(ino), "forget: inode {ino} not in inode table" ); @@ -762,42 +668,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.icache.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.icache.statfs()) } } diff --git a/src/fs/mescloud/repo.rs b/src/fs/mescloud/repo.rs index 3aaa98e..387415d 100644 --- a/src/fs/mescloud/repo.rs +++ b/src/fs/mescloud/repo.rs @@ -14,10 +14,11 @@ use crate::fs::r#trait::{ LockOwner, OpenFile, OpenFlags, }; -use super::common::{self, InodeControlBlock, InodeFactory}; pub use super::common::{ GetAttrError, LookupError, OpenError, ReadDirError, ReadError, ReleaseError, }; +use super::icache as mescloud_icache; +use super::icache::MescloudICache; /// A filesystem rooted at a single mesa repository. /// @@ -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, + icache: MescloudICache, 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, + icache: MescloudICache::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.icache.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.icache.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.icache.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.icache.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.icache.make_common_file_attr(ino, 0o644, now, now), size, - blocks: common::blocks_of_size(Self::BLOCK_SIZE, size), + blocks: mescloud_icache::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.icache.make_common_file_attr(ino, 0o755, now, now), }, }; - common::cache_attr(&mut self.inode_table, ino, attr); + self.icache.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.icache.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.icache.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.icache.contains(ino), "readdir: inode {ino} not in inode table" ); debug_assert!( matches!( - self.inode_table.get(&ino).and_then(|icb| icb.attr), + self.icache.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.icache.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) + .icache + .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.icache.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.icache.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.icache.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.icache.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.icache.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.icache.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.icache.statfs()) } } diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 4b3ade1..ef40322 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -1,5 +1,5 @@ pub mod fuser; -pub mod inode_bridge; +pub mod icache; pub mod local; pub mod mescloud; pub mod r#trait; diff --git a/src/fs/trait.rs b/src/fs/trait.rs index 6092dac..f4d9852 100644 --- a/src/fs/trait.rs +++ b/src/fs/trait.rs @@ -328,7 +328,7 @@ pub trait Fs { type ReaddirError: std::error::Error; type ReleaseError: std::error::Error; - /// For each lookup call made by the kernel, it expects the dcache to be updated with the + /// For each lookup call made by the kernel, it expects the icache to be updated with the /// returned `FileAttr`. async fn lookup(&mut self, parent: Inode, name: &OsStr) -> Result;