Skip to main content

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}