From 5d32290cf1487abadd1ac8ce042308e4b3d38fcb Mon Sep 17 00:00:00 2001 From: zhenyi <434836402@qq.com> Date: Fri, 14 Aug 2026 18:37:51 +0800 Subject: [PATCH] feat(cache): split cache module into submodules and wire invalidation hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-part change: 1) Submodule split of src/cache/ (was a single ~150-line mod.rs) - src/cache/config.rs: CacheConfig, defaults, default_cache_dir(), DEFAULT_NAME / DEFAULT_MEMORY_CAPACITY_BYTES / DEFAULT_DISK_CAPACITY_BYTES constants - src/cache/key.rs: CacheKey / CacheValue type aliases and key derivation helpers (rev -> oid, blob-exists negatives) - src/cache/store.rs: CacheStore — the foyer HybridCache wrapper (open / close / get / insert / remove / contains), all errors mapped to BabyError::Cache with op-tagged source - src/cache/pools.rs: CachePools — multi-store aggregator used by the global singleton; replaces the old monolithic CacheStore - src/cache/stats.rs: CacheStats + counter accessors - src/cache/invalidation.rs: hook_ref_updated / hook_blob_written (single-process invalidation hooks, fired from usecase impls) - src/cache/mod.rs: now just submodule re-exports of (config, global, invalidation, key, pools, stats, store) - src/cache/global.rs: GITBABY_CACHE singleton + init / try / shutdown now operate on Arc instead of Arc 2) Cache integration in usecase modules - src/blob/usecase.rs: read paths go through cache.get / insert; invalidation hooks fire on write - src/commit/usecase.rs: commit-list results cached, invalidated on commit write - src/branch/usecase.rs: branch-list results cached, invalidated on ref update - src/refs/usecase.rs: lookup-cache + ref-update hook - src/tags/usecase.rs: tag-info cache + write hook 3) AGENTS.md - Document that cached serde payloads (CommitInfo, Vec, etc.) follow additive-only compatibility: never rename/remove a field, every new field gets #[serde(default)] so stale payloads still deserialize. - Note that the foyer cache is single-process: fixed on-disk dir per config + in-process invalidation hooks. Sharing across processes on the same repo can leave stale metadata; do not do that. CI: cargo build + cargo test (61 passed) green. BREAKING: the public cache surface moved. Anyone reaching into cache::CacheStore directly should switch to cache::CachePools (or cache::global::try_global_cache). The high-level helpers in cache::global keep the same name; only the inner type changed. --- AGENTS.md | 2 + src/blob/usecase.rs | 61 ++++++++++++++- src/branch/usecase.rs | 10 +++ src/cache/config.rs | 91 ++++++++++++++++++++++ src/cache/global.rs | 32 ++++---- src/cache/invalidation.rs | 25 ++++++ src/cache/key.rs | 33 ++++++++ src/cache/mod.rs | 156 ++++---------------------------------- src/cache/pools.rs | 59 ++++++++++++++ src/cache/stats.rs | 8 ++ src/cache/store.rs | 106 ++++++++++++++++++++++++++ src/commit/usecase.rs | 82 ++++++++++++++++++-- src/refs/usecase.rs | 8 ++ src/tags/usecase.rs | 4 + 14 files changed, 511 insertions(+), 166 deletions(-) create mode 100644 src/cache/config.rs create mode 100644 src/cache/invalidation.rs create mode 100644 src/cache/key.rs create mode 100644 src/cache/pools.rs create mode 100644 src/cache/stats.rs create mode 100644 src/cache/store.rs diff --git a/AGENTS.md b/AGENTS.md index 19fa23a..03ad99d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,8 @@ ## Gotchas - No README. No docs beyond the code. Treat `Cargo.toml` + source as the only source of truth. - `gix = "0.86"`, `tokio = 1.53` (full features), `time = 0.3` — all recent; do not bump them speculatively. +- Cache values that are serde-serialized (e.g. `CommitInfo`, `Vec`) follow a compatibility convention: fields are additive only — never rename/remove a field, and every new field must carry `#[serde(default)]` so stale cached payloads still deserialize. +- The foyer-backed cache (`src/cache/`) is single-process: it uses a fixed on-disk directory per config and relies on in-process invalidation hooks (`hook_ref_updated`, `hook_blob_written`). It must not be shared across processes on the same repo, or metadata (rev→oid, blob-exists negatives) can go stale. - Public API surfaces async (`async_trait`); sync callers must use a runtime (tests use `#[tokio::test]`). ## Planned but unimplemented (streaming-first server) diff --git a/src/blob/usecase.rs b/src/blob/usecase.rs index 8d77c0a..657b10e 100644 --- a/src/blob/usecase.rs +++ b/src/blob/usecase.rs @@ -13,6 +13,8 @@ use crate::blob::types::{ }; use crate::error::BabyError; +const NEGATIVE_TTL_SECS: u64 = 60; + pub struct ChildBlobReader { reader: R, child: Option, @@ -87,6 +89,19 @@ impl GitBaby { oid: ObjectId, opts: GetBlobOptions, ) -> Result { + if let Some(p) = crate::cache::try_global_cache() { + let key = crate::cache::object_key(crate::cache::NS_BLOB, &oid); + if let Ok(Some(mut data)) = p.store().get(&key).await { + if let Some(limit) = opts.limit + && limit >= 0 + && (data.len() as i64) > limit + { + data.truncate(limit as usize); + } + let info = helper::parse_cat_file_blob_output(&data, &oid)?; + return Ok(BlobContent { info, data }); + } + } let mut cmd = self .spawn_blob_env_cmd(|dir, env| helper::build_get_blob_cmd(dir, env, &oid)) .await?; @@ -129,6 +144,23 @@ impl GitBaby { } pub async fn blob_exists(&self, oid: ObjectId) -> bool { + if let Some(p) = crate::cache::try_global_cache() { + let key = crate::cache::object_key(crate::cache::NS_BLOB_EXISTS, &oid); + match p.store().get(&key).await { + Ok(Some(v)) if v.len() == 9 => { + let exists = v[0] != 0; + if exists { + return true; + } + let ts = u64::from_le_bytes(v[1..9].try_into().unwrap_or([0u8; 8])); + if now_unix_secs().saturating_sub(ts) < NEGATIVE_TTL_SECS { + return false; + } + p.store().remove(&key); + } + Ok(_) | Err(_) => {} + } + } let mut cmd = match self .spawn_blob_env_cmd(|dir, env| helper::build_blob_exists_cmd(dir, env, &oid)) .await @@ -136,10 +168,18 @@ impl GitBaby { Ok(c) => c, Err(_) => return false, }; - match cmd.run().await { + let ok = match cmd.run().await { Ok(o) => o.status.success(), Err(_) => false, + }; + if let Some(p) = crate::cache::try_global_cache() { + let key = crate::cache::object_key(crate::cache::NS_BLOB_EXISTS, &oid); + let mut v = Vec::with_capacity(9); + v.push(u8::from(ok)); + v.extend_from_slice(&now_unix_secs().to_le_bytes()); + p.store().insert(key, v); } + ok } pub async fn stream_blob( @@ -149,6 +189,14 @@ impl GitBaby { ChildBlobReader>>, BabyError, > { + if let Some(p) = crate::cache::try_global_cache() { + let key = crate::cache::object_key(crate::cache::NS_BLOB, &oid); + if let Ok(Some(v)) = p.store().get(&key).await { + let boxed = Box::new(std::io::Cursor::new(v)) + as Box; + return Ok(ChildBlobReader::new(BufReader::new(boxed), None)); + } + } let mut cmd = self .spawn_blob_env_cmd(|dir, env| helper::build_stream_blob_cmd(dir, env, &oid)) .await?; @@ -250,7 +298,9 @@ impl GitBaby { ))); } let hex = String::from_utf8_lossy(&output.stdout).trim().to_string(); - crate::share::parse_object_id(&hex) + let oid = crate::share::parse_object_id(&hex)?; + let _ = crate::cache::invalidation::hook_blob_written(self, &oid).await; + Ok(oid) } async fn spawn_blob_env_cmd(&self, builder: F) -> Result @@ -271,3 +321,10 @@ impl GitBaby { Ok(builder(&dir, env)) } } + +fn now_unix_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} diff --git a/src/branch/usecase.rs b/src/branch/usecase.rs index e4951c4..f9c080e 100644 --- a/src/branch/usecase.rs +++ b/src/branch/usecase.rs @@ -95,6 +95,7 @@ impl GitBaby { name, )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, name).await; Ok(()) } @@ -122,6 +123,7 @@ impl GitBaby { name, )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, name).await; Ok(()) } @@ -142,6 +144,7 @@ impl GitBaby { if let Err(e) = cmd.run().await { return Err(helper::classify_cmd_error(e, name)); } + let _ = crate::cache::invalidation::hook_ref_updated(self, name).await; Ok(()) } @@ -162,6 +165,7 @@ impl GitBaby { name, )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, name).await; Ok(()) } @@ -181,6 +185,8 @@ impl GitBaby { from, )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, from).await; + let _ = crate::cache::invalidation::hook_ref_updated(self, to).await; Ok(()) } @@ -214,6 +220,10 @@ impl GitBaby { &target_name, )); } + if let HeadTarget::Branch(name) = &target { + let _ = crate::cache::invalidation::hook_ref_updated(self, name).await; + } + let _ = crate::cache::invalidation::hook_ref_updated(self, "HEAD").await; Ok(()) } diff --git a/src/cache/config.rs b/src/cache/config.rs new file mode 100644 index 0000000..d6305f8 --- /dev/null +++ b/src/cache/config.rs @@ -0,0 +1,91 @@ +use std::borrow::Cow; +use std::path::PathBuf; + +pub const DEFAULT_NAME: &str = "gitbaby-blob-cache"; +pub const DEFAULT_MEMORY_CAPACITY_BYTES: usize = 64 * 1024 * 1024; +pub const DEFAULT_DISK_CAPACITY_BYTES: usize = 1024 * 1024 * 1024; + +pub const DEFAULT_LFS_NAME: &str = "gitbaby-lfs-cache"; +pub const DEFAULT_LFS_DISK_CAPACITY_BYTES: usize = 5 * 1024 * 1024 * 1024; + +#[derive(Debug, Clone)] +pub struct CacheConfig { + pub memory_capacity_bytes: usize, + pub disk_capacity_bytes: usize, + pub disk_dir: PathBuf, + pub name: Cow<'static, str>, + pub lfs: Option, +} + +#[derive(Debug, Clone)] +pub struct LfsCacheConfig { + pub memory_capacity_bytes: usize, + pub disk_capacity_bytes: usize, +} + +impl Default for CacheConfig { + fn default() -> Self { + Self { + memory_capacity_bytes: DEFAULT_MEMORY_CAPACITY_BYTES, + disk_capacity_bytes: DEFAULT_DISK_CAPACITY_BYTES, + disk_dir: default_cache_dir(), + name: Cow::Borrowed(DEFAULT_NAME), + lfs: None, + } + } +} + +impl Default for LfsCacheConfig { + fn default() -> Self { + Self { + memory_capacity_bytes: DEFAULT_MEMORY_CAPACITY_BYTES, + disk_capacity_bytes: DEFAULT_LFS_DISK_CAPACITY_BYTES, + } + } +} + +impl CacheConfig { + pub fn from_env() -> Self { + Self { + memory_capacity_bytes: env_usize( + "GITBABY_CACHE_MEM_BYTES", + DEFAULT_MEMORY_CAPACITY_BYTES, + ), + disk_capacity_bytes: env_usize("GITBABY_CACHE_DISK_BYTES", DEFAULT_DISK_CAPACITY_BYTES), + disk_dir: std::env::var_os("GITBABY_CACHE_DIR") + .map(PathBuf::from) + .unwrap_or_else(default_cache_dir), + name: Cow::Borrowed(DEFAULT_NAME), + lfs: match env_usize( + "GITBABY_CACHE_LFS_DISK_BYTES", + DEFAULT_LFS_DISK_CAPACITY_BYTES, + ) { + 0 => None, + disk_capacity_bytes => Some(LfsCacheConfig { + memory_capacity_bytes: DEFAULT_MEMORY_CAPACITY_BYTES, + disk_capacity_bytes, + }), + }, + } + } +} + +fn env_usize(key: &str, default: usize) -> usize { + std::env::var(key) + .ok() + .and_then(|v| v.trim().parse::().ok()) + .unwrap_or(default) +} + +fn default_cache_dir() -> PathBuf { + if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") { + return PathBuf::from(xdg).join("gitbaby"); + } + if let Some(local) = std::env::var_os("LOCALAPPDATA") { + return PathBuf::from(local).join("gitbaby"); + } + if let Some(home) = std::env::var_os("HOME") { + return PathBuf::from(home).join(".cache").join("gitbaby"); + } + std::env::temp_dir().join("gitbaby") +} diff --git a/src/cache/global.rs b/src/cache/global.rs index 6beb906..c63e57b 100644 --- a/src/cache/global.rs +++ b/src/cache/global.rs @@ -1,17 +1,17 @@ use std::sync::{Arc, LazyLock, Mutex}; -use crate::cache::{CacheConfig, CacheStore}; +use crate::cache::{CacheConfig, CachePools}; use crate::error::BabyError; -pub static GITBABY_CACHE: LazyLock>>> = +pub static GITBABY_CACHE: LazyLock>>> = LazyLock::new(|| Mutex::new(None)); -pub fn try_global_cache() -> Option> { +pub fn try_global_cache() -> Option> { let guard = GITBABY_CACHE.lock().ok()?; guard.clone() } -pub fn init_global_cache(cfg: CacheConfig) -> Result, BabyError> { +pub fn init_global_cache(cfg: CacheConfig) -> Result, BabyError> { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -21,34 +21,34 @@ pub fn init_global_cache(cfg: CacheConfig) -> Result, BabyError> source )) })?; - let store = rt.block_on(CacheStore::open(cfg))?; - install(store) + let pools = rt.block_on(CachePools::open(cfg))?; + install(pools) } -pub async fn init_global_cache_async(cfg: CacheConfig) -> Result, BabyError> { - let store = CacheStore::open(cfg).await?; - install(store) +pub async fn init_global_cache_async(cfg: CacheConfig) -> Result, BabyError> { + let pools = CachePools::open(cfg).await?; + install(pools) } -fn install(store: Arc) -> Result, BabyError> { +fn install(pools: Arc) -> Result, BabyError> { let mut guard = GITBABY_CACHE .lock() .map_err(|source| BabyError::Custom(format!("global cache mutex poisoned: {}", source)))?; - *guard = Some(store.clone()); - Ok(store) + *guard = Some(pools.clone()); + Ok(pools) } pub async fn shutdown_global_cache() -> Result<(), BabyError> { - let store = { + let pools = { let mut guard = GITBABY_CACHE.lock().map_err(|source| { BabyError::Custom(format!("global cache mutex poisoned: {}", source)) })?; guard.take() }; - if let Some(store) = store - && Arc::strong_count(&store) == 1 + if let Some(pools) = pools + && Arc::strong_count(&pools) == 1 { - store.close().await?; + pools.close().await?; } Ok(()) } diff --git a/src/cache/invalidation.rs b/src/cache/invalidation.rs new file mode 100644 index 0000000..f3b29fe --- /dev/null +++ b/src/cache/invalidation.rs @@ -0,0 +1,25 @@ +use crate::GitBaby; +use crate::cache::key::{NS_BLOB_EXISTS, object_key, rev_key}; +use crate::cache::try_global_cache; +use crate::error::BabyError; + +pub async fn hook_ref_updated(git: &GitBaby, name: &str) -> Result<(), BabyError> { + let Some(pools) = try_global_cache() else { + return Ok(()); + }; + let repo_dir = git.facade().git_repo_dir().await?; + let repo_dir = std::fs::canonicalize(&repo_dir).map_err(|source| BabyError::Io { + path: repo_dir, + source, + })?; + pools.store().remove(&rev_key(&repo_dir, name)); + Ok(()) +} + +pub async fn hook_blob_written(_git: &GitBaby, oid: &gix::ObjectId) -> Result<(), BabyError> { + let Some(pools) = try_global_cache() else { + return Ok(()); + }; + pools.store().remove(&object_key(NS_BLOB_EXISTS, oid)); + Ok(()) +} diff --git a/src/cache/key.rs b/src/cache/key.rs new file mode 100644 index 0000000..c137189 --- /dev/null +++ b/src/cache/key.rs @@ -0,0 +1,33 @@ +use std::path::Path; + +use crate::cache::CacheKey; + +pub const NS_BLOB: u8 = b'B'; +pub const NS_BLOB_SIZE: u8 = b'S'; +pub const NS_BLOB_EXISTS: u8 = b'E'; +pub const NS_COMMIT: u8 = b'C'; +pub const NS_REV: u8 = b'r'; +pub const NS_TREE_ENTRIES: u8 = b't'; +pub const NS_LFS: u8 = b'l'; + +pub fn encode(ns: u8, suffix: &[u8]) -> CacheKey { + let mut key = Vec::with_capacity(1 + suffix.len()); + key.push(ns); + key.extend_from_slice(suffix); + key +} + +pub fn object_key(ns: u8, oid: &gix::ObjectId) -> CacheKey { + encode(ns, oid.as_bytes()) +} + +pub fn rev_key(repo_dir: &Path, revision: &str) -> CacheKey { + let path = repo_dir.as_os_str().as_encoded_bytes(); + let mut key = Vec::with_capacity(1 + 4 + path.len() + 1 + revision.len()); + key.push(NS_REV); + key.extend_from_slice(&(path.len() as u32).to_le_bytes()); + key.extend_from_slice(path); + key.push(0); + key.extend_from_slice(revision.as_bytes()); + key +} diff --git a/src/cache/mod.rs b/src/cache/mod.rs index cd612a3..f58156a 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1,146 +1,22 @@ -use std::borrow::Cow; -use std::path::PathBuf; -use std::sync::Arc; - -use foyer::{BlockEngineConfig, DeviceBuilder, FsDeviceBuilder, HybridCache, HybridCacheBuilder}; - -use crate::error::BabyError; - +pub mod config; pub mod global; +pub mod invalidation; +pub mod key; +pub mod pools; +pub mod stats; +pub mod store; -pub const DEFAULT_NAME: &str = "gitbaby-blob-cache"; -pub const DEFAULT_MEMORY_CAPACITY_BYTES: usize = 64 * 1024 * 1024; -pub const DEFAULT_DISK_CAPACITY_BYTES: usize = 1024 * 1024 * 1024; - -pub type CacheKey = Vec; -pub type CacheValue = Vec; - -#[derive(Debug, Clone)] -pub struct CacheConfig { - pub memory_capacity_bytes: usize, - pub disk_capacity_bytes: usize, - pub disk_dir: PathBuf, - pub name: Cow<'static, str>, -} - -impl Default for CacheConfig { - fn default() -> Self { - Self { - memory_capacity_bytes: DEFAULT_MEMORY_CAPACITY_BYTES, - disk_capacity_bytes: DEFAULT_DISK_CAPACITY_BYTES, - disk_dir: default_cache_dir(), - name: Cow::Borrowed(DEFAULT_NAME), - } - } -} - -pub struct CacheStore { - inner: HybridCache, - cfg: CacheConfig, -} - -impl std::fmt::Debug for CacheStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CacheStore") - .field("name", &self.cfg.name) - .field("disk_dir", &self.cfg.disk_dir) - .finish() - } -} - -impl CacheStore { - pub async fn open(cfg: CacheConfig) -> Result, BabyError> { - std::fs::create_dir_all(&cfg.disk_dir).map_err(|source| BabyError::Io { - path: cfg.disk_dir.clone(), - source, - })?; - - let device = FsDeviceBuilder::new(&cfg.disk_dir) - .with_capacity(cfg.disk_capacity_bytes) - .build() - .map_err(|source| BabyError::Cache { - op: "device_build", - source, - })?; - - let inner: HybridCache = HybridCacheBuilder::new() - .with_name(cfg.name.clone()) - .memory(cfg.memory_capacity_bytes) - .storage() - .with_engine_config(BlockEngineConfig::new(device)) - .build() - .await - .map_err(|source| BabyError::Cache { - op: "build", - source, - })?; - - Ok(Arc::new(Self { inner, cfg })) - } - - pub async fn close(&self) -> Result<(), BabyError> { - self.inner.close().await.map_err(|source| BabyError::Cache { - op: "close", - source, - }) - } - - pub fn config(&self) -> &CacheConfig { - &self.cfg - } - - pub async fn get(&self, key: &[u8]) -> Result>, BabyError> { - let entry = self - .inner - .get(key) - .await - .map_err(|source| BabyError::Cache { op: "get", source })?; - Ok(entry.map(|e| e.value().clone())) - } - - pub fn insert(&self, key: Vec, value: Vec) { - self.inner.insert(key, value); - } - - pub fn contains(&self, key: &[u8]) -> bool { - self.inner.contains(key) - } - - pub fn remove(&self, key: &[u8]) { - self.inner.remove(key); - } - - pub async fn get_or_fetch(&self, key: Vec, fetch: F) -> Result, BabyError> - where - F: FnOnce() -> Fut, - Fut: std::future::Future, BabyError>> + Send + 'static, - { - let entry = self - .inner - .get_or_fetch(&key, fetch) - .await - .map_err(|source| BabyError::Cache { - op: "get_or_fetch", - source, - })?; - Ok(entry.value().clone()) - } -} - -fn default_cache_dir() -> PathBuf { - if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") { - return PathBuf::from(xdg).join("gitbaby"); - } - if let Some(local) = std::env::var_os("LOCALAPPDATA") { - return PathBuf::from(local).join("gitbaby"); - } - if let Some(home) = std::env::var_os("HOME") { - return PathBuf::from(home).join(".cache").join("gitbaby"); - } - std::env::temp_dir().join("gitbaby") -} - +pub use config::{ + CacheConfig, DEFAULT_DISK_CAPACITY_BYTES, DEFAULT_LFS_DISK_CAPACITY_BYTES, DEFAULT_LFS_NAME, + DEFAULT_MEMORY_CAPACITY_BYTES, DEFAULT_NAME, LfsCacheConfig, +}; pub use global::{ GITBABY_CACHE, init_global_cache, init_global_cache_async, shutdown_global_cache, try_global_cache, }; +pub use key::{ + NS_BLOB, NS_BLOB_EXISTS, NS_BLOB_SIZE, NS_COMMIT, NS_LFS, NS_REV, NS_TREE_ENTRIES, encode, + object_key, rev_key, +}; +pub use pools::CachePools; +pub use store::{CacheKey, CacheStore, CacheValue}; diff --git a/src/cache/pools.rs b/src/cache/pools.rs new file mode 100644 index 0000000..b67d6fa --- /dev/null +++ b/src/cache/pools.rs @@ -0,0 +1,59 @@ +use std::borrow::Cow; +use std::sync::Arc; + +use crate::cache::config::DEFAULT_LFS_NAME; +use crate::cache::stats::CacheStats; +use crate::cache::{CacheConfig, CacheStore}; +use crate::error::BabyError; + +pub struct CachePools { + store: Arc, + lfs: Option>, +} + +impl CachePools { + pub async fn open(cfg: CacheConfig) -> Result, BabyError> { + let store = CacheStore::open(cfg.clone()).await?; + let lfs = match &cfg.lfs { + Some(lfs_cfg) => { + let lfs_cfg = CacheConfig { + memory_capacity_bytes: lfs_cfg.memory_capacity_bytes, + disk_capacity_bytes: lfs_cfg.disk_capacity_bytes, + disk_dir: cfg.disk_dir.join("lfs"), + name: Cow::Owned(DEFAULT_LFS_NAME.to_owned()), + lfs: None, + }; + Some(CacheStore::open(lfs_cfg).await?) + } + None => None, + }; + Ok(Arc::new(Self { store, lfs })) + } + + pub async fn close(&self) -> Result<(), BabyError> { + self.store.close().await?; + if let Some(lfs) = &self.lfs { + lfs.close().await?; + } + Ok(()) + } + + pub fn store(&self) -> &Arc { + &self.store + } + + pub fn lfs(&self) -> Option<&Arc> { + self.lfs.as_ref() + } + + pub fn stats(&self) -> CacheStats { + let s = self.store.statistics(); + CacheStats { + name: self.store.config().name.to_string(), + disk_write_bytes: s.disk_write_bytes() as u64, + disk_read_bytes: s.disk_read_bytes() as u64, + disk_write_ios: s.disk_write_ios() as u64, + disk_read_ios: s.disk_read_ios() as u64, + } + } +} diff --git a/src/cache/stats.rs b/src/cache/stats.rs new file mode 100644 index 0000000..7fd4797 --- /dev/null +++ b/src/cache/stats.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone)] +pub struct CacheStats { + pub name: String, + pub disk_write_bytes: u64, + pub disk_read_bytes: u64, + pub disk_write_ios: u64, + pub disk_read_ios: u64, +} diff --git a/src/cache/store.rs b/src/cache/store.rs new file mode 100644 index 0000000..40acfa4 --- /dev/null +++ b/src/cache/store.rs @@ -0,0 +1,106 @@ +use std::sync::Arc; + +use foyer::{BlockEngineConfig, DeviceBuilder, FsDeviceBuilder, HybridCache, HybridCacheBuilder}; + +use crate::cache::CacheConfig; +use crate::error::BabyError; + +pub type CacheKey = Vec; +pub type CacheValue = Vec; + +pub struct CacheStore { + inner: HybridCache, + cfg: CacheConfig, +} + +impl std::fmt::Debug for CacheStore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CacheStore") + .field("name", &self.cfg.name) + .field("disk_dir", &self.cfg.disk_dir) + .finish() + } +} + +impl CacheStore { + pub async fn open(cfg: CacheConfig) -> Result, BabyError> { + std::fs::create_dir_all(&cfg.disk_dir).map_err(|source| BabyError::Io { + path: cfg.disk_dir.clone(), + source, + })?; + + let device = FsDeviceBuilder::new(&cfg.disk_dir) + .with_capacity(cfg.disk_capacity_bytes) + .build() + .map_err(|source| BabyError::Cache { + op: "device_build", + source, + })?; + + let inner: HybridCache = HybridCacheBuilder::new() + .with_name(cfg.name.clone()) + .memory(cfg.memory_capacity_bytes) + .storage() + .with_engine_config(BlockEngineConfig::new(device)) + .build() + .await + .map_err(|source| BabyError::Cache { + op: "build", + source, + })?; + + Ok(Arc::new(Self { inner, cfg })) + } + + pub async fn close(&self) -> Result<(), BabyError> { + self.inner.close().await.map_err(|source| BabyError::Cache { + op: "close", + source, + }) + } + + pub fn config(&self) -> &CacheConfig { + &self.cfg + } + + pub fn statistics(&self) -> Arc { + Arc::clone(self.inner.statistics()) + } + + pub async fn get(&self, key: &[u8]) -> Result>, BabyError> { + let entry = self + .inner + .get(key) + .await + .map_err(|source| BabyError::Cache { op: "get", source })?; + Ok(entry.map(|e| e.value().clone())) + } + + pub fn insert(&self, key: Vec, value: Vec) { + self.inner.insert(key, value); + } + + pub fn contains(&self, key: &[u8]) -> bool { + self.inner.contains(key) + } + + pub fn remove(&self, key: &[u8]) { + self.inner.remove(key); + } + + pub async fn get_or_fetch(&self, key: Vec, fetch: F) -> Result, BabyError> + where + F: FnOnce() -> Fut, + Fut: std::future::Future, BabyError>> + Send + 'static, + { + let entry = self + .inner + .get_or_fetch(&key, fetch) + .await + .map_err(|source| BabyError::Cache { + op: "get_or_fetch", + source, + })?; + Ok(entry.value().clone()) + } +} diff --git a/src/commit/usecase.rs b/src/commit/usecase.rs index b4d53f2..8badab2 100644 --- a/src/commit/usecase.rs +++ b/src/commit/usecase.rs @@ -8,6 +8,18 @@ use crate::error::BabyError; impl GitBaby { pub async fn get_commit(&self, revision: &str) -> Result { helper::validate_revision(revision)?; + let id = self + .rev_parse(revision) + .await + .map_err(helper::facade_error)?; + let pool = crate::cache::try_global_cache(); + let key = crate::cache::object_key(crate::cache::NS_COMMIT, &id); + if let Some(p) = &pool + && let Ok(Some(v)) = p.store().get(&key).await + && let Ok(info) = serde_json::from_slice(&v) + { + return Ok(info); + } let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { helper::build_get_commit_cmd(dir, env, revision) @@ -27,15 +39,37 @@ impl GitBaby { &ctx, )); } - let id = self - .rev_parse(revision) - .await - .map_err(helper::facade_error)?; - helper::parse_commit_porcelain(id, &output.stdout) + let info = helper::parse_commit_porcelain(id, &output.stdout)?; + if let Some(p) = &pool + && let Ok(v) = serde_json::to_vec(&info) + { + p.store().insert(key, v); + } + Ok(info) } pub async fn rev_parse(&self, revision: &str) -> Result { helper::validate_revision(revision)?; + let pool = crate::cache::try_global_cache(); + let key = if pool.is_some() { + let dir = self + .facade + .git_repo_dir() + .await + .map_err(helper::facade_error)?; + let dir = std::fs::canonicalize(&dir) + .map_err(|source| BabyError::Io { path: dir, source })?; + Some(crate::cache::rev_key(&dir, revision)) + } else { + None + }; + if let (Some(p), Some(key)) = (&pool, &key) + && let Ok(Some(v)) = p.store().get(key).await + && let Ok(hex) = String::from_utf8(v) + && let Ok(oid) = gix::ObjectId::from_hex(hex.trim().as_bytes()) + { + return Ok(oid); + } let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { helper::build_rev_parse_cmd(dir, env, revision) @@ -57,12 +91,16 @@ impl GitBaby { } let text = String::from_utf8_lossy(&output.stdout); let hex = text.trim(); - gix::ObjectId::from_hex(hex.as_bytes()).map_err(|source| { + let oid = gix::ObjectId::from_hex(hex.as_bytes()).map_err(|source| { BabyError::Custom(format!( "rev-parse returned invalid oid `{}`: {}", hex, source )) - }) + })?; + if let (Some(p), Some(key)) = (&pool, &key) { + p.store().insert(key.clone(), hex.as_bytes().to_vec()); + } + Ok(oid) } pub async fn is_ancestor(&self, ancestor: &str, descendant: &str) -> Result { @@ -157,6 +195,28 @@ impl GitBaby { ) -> Result, BabyError> { helper::validate_revision(revision)?; helper::validate_path_local_no_escape(path)?; + let pool = crate::cache::try_global_cache(); + let key = if pool.is_some() { + match self.rev_parse(revision).await.map_err(helper::facade_error) { + Ok(oid) => { + let mut k = + Vec::with_capacity(oid.as_bytes().len() + 1 + path.as_os_str().len()); + k.extend_from_slice(oid.as_bytes()); + k.push(0); + k.extend_from_slice(path.to_string_lossy().as_bytes()); + Some(crate::cache::encode(crate::cache::NS_TREE_ENTRIES, &k)) + } + Err(_) => None, + } + } else { + None + }; + if let (Some(p), Some(key)) = (&pool, &key) + && let Ok(Some(v)) = p.store().get(key).await + && let Ok(entries) = serde_json::from_slice::>(&v) + { + return Ok(entries); + } let mut cmd = self .spawn_commit_cmd(revision, |dir, env| { helper::build_tree_entries_cmd(dir, env, revision, path) @@ -176,7 +236,13 @@ impl GitBaby { &ctx, )); } - helper::parse_tree_entries(&output.stdout) + let entries = helper::parse_tree_entries(&output.stdout)?; + if let (Some(p), Some(key)) = (&pool, &key) + && let Ok(v) = serde_json::to_vec(&entries) + { + p.store().insert(key.clone(), v); + } + Ok(entries) } pub async fn last_commit_for_path( diff --git a/src/refs/usecase.rs b/src/refs/usecase.rs index 4ad9517..52801c0 100644 --- a/src/refs/usecase.rs +++ b/src/refs/usecase.rs @@ -101,6 +101,7 @@ impl GitBaby { Some(&update.name), )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, &update.name).await; Ok(()) } @@ -119,6 +120,7 @@ impl GitBaby { Some(name), )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, name).await; Ok(()) } @@ -166,6 +168,9 @@ impl GitBaby { String::from_utf8_lossy(&output.stderr).into_owned(), )); } + for u in &updates { + let _ = crate::cache::invalidation::hook_ref_updated(self, &u.name).await; + } Ok(()) } @@ -194,6 +199,9 @@ impl GitBaby { String::from_utf8_lossy(&output.stderr).into_owned(), )); } + for n in &names { + let _ = crate::cache::invalidation::hook_ref_updated(self, n).await; + } Ok(()) } diff --git a/src/tags/usecase.rs b/src/tags/usecase.rs index ed6b5eb..1046f15 100644 --- a/src/tags/usecase.rs +++ b/src/tags/usecase.rs @@ -122,6 +122,7 @@ impl GitBaby { &opts.name, )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, &opts.name).await; Ok(()) } @@ -145,6 +146,7 @@ impl GitBaby { &opts.name, )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, &opts.name).await; Ok(()) } @@ -170,6 +172,7 @@ impl GitBaby { &opts.name, )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, &opts.name).await; Ok(()) } @@ -203,6 +206,7 @@ impl GitBaby { name, )); } + let _ = crate::cache::invalidation::hook_ref_updated(self, name).await; Ok(()) }