use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use crate::wasm::{ExecutorState, SandboxPre};
#[derive(Debug, thiserror::Error)]
pub enum CacheError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Cache entry corrupted: {0}")]
Corrupted(String),
}
pub trait ComponentCache: Send + Sync {
fn get(&self, key: &CacheKey) -> Option<Vec<u8>>;
fn put(&self, key: &CacheKey, precompiled: Vec<u8>) -> Result<(), CacheError>;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
pub extensions_hash: [u8; 32],
pub eryx_version: &'static str,
pub wasmtime_version: &'static str,
}
impl CacheKey {
#[must_use]
pub fn embedded_runtime() -> Self {
Self {
extensions_hash: [0u8; 32], eryx_version: env!("CARGO_PKG_VERSION"),
wasmtime_version: wasmtime_version(),
}
}
#[cfg(feature = "native-extensions")]
pub fn from_extensions(extensions: &[eryx_runtime::linker::NativeExtension]) -> Self {
let mut hasher = Sha256::new();
let mut sorted: Vec<_> = extensions.iter().collect();
sorted.sort_by(|a, b| a.name.cmp(&b.name));
for ext in sorted {
hasher.update(ext.name.as_bytes());
hasher.update((ext.bytes.len() as u64).to_le_bytes());
hasher.update(&ext.bytes);
}
let extensions_hash: [u8; 32] = hasher.finalize().into();
Self {
extensions_hash,
eryx_version: env!("CARGO_PKG_VERSION"),
wasmtime_version: wasmtime_version(),
}
}
#[must_use]
pub fn to_hex(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.extensions_hash);
hasher.update(self.eryx_version.as_bytes());
hasher.update(self.wasmtime_version.as_bytes());
let full_hash: [u8; 32] = hasher.finalize().into();
hex_encode(&full_hash)
}
}
fn wasmtime_version() -> &'static str {
"39"
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[derive(Debug, Clone)]
pub struct FilesystemCache {
cache_dir: PathBuf,
}
impl FilesystemCache {
pub fn new(cache_dir: impl AsRef<Path>) -> Result<Self, CacheError> {
let cache_dir = cache_dir.as_ref().to_path_buf();
fs::create_dir_all(&cache_dir)?;
Ok(Self { cache_dir })
}
#[must_use]
pub fn get_path(&self, key: &CacheKey) -> Option<PathBuf> {
let path = self.cache_path(key);
if path.exists() { Some(path) } else { None }
}
fn cache_path(&self, key: &CacheKey) -> PathBuf {
self.cache_dir.join(format!("{}.cwasm", key.to_hex()))
}
}
impl ComponentCache for FilesystemCache {
fn get(&self, key: &CacheKey) -> Option<Vec<u8>> {
let path = self.cache_path(key);
fs::read(&path).ok()
}
fn put(&self, key: &CacheKey, precompiled: Vec<u8>) -> Result<(), CacheError> {
let path = self.cache_path(key);
let temp_path = path.with_extension("cwasm.tmp");
fs::write(&temp_path, &precompiled)?;
fs::rename(&temp_path, &path)?;
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct InMemoryCache {
cache: Arc<Mutex<HashMap<[u8; 32], Vec<u8>>>>,
}
impl InMemoryCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl ComponentCache for InMemoryCache {
fn get(&self, key: &CacheKey) -> Option<Vec<u8>> {
let cache = self.cache.lock().ok()?;
let mut hasher = Sha256::new();
hasher.update(key.extensions_hash);
hasher.update(key.eryx_version.as_bytes());
hasher.update(key.wasmtime_version.as_bytes());
let full_hash: [u8; 32] = hasher.finalize().into();
cache.get(&full_hash).cloned()
}
fn put(&self, key: &CacheKey, precompiled: Vec<u8>) -> Result<(), CacheError> {
let mut cache = self
.cache
.lock()
.map_err(|e| CacheError::Corrupted(format!("Cache lock poisoned: {e}")))?;
let mut hasher = Sha256::new();
hasher.update(key.extensions_hash);
hasher.update(key.eryx_version.as_bytes());
hasher.update(key.wasmtime_version.as_bytes());
let full_hash: [u8; 32] = hasher.finalize().into();
cache.insert(full_hash, precompiled);
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoCache;
impl ComponentCache for NoCache {
fn get(&self, _key: &CacheKey) -> Option<Vec<u8>> {
None
}
fn put(&self, _key: &CacheKey, _precompiled: Vec<u8>) -> Result<(), CacheError> {
Ok(())
}
}
pub struct InstancePreCache {
cache: RwLock<HashMap<CacheKey, SandboxPre<ExecutorState>>>,
}
impl InstancePreCache {
pub fn global() -> &'static Self {
static CACHE: OnceLock<InstancePreCache> = OnceLock::new();
CACHE.get_or_init(|| {
tracing::debug!("initializing global InstancePreCache");
InstancePreCache {
cache: RwLock::new(HashMap::new()),
}
})
}
#[must_use]
pub fn get(&self, key: &CacheKey) -> Option<SandboxPre<ExecutorState>> {
let cache = self.cache.read().ok()?;
let result = cache.get(key).cloned();
if result.is_some() {
tracing::trace!(key = %key.to_hex(), "instance_pre cache hit");
}
result
}
pub fn put(&self, key: CacheKey, pre: SandboxPre<ExecutorState>) {
if let Ok(mut cache) = self.cache.write() {
use std::collections::hash_map::Entry;
if let Entry::Vacant(e) = cache.entry(key.clone()) {
tracing::debug!(key = %key.to_hex(), "instance_pre cache store");
e.insert(pre);
}
}
}
pub fn clear(&self) {
if let Ok(mut cache) = self.cache.write() {
let count = cache.len();
cache.clear();
tracing::debug!(entries = count, "instance_pre cache cleared");
}
}
#[must_use]
pub fn len(&self) -> usize {
self.cache.read().map(|c| c.len()).unwrap_or(0)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl std::fmt::Debug for InstancePreCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InstancePreCache")
.field("entries", &self.len())
.finish()
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn cache_key_to_hex_is_deterministic() {
let key = CacheKey {
extensions_hash: [0u8; 32],
eryx_version: "0.1.0",
wasmtime_version: "39.0.0",
};
let hex1 = key.to_hex();
let hex2 = key.to_hex();
assert_eq!(hex1, hex2);
assert_eq!(hex1.len(), 64); }
#[test]
fn cache_key_different_versions_produce_different_hex() {
let key1 = CacheKey {
extensions_hash: [0u8; 32],
eryx_version: "0.1.0",
wasmtime_version: "39.0.0",
};
let key2 = CacheKey {
extensions_hash: [0u8; 32],
eryx_version: "0.2.0",
wasmtime_version: "39.0.0",
};
assert_ne!(key1.to_hex(), key2.to_hex());
}
#[test]
fn in_memory_cache_stores_and_retrieves() {
let cache = InMemoryCache::new();
let key = CacheKey {
extensions_hash: [1u8; 32],
eryx_version: "0.1.0",
wasmtime_version: "39.0.0",
};
assert!(cache.get(&key).is_none());
let data = vec![1, 2, 3, 4];
cache.put(&key, data.clone()).unwrap();
let retrieved = cache.get(&key);
assert_eq!(retrieved, Some(data));
}
#[test]
fn no_cache_never_stores() {
let cache = NoCache;
let key = CacheKey {
extensions_hash: [2u8; 32],
eryx_version: "0.1.0",
wasmtime_version: "39.0.0",
};
let data = vec![5, 6, 7, 8];
cache.put(&key, data).unwrap();
assert!(cache.get(&key).is_none());
}
#[test]
fn filesystem_cache_creates_directory() {
let temp_dir = std::env::temp_dir().join("eryx-cache-test");
let _ = std::fs::remove_dir_all(&temp_dir);
let cache = FilesystemCache::new(&temp_dir).unwrap();
assert!(temp_dir.exists());
drop(cache);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn filesystem_cache_stores_and_retrieves() {
let temp_dir = std::env::temp_dir().join("eryx-cache-test-2");
let _ = std::fs::remove_dir_all(&temp_dir);
let cache = FilesystemCache::new(&temp_dir).unwrap();
let key = CacheKey {
extensions_hash: [3u8; 32],
eryx_version: "0.1.0",
wasmtime_version: "39.0.0",
};
assert!(cache.get(&key).is_none());
let data = vec![10, 20, 30, 40];
cache.put(&key, data.clone()).unwrap();
let retrieved = cache.get(&key);
assert_eq!(retrieved, Some(data));
let expected_path = cache.cache_path(&key);
assert!(expected_path.exists());
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn cache_key_embedded_runtime_is_deterministic() {
let key1 = CacheKey::embedded_runtime();
let key2 = CacheKey::embedded_runtime();
assert_eq!(key1, key2);
assert_eq!(key1.extensions_hash, [0u8; 32]); assert_eq!(key1.eryx_version, env!("CARGO_PKG_VERSION"));
}
#[test]
fn cache_key_embedded_runtime_differs_from_extensions() {
let embedded_key = CacheKey::embedded_runtime();
let other_key = CacheKey {
extensions_hash: [1u8; 32], eryx_version: env!("CARGO_PKG_VERSION"),
wasmtime_version: "39",
};
assert_ne!(embedded_key, other_key);
assert_ne!(embedded_key.to_hex(), other_key.to_hex());
}
#[test]
fn instance_pre_cache_global_returns_same_instance() {
let cache1 = InstancePreCache::global();
let cache2 = InstancePreCache::global();
assert!(std::ptr::eq(cache1, cache2));
}
#[test]
fn instance_pre_cache_is_initially_empty() {
let cache = InstancePreCache::global();
let initial_len = cache.len();
let _ = cache.is_empty();
assert_eq!(cache.len(), initial_len);
}
#[test]
fn instance_pre_cache_debug_format() {
let cache = InstancePreCache::global();
let debug_str = format!("{:?}", cache);
assert!(debug_str.contains("InstancePreCache"));
assert!(debug_str.contains("entries"));
}
}