Skip to main content

greentic_runner_host/runner/
contract_cache.rs

1use std::collections::{HashMap, VecDeque};
2use std::sync::Arc;
3
4use parking_lot::Mutex;
5use serde::{Deserialize, Serialize};
6
7const DEFAULT_CONTRACT_CACHE_MAX_BYTES: u64 = 256 * 1024 * 1024;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct ContractSnapshot {
11    pub resolved_digest: String,
12    pub component_id: String,
13    pub operation_id: String,
14    pub validate_output: bool,
15    pub strict: bool,
16    pub describe_hash: Option<String>,
17    pub schema_hash: Option<String>,
18}
19
20impl ContractSnapshot {
21    pub fn new(
22        resolved_digest: String,
23        component_id: String,
24        operation_id: String,
25        validate_output: bool,
26        strict: bool,
27    ) -> Self {
28        Self {
29            resolved_digest,
30            component_id,
31            operation_id,
32            validate_output,
33            strict,
34            describe_hash: None,
35            schema_hash: None,
36        }
37    }
38
39    fn estimated_bytes(&self) -> u64 {
40        let mut bytes = 128_u64;
41        bytes = bytes.saturating_add(self.resolved_digest.len() as u64);
42        bytes = bytes.saturating_add(self.component_id.len() as u64);
43        bytes = bytes.saturating_add(self.operation_id.len() as u64);
44        if let Some(value) = &self.describe_hash {
45            bytes = bytes.saturating_add(value.len() as u64);
46        }
47        if let Some(value) = &self.schema_hash {
48            bytes = bytes.saturating_add(value.len() as u64);
49        }
50        bytes
51    }
52}
53
54#[derive(Debug, Clone, Default, PartialEq, Eq)]
55pub struct ContractCacheStats {
56    pub hits: u64,
57    pub misses: u64,
58    pub evictions: u64,
59    pub entries: u64,
60    pub total_bytes: u64,
61}
62
63#[derive(Debug, Clone)]
64pub struct ContractCache {
65    max_bytes: u64,
66    state: Arc<Mutex<ContractCacheState>>,
67}
68
69#[derive(Debug, Default)]
70struct ContractCacheState {
71    entries: HashMap<String, ContractCacheEntry>,
72    lru: VecDeque<String>,
73    total_bytes: u64,
74    hits: u64,
75    misses: u64,
76    evictions: u64,
77}
78
79#[derive(Debug)]
80struct ContractCacheEntry {
81    snapshot: Arc<ContractSnapshot>,
82    bytes_estimate: u64,
83}
84
85impl ContractCache {
86    pub fn new(max_bytes: u64) -> Self {
87        Self {
88            max_bytes,
89            state: Arc::new(Mutex::new(ContractCacheState::default())),
90        }
91    }
92
93    pub fn from_env() -> Self {
94        let max_bytes = std::env::var("GREENTIC_CONTRACT_CACHE_MAX_BYTES")
95            .ok()
96            .and_then(|raw| raw.trim().parse::<u64>().ok())
97            .unwrap_or(DEFAULT_CONTRACT_CACHE_MAX_BYTES);
98        Self::new(max_bytes)
99    }
100
101    pub fn get(&self, key: &str) -> Option<Arc<ContractSnapshot>> {
102        let mut state = self.state.lock();
103        if state.entries.contains_key(key) {
104            state.hits = state.hits.saturating_add(1);
105            let snapshot = state
106                .entries
107                .get(key)
108                .map(|entry| Arc::clone(&entry.snapshot));
109            touch_lru(&mut state.lru, key);
110            return snapshot;
111        }
112        state.misses = state.misses.saturating_add(1);
113        None
114    }
115
116    pub fn insert(&self, key: String, snapshot: Arc<ContractSnapshot>) {
117        let mut state = self.state.lock();
118        if let Some(existing) = state.entries.remove(&key) {
119            state.total_bytes = state.total_bytes.saturating_sub(existing.bytes_estimate);
120            remove_lru(&mut state.lru, &key);
121        }
122        let bytes_estimate = snapshot.estimated_bytes();
123        state.entries.insert(
124            key.clone(),
125            ContractCacheEntry {
126                snapshot,
127                bytes_estimate,
128            },
129        );
130        state.total_bytes = state.total_bytes.saturating_add(bytes_estimate);
131        state.lru.push_front(key);
132        self.evict_if_needed(&mut state);
133    }
134
135    pub fn stats(&self) -> ContractCacheStats {
136        let state = self.state.lock();
137        ContractCacheStats {
138            hits: state.hits,
139            misses: state.misses,
140            evictions: state.evictions,
141            entries: state.entries.len() as u64,
142            total_bytes: state.total_bytes,
143        }
144    }
145
146    fn evict_if_needed(&self, state: &mut ContractCacheState) {
147        if self.max_bytes == 0 {
148            return;
149        }
150        while state.total_bytes > self.max_bytes {
151            let Some(candidate) = state.lru.pop_back() else {
152                break;
153            };
154            if let Some(entry) = state.entries.remove(&candidate) {
155                state.total_bytes = state.total_bytes.saturating_sub(entry.bytes_estimate);
156                state.evictions = state.evictions.saturating_add(1);
157            }
158        }
159    }
160}
161
162fn touch_lru(lru: &mut VecDeque<String>, key: &str) {
163    if let Some(pos) = lru.iter().position(|item| item == key) {
164        lru.remove(pos);
165        lru.push_front(key.to_string());
166    }
167}
168
169fn remove_lru(lru: &mut VecDeque<String>, key: &str) {
170    if let Some(pos) = lru.iter().position(|item| item == key) {
171        lru.remove(pos);
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn cache_tracks_hits_and_lru_eviction() {
181        let cache = ContractCache::new(256);
182        let key_a = "sha256:a::component.alpha::run".to_string();
183        let key_b = "sha256:b::component.beta::run".to_string();
184
185        cache.insert(
186            key_a.clone(),
187            Arc::new(ContractSnapshot::new(
188                "sha256:a".to_string(),
189                "component.alpha".to_string(),
190                "run".to_string(),
191                true,
192                true,
193            )),
194        );
195        assert!(cache.get(&key_a).is_some());
196        cache.insert(
197            key_b.clone(),
198            Arc::new(ContractSnapshot::new(
199                "sha256:b".to_string(),
200                "component.beta".to_string(),
201                "run".to_string(),
202                true,
203                true,
204            )),
205        );
206        let stats = cache.stats();
207        assert!(stats.hits >= 1);
208        assert!(stats.entries >= 1);
209        assert!(cache.get(&key_b).is_some());
210    }
211}