use std::sync::OnceLock; use std::time::Duration; use moka::sync::Cache; use prost::Message; use crate::pb::{ObjectSelector, object_selector}; const GLOBAL_CACHE_MAX: u64 = 65_536; const CACHE_TTL: Duration = Duration::from_secs(300); static GLOBAL_CACHE: OnceLock, Vec>> = OnceLock::new(); fn cache() -> &'static Cache, Vec> { GLOBAL_CACHE.get_or_init(|| { Cache::builder() .max_capacity(GLOBAL_CACHE_MAX) .time_to_live(CACHE_TTL) .build() }) } fn cache_key(namespace: &str, request: &Req) -> Vec where Req: Message, { let mut key = Vec::with_capacity(namespace.len() + 1 + request.encoded_len()); key.extend_from_slice(namespace.as_bytes()); key.push(0); request .encode(&mut key) .expect("encoding a prost message into Vec cannot fail"); key } pub(crate) fn cached_response( namespace: &'static str, request: &Req, build: F, ) -> Result where Req: Message, Res: Message + Default, F: FnOnce() -> Result, { let key = cache_key(namespace, request); if let Some(bytes) = cache().get(&key) && let Ok(response) = Res::decode(bytes.as_slice()) { tracing::debug!( namespace = %namespace, key_len = key.len(), "cache hit" ); return Ok(response); } tracing::debug!( namespace = %namespace, key_len = key.len(), "cache miss, building response" ); let response = build()?; let mut bytes = Vec::with_capacity(response.encoded_len()); response .encode(&mut bytes) .expect("encoding a prost message into Vec cannot fail"); cache().insert(key, bytes); Ok(response) } pub(crate) fn cached_vec_response( namespace: &'static str, request: &Req, build: F, ) -> Result, E> where Req: Message, Item: Message + Default, F: FnOnce() -> Result, E>, { let key = cache_key(namespace, request); if let Some(bytes) = cache().get(&key) { let mut remaining = bytes.as_slice(); let mut items = Vec::new(); let mut valid = true; while !remaining.is_empty() { match Item::decode_length_delimited(&mut remaining) { Ok(item) => items.push(item), Err(_) => { valid = false; break; } } } if valid { tracing::debug!( namespace = %namespace, key_len = key.len(), item_count = items.len(), "vec cache hit" ); return Ok(items); } tracing::warn!( namespace = %namespace, "vec cache decode failed, rebuilding" ); } tracing::debug!( namespace = %namespace, key_len = key.len(), "vec cache miss, building response" ); let response = build()?; let mut bytes = Vec::new(); for item in &response { item.encode_length_delimited(&mut bytes) .expect("encoding a prost message into Vec cannot fail"); } cache().insert(key, bytes); Ok(response) } /// Invalidate all cache entries related to a specific repository. /// Called when refs are updated (create branch, create commit, etc.) /// so that stale data is not served. pub(crate) fn invalidate_repo(relative_path: &str) { let c = cache(); // Encode the relative_path to match how it appears in cache keys let target_path_bytes = relative_path.as_bytes(); // Remove all keys that reference this repository // Cache keys are: namespace\0 + prost-encoded request let keys_to_remove: Vec>> = c .iter() .filter_map(|(key, _)| { // Find the null byte separator if let Some(null_pos) = key.iter().position(|&b| b == 0) { let encoded_request = &key[null_pos + 1..]; // Check if this encoded request contains the repository path // We use a sliding window to find the path bytes in the encoded protobuf // This is conservative but correct: we may invalidate slightly more than // necessary, but we won't miss any entries for this repository. // // The encoded protobuf format embeds string fields as length-prefixed data, // so the relative_path bytes should appear verbatim somewhere in the message. if contains_subslice(encoded_request, target_path_bytes) { return Some(key); } } else { // Malformed key without separator, remove it to be safe tracing::warn!("found cache key without null separator, removing"); return Some(key); } None }) .collect(); let removed = keys_to_remove.len(); for key in keys_to_remove { c.invalidate(key.as_ref()); } if removed > 0 { tracing::debug!( relative_path = %relative_path, entries_removed = removed, "cache invalidated for repository" ); } } /// Check if a byte slice contains a subslice fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool { if needle.is_empty() { return true; } if needle.len() > haystack.len() { return false; } haystack .windows(needle.len()) .any(|window| window == needle) } pub(crate) fn selector_is_oid(selector: &Option) -> bool { matches!( selector.as_ref().and_then(|s| s.selector.as_ref()), Some(object_selector::Selector::Oid(_)) ) } pub(crate) fn selectors_are_oid( left: &Option, right: &Option, ) -> bool { selector_is_oid(left) && selector_is_oid(right) }