sui_dockerfile_wrapper/cache.rs
1//! The cache seam this crate consumes.
2//!
3//! This crate does **not** invent a new cache abstraction — it consumes
4//! [`sui_castore::StorageBackend`] exactly as `sui cache serve`
5//! does. A node is "cached" when its `content_hash` has a narinfo entry;
6//! the narinfo *content* is the image reference the node's cache write
7//! produced, so a cache hit can pull the already-built image instead of
8//! rebuilding it. Real callers construct the backend via
9//! [`sui_cache::build_backend`] against a
10//! [`sui_cache::BackendConfig`] (Postgres L2 + Redis L1, per
11//! `sui-supercacheci`); tests use [`MockCacheBackend`], an in-memory
12//! implementation of the same trait — the identical shape the
13//! `sui-store` crate's `TestStore` uses to prove `Store`'s default
14//! methods without a real database.
15
16use std::collections::BTreeMap;
17use std::sync::Mutex;
18
19use async_trait::async_trait;
20use sui_cache::CacheError;
21use sui_cache::{MemNarRefIndex, NarRefIndex, StorageBackend};
22
23/// An in-memory [`StorageBackend`] for unit tests. Never touches a
24/// filesystem, Redis, or Postgres — narinfo entries live in a
25/// `Mutex<BTreeMap>` for the duration of the test.
26#[derive(Default)]
27pub struct MockCacheBackend {
28 narinfos: Mutex<BTreeMap<String, String>>,
29 /// The reverse index. Shared semantics with production via
30 /// [`MemNarRefIndex`] rather than a hand-rolled map — a double whose index
31 /// disagreed with a real backend's would prove nothing.
32 nar_refs: MemNarRefIndex,
33}
34
35impl MockCacheBackend {
36 #[must_use]
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 /// Pre-populate a cache entry — the constructor tests use to set up a
42 /// full or partial cache-hit fixture.
43 ///
44 /// # Panics
45 ///
46 /// Panics only if the internal mutex is poisoned (a prior panic
47 /// while holding the lock) — never in normal test use.
48 #[must_use]
49 pub fn with_entry(self, hash: &str, image_ref: &str) -> Self {
50 self.narinfos
51 .lock()
52 .expect("mock mutex poisoned")
53 .insert(hash.to_string(), image_ref.to_string());
54 self
55 }
56}
57
58#[async_trait]
59impl StorageBackend for MockCacheBackend {
60 async fn get_narinfo(&self, hash: &str) -> Result<Option<String>, CacheError> {
61 Ok(self.narinfos.lock().expect("mock mutex poisoned").get(hash).cloned())
62 }
63
64 async fn put_narinfo_record(&self, hash: &str, content: &str) -> Result<(), CacheError> {
65 self.narinfos
66 .lock()
67 .expect("mock mutex poisoned")
68 .insert(hash.to_string(), content.to_string());
69 Ok(())
70 }
71
72 async fn delete_narinfo_record(&self, hash: &str) -> Result<(), CacheError> {
73 self.narinfos.lock().expect("mock mutex poisoned").remove(hash);
74 Ok(())
75 }
76
77 /// This double stores no NAR bytes, so there is nothing to remove.
78 async fn delete_nar_record(&self, _nar_path: &str) -> Result<(), CacheError> {
79 Ok(())
80 }
81
82 fn nar_ref_index(&self) -> &dyn NarRefIndex {
83 &self.nar_refs
84 }
85
86 async fn get_nar(&self, _path: &str) -> Result<Option<Vec<u8>>, CacheError> {
87 Ok(None)
88 }
89
90 async fn put_nar(&self, _path: &str, _data: &[u8]) -> Result<(), CacheError> {
91 Ok(())
92 }
93
94 /// This double stores no NAR bytes at all (`get_nar` is always a miss,
95 /// `put_nar` a no-op), so it cannot stream — declaring `WholeValue` keeps
96 /// the claim honest rather than borrowing a bound it never exercises.
97 fn nar_residency(&self) -> sui_cache::NarResidency {
98 sui_cache::NarResidency::WholeValue
99 }
100
101 async fn list_narinfos(&self) -> Result<Vec<String>, CacheError> {
102 Ok(self.narinfos.lock().expect("mock mutex poisoned").keys().cloned().collect())
103 }
104}