feat(cache): split cache module into submodules and wire invalidation hooks

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<CachePools> instead of Arc<CacheStore>

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<TreeEntry>,
     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.
This commit is contained in:
zhenyi 2026-08-14 18:37:51 +08:00
parent 4d5e5ba5f9
commit 5d32290cf1
14 changed files with 511 additions and 166 deletions

View File

@ -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<TreeEntry>`) 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)

View File

@ -13,6 +13,8 @@ use crate::blob::types::{
};
use crate::error::BabyError;
const NEGATIVE_TTL_SECS: u64 = 60;
pub struct ChildBlobReader<R: AsyncBufRead + Unpin + Send> {
reader: R,
child: Option<Child>,
@ -87,6 +89,19 @@ impl GitBaby {
oid: ObjectId,
opts: GetBlobOptions,
) -> Result<BlobContent, 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(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<BufReader<Box<dyn tokio::io::AsyncRead + Unpin + Send + Sync>>>,
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<dyn tokio::io::AsyncRead + Unpin + Send + Sync>;
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<F>(&self, builder: F) -> Result<crate::command::cmd::Cmd, BabyError>
@ -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)
}

View File

@ -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(())
}

91
src/cache/config.rs vendored Normal file
View File

@ -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<LfsCacheConfig>,
}
#[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::<usize>().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")
}

32
src/cache/global.rs vendored
View File

@ -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<Mutex<Option<Arc<CacheStore>>>> =
pub static GITBABY_CACHE: LazyLock<Mutex<Option<Arc<CachePools>>>> =
LazyLock::new(|| Mutex::new(None));
pub fn try_global_cache() -> Option<Arc<CacheStore>> {
pub fn try_global_cache() -> Option<Arc<CachePools>> {
let guard = GITBABY_CACHE.lock().ok()?;
guard.clone()
}
pub fn init_global_cache(cfg: CacheConfig) -> Result<Arc<CacheStore>, BabyError> {
pub fn init_global_cache(cfg: CacheConfig) -> Result<Arc<CachePools>, BabyError> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
@ -21,34 +21,34 @@ pub fn init_global_cache(cfg: CacheConfig) -> Result<Arc<CacheStore>, 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<Arc<CacheStore>, BabyError> {
let store = CacheStore::open(cfg).await?;
install(store)
pub async fn init_global_cache_async(cfg: CacheConfig) -> Result<Arc<CachePools>, BabyError> {
let pools = CachePools::open(cfg).await?;
install(pools)
}
fn install(store: Arc<CacheStore>) -> Result<Arc<CacheStore>, BabyError> {
fn install(pools: Arc<CachePools>) -> Result<Arc<CachePools>, 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(())
}

25
src/cache/invalidation.rs vendored Normal file
View File

@ -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(())
}

33
src/cache/key.rs vendored Normal file
View File

@ -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
}

156
src/cache/mod.rs vendored
View File

@ -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<u8>;
pub type CacheValue = Vec<u8>;
#[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<CacheKey, CacheValue>,
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<Arc<Self>, 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<CacheKey, CacheValue> = 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<Option<Vec<u8>>, 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<u8>, value: Vec<u8>) {
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<F, Fut>(&self, key: Vec<u8>, fetch: F) -> Result<Vec<u8>, BabyError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<Vec<u8>, 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};

59
src/cache/pools.rs vendored Normal file
View File

@ -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<CacheStore>,
lfs: Option<Arc<CacheStore>>,
}
impl CachePools {
pub async fn open(cfg: CacheConfig) -> Result<Arc<Self>, 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<CacheStore> {
&self.store
}
pub fn lfs(&self) -> Option<&Arc<CacheStore>> {
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,
}
}
}

8
src/cache/stats.rs vendored Normal file
View File

@ -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,
}

106
src/cache/store.rs vendored Normal file
View File

@ -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<u8>;
pub type CacheValue = Vec<u8>;
pub struct CacheStore {
inner: HybridCache<CacheKey, CacheValue>,
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<Arc<Self>, 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<CacheKey, CacheValue> = 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<foyer::Statistics> {
Arc::clone(self.inner.statistics())
}
pub async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, 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<u8>, value: Vec<u8>) {
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<F, Fut>(&self, key: Vec<u8>, fetch: F) -> Result<Vec<u8>, BabyError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<Vec<u8>, 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())
}
}

View File

@ -8,6 +8,18 @@ use crate::error::BabyError;
impl GitBaby {
pub async fn get_commit(&self, revision: &str) -> Result<CommitInfo, BabyError> {
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<gix::ObjectId, BabyError> {
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<bool, BabyError> {
@ -157,6 +195,28 @@ impl GitBaby {
) -> Result<Vec<TreeEntry>, 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::<Vec<TreeEntry>>(&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(

View File

@ -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(())
}

View File

@ -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(())
}