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:
+59
-29
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user