a3s_box_core/traits/cache.rs
1//! Cache backend abstraction.
2//!
3//! Decouples rootfs and layer caching from the filesystem-based LRU
4//! implementation in `a3s-box-runtime`. Implementations can use any
5//! storage backend: local filesystem, shared NFS, Redis, content-addressable
6//! store, etc.
7//!
8//! The cache operates on opaque string keys and directory-shaped values
9//! (a `PathBuf` pointing to a directory tree). The backend is responsible
10//! for storage, retrieval, and eviction.
11
12use std::path::{Path, PathBuf};
13
14use serde::{Deserialize, Serialize};
15
16use crate::error::Result;
17
18/// Metadata about a single cache entry.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct CacheEntry {
21 /// Cache key.
22 pub key: String,
23
24 /// Human-readable description of the cached content.
25 pub description: String,
26
27 /// Size of the cached content in bytes.
28 pub size_bytes: u64,
29
30 /// When this entry was first cached (Unix timestamp).
31 pub cached_at: i64,
32
33 /// When this entry was last accessed (Unix timestamp).
34 pub last_accessed: i64,
35}
36
37/// Statistics about the cache as a whole.
38#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39pub struct CacheStats {
40 /// Number of entries in the cache.
41 pub entry_count: usize,
42
43 /// Total size of all cached content in bytes.
44 pub total_bytes: u64,
45}
46
47/// Abstraction over directory-based caching.
48///
49/// Used for both OCI layer caching and fully-built rootfs caching.
50/// The key is an opaque string (typically a content hash), and the
51/// value is a directory tree on disk.
52///
53/// # Lifecycle
54///
55/// 1. `get(key)` — check if a cached directory exists for this key
56/// 2. On miss: build the content, then `put(key, source_dir, desc)`
57/// 3. `prune(max_entries, max_bytes)` — evict old entries to stay within limits
58///
59/// # Thread Safety
60///
61/// Implementations must be `Send + Sync`. Concurrent `get`/`put` calls
62/// with different keys must be safe. Concurrent calls with the same key
63/// have implementation-defined behavior (last writer wins is acceptable).
64pub trait CacheBackend: Send + Sync {
65 /// Retrieve a cached directory by key.
66 ///
67 /// Returns `Some(path)` if the key exists and the cached content is valid.
68 /// The returned path points to a directory that the caller can read from.
69 /// Returns `None` on cache miss.
70 ///
71 /// Implementations should update the last-accessed timestamp on hit.
72 fn get(&self, key: &str) -> Result<Option<PathBuf>>;
73
74 /// Store a directory tree in the cache under the given key.
75 ///
76 /// Copies (or moves) the contents of `source_dir` into the cache.
77 /// If an entry with this key already exists, it is replaced.
78 ///
79 /// Returns the path to the cached directory (which may differ from `source_dir`).
80 fn put(&self, key: &str, source_dir: &Path, description: &str) -> Result<PathBuf>;
81
82 /// Remove a cached entry by key.
83 ///
84 /// No-op if the key does not exist.
85 fn invalidate(&self, key: &str) -> Result<()>;
86
87 /// Evict entries to satisfy the given constraints.
88 ///
89 /// Implementations should evict least-recently-accessed entries first.
90 /// Returns the number of entries evicted.
91 fn prune(&self, max_entries: usize, max_bytes: u64) -> Result<usize>;
92
93 /// List all cache entries with their metadata.
94 fn list(&self) -> Result<Vec<CacheEntry>>;
95
96 /// Get aggregate cache statistics.
97 fn stats(&self) -> Result<CacheStats> {
98 let entries = self.list()?;
99 Ok(CacheStats {
100 entry_count: entries.len(),
101 total_bytes: entries.iter().map(|e| e.size_bytes).sum(),
102 })
103 }
104}