refactor(cache): redesign cache system with structured keys and improved performance

- Add repo_path parameter to cached_response and cached_vec_response functions
- Implement structured cache key format with namespace, repo_path, and request proto
- Replace global cache with Moka in-memory cache using weight-based eviction
- Set 256MB memory cap with 10-minute TTL and 2-minute TTI policy
- Add metrics collection for cache operations and evictions
- Implement efficient repo-scoped invalidation using key structure
- Add detailed documentation comments explaining cache architecture
- Remove outdated dependencies and update dependency versions
- Add error handling for encoding failures in cache operations
- Optimize Vec responses with length-delimited encoding and pre-allocation
This commit is contained in:
zhenyi
2026-06-12 12:53:23 +08:00
parent a40da90ef9
commit 934858bebf
82 changed files with 1273 additions and 4969 deletions
+59 -29
View File
@@ -29,18 +29,22 @@ const INFO_REFS_DIR_RELATIVE: &str = "+gitks-cache/info_refs";
fn random_value() -> String {
use std::fmt::Write;
use std::sync::atomic::{AtomicU64, Ordering};
let mut buf = [0u8; 16];
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
buf[..8].copy_from_slice(&nanos.to_le_bytes());
static COUNTER: AtomicU64 = AtomicU64::new(0);
let c = COUNTER.fetch_add(1, Ordering::Relaxed);
buf[8..].copy_from_slice(&c.to_le_bytes());
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut buf = [0u8; 16];
buf[..8].copy_from_slice(&nanos.to_le_bytes());
buf[8..].copy_from_slice(&counter.to_le_bytes());
let mut s = String::with_capacity(32);
for byte in &buf {
write!(s, "{byte:02x}").unwrap();
let _ = write!(s, "{byte:02x}");
}
s
}
@@ -56,16 +60,17 @@ fn sha256_digest(parts: &[&str]) -> String {
let mut s = String::with_capacity(64);
for byte in result {
use std::fmt::Write;
write!(s, "{byte:02x}").unwrap();
let _ = write!(s, "{byte:02x}");
}
s
}
/// Convert a digest into a two-level file path: `${digest:0:2}/${digest:2}`.
pub fn digest_to_path(digest: &str) -> PathBuf {
let prefix = &digest[..2];
let rest = &digest[2..];
PathBuf::from(prefix).join(rest)
match (digest.get(..2), digest.get(2..)) {
(Some(prefix), Some(rest)) => PathBuf::from(prefix).join(rest),
_ => PathBuf::from(digest),
}
}
/// DiskCache manages per-repository state and cached response files on local disk.
@@ -116,7 +121,7 @@ impl DiskCache {
}
/// Ensure the state directory for a repository exists and has a `latest` file.
/// If `latest` does not exist, create it with a random value.
/// If `latest` does not exist, create it atomically with a random value.
pub fn ensure_state(&self, relative_path: &str) -> GitResult<String> {
if !self.enabled {
return Ok(random_value());
@@ -129,12 +134,15 @@ impl DiskCache {
let latest_path = self.latest_path_for(relative_path);
if latest_path.exists() {
let val = std::fs::read_to_string(&latest_path).map_err(GitError::Io)?;
Ok(val.trim().to_string())
} else {
let val = random_value();
std::fs::write(&latest_path, &val).map_err(GitError::Io)?;
Ok(val)
return Ok(val.trim().to_string());
}
// Atomic write: create temp file, then rename into place
let val = random_value();
let tmp_path = latest_path.with_extension("tmp");
std::fs::write(&tmp_path, &val).map_err(GitError::Io)?;
std::fs::rename(&tmp_path, &latest_path).map_err(GitError::Io)?;
Ok(val)
}
/// Create a lease file for a mutating RPC.
@@ -448,30 +456,52 @@ impl DiskCache {
if !dir.exists() {
continue;
}
for prefix_entry in std::fs::read_dir(&dir).map_err(GitError::Io)? {
let prefix_entry = prefix_entry.map_err(GitError::Io)?;
let prefix_dir = prefix_entry.path();
let prefix_iter = match std::fs::read_dir(&dir) {
Ok(iter) => iter,
Err(_) => continue,
};
for prefix_entry in prefix_iter {
let prefix_dir = match prefix_entry {
Ok(e) => e.path(),
Err(_) => continue,
};
if !prefix_dir.is_dir() {
continue;
}
for entry in std::fs::read_dir(&prefix_dir).map_err(GitError::Io)? {
let entry = entry.map_err(GitError::Io)?;
let path = entry.path();
if let Ok(metadata) = entry.metadata()
&& let Ok(modified) = metadata.modified()
&& let Ok(age) = now.duration_since(modified)
&& age > self.max_age
{
// Process all entries in this prefix directory
let entries = match std::fs::read_dir(&prefix_dir) {
Ok(iter) => iter,
Err(_) => continue,
};
let mut prefix_empty = true;
for entry in entries {
let path = match entry {
Ok(e) => e.path(),
Err(_) => continue,
};
let expired = match std::fs::metadata(&path) {
Ok(meta) => meta
.modified()
.ok()
.and_then(|mtime| now.duration_since(mtime).ok())
.is_some_and(|age| age > self.max_age),
Err(_) => false,
};
if expired {
tracing::debug!(
path = %path.display(),
age_secs = age.as_secs(),
"removing expired cache entry"
);
std::fs::remove_file(&path).ok();
removed += 1;
} else {
prefix_empty = false;
}
}
std::fs::remove_dir(&prefix_dir).ok();
// Remove empty prefix directory
if prefix_empty {
std::fs::remove_dir(&prefix_dir).ok();
}
}
}
if removed > 0 {