7631e57f69
- Add test_from_header_valid to verify valid repository header parsing - Add test_from_header_empty_path to handle empty path scenarios - Add test_from_header_relative_storage_path to validate absolute paths - Add test_from_header_relative_path_without_storage for missing storage - Add test_from_header_nonexistent_repo to check repo existence - Add test_from_header_path_traversal to prevent directory traversal - Add test_from_header_not_a_directory for file instead of directory - Add test_from_header_dir_without_head to verify bare repository format - Add test_object_format to validate object format detection - Add test_oid_to_pb to verify OID conversion functionality - Add test_oid_to_pb_invalid_hex to handle invalid hex input gracefully test(error): add comprehensive error handling tests - Add test_error_display_variants to verify error message formatting - Add test_error_is_debug to
156 lines
3.9 KiB
Rust
156 lines
3.9 KiB
Rust
use std::num::NonZeroUsize;
|
|
use std::sync::{Mutex, OnceLock};
|
|
|
|
use clru::CLruCache;
|
|
use prost::Message;
|
|
|
|
use crate::pb::{ObjectSelector, object_selector};
|
|
|
|
const GLOBAL_CACHE_MAX: usize = 65_545;
|
|
|
|
type Cache = CLruCache<Vec<u8>, Vec<u8>>;
|
|
|
|
static GLOBAL_CACHE: OnceLock<Mutex<Cache>> = OnceLock::new();
|
|
|
|
fn cache() -> &'static Mutex<Cache> {
|
|
GLOBAL_CACHE.get_or_init(|| {
|
|
let capacity =
|
|
NonZeroUsize::new(GLOBAL_CACHE_MAX).expect("cache capacity must be non-zero");
|
|
Mutex::new(CLruCache::new(capacity))
|
|
})
|
|
}
|
|
|
|
fn cache_key<Req>(namespace: &str, request: &Req) -> Vec<u8>
|
|
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<Req, Res, E, F>(
|
|
namespace: &'static str,
|
|
request: &Req,
|
|
build: F,
|
|
) -> Result<Res, E>
|
|
where
|
|
Req: Message,
|
|
Res: Message + Default,
|
|
F: FnOnce() -> Result<Res, E>,
|
|
{
|
|
let key = cache_key(namespace, request);
|
|
|
|
if let Some(bytes) = cache()
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.get(&key)
|
|
.cloned()
|
|
&& 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()
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.put(key, bytes);
|
|
Ok(response)
|
|
}
|
|
|
|
pub(crate) fn cached_vec_response<Req, Item, E, F>(
|
|
namespace: &'static str,
|
|
request: &Req,
|
|
build: F,
|
|
) -> Result<Vec<Item>, E>
|
|
where
|
|
Req: Message,
|
|
Item: Message + Default,
|
|
F: FnOnce() -> Result<Vec<Item>, E>,
|
|
{
|
|
let key = cache_key(namespace, request);
|
|
|
|
if let Some(bytes) = cache()
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.get(&key)
|
|
.cloned()
|
|
{
|
|
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()
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.put(key, bytes);
|
|
Ok(response)
|
|
}
|
|
|
|
pub(crate) fn selector_is_oid(selector: &Option<ObjectSelector>) -> 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<ObjectSelector>,
|
|
right: &Option<ObjectSelector>,
|
|
) -> bool {
|
|
selector_is_oid(left) && selector_is_oid(right)
|
|
}
|