greentic-runner-host 0.4.72

Host runtime shim for Greentic runner: config, pack loading, activity handling
Documentation
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;

use parking_lot::Mutex;
use serde::{Deserialize, Serialize};

const DEFAULT_CONTRACT_CACHE_MAX_BYTES: u64 = 256 * 1024 * 1024;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractSnapshot {
    pub resolved_digest: String,
    pub component_id: String,
    pub operation_id: String,
    pub validate_output: bool,
    pub strict: bool,
    pub describe_hash: Option<String>,
    pub schema_hash: Option<String>,
}

impl ContractSnapshot {
    pub fn new(
        resolved_digest: String,
        component_id: String,
        operation_id: String,
        validate_output: bool,
        strict: bool,
    ) -> Self {
        Self {
            resolved_digest,
            component_id,
            operation_id,
            validate_output,
            strict,
            describe_hash: None,
            schema_hash: None,
        }
    }

    fn estimated_bytes(&self) -> u64 {
        let mut bytes = 128_u64;
        bytes = bytes.saturating_add(self.resolved_digest.len() as u64);
        bytes = bytes.saturating_add(self.component_id.len() as u64);
        bytes = bytes.saturating_add(self.operation_id.len() as u64);
        if let Some(value) = &self.describe_hash {
            bytes = bytes.saturating_add(value.len() as u64);
        }
        if let Some(value) = &self.schema_hash {
            bytes = bytes.saturating_add(value.len() as u64);
        }
        bytes
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ContractCacheStats {
    pub hits: u64,
    pub misses: u64,
    pub evictions: u64,
    pub entries: u64,
    pub total_bytes: u64,
}

#[derive(Debug, Clone)]
pub struct ContractCache {
    max_bytes: u64,
    state: Arc<Mutex<ContractCacheState>>,
}

#[derive(Debug, Default)]
struct ContractCacheState {
    entries: HashMap<String, ContractCacheEntry>,
    lru: VecDeque<String>,
    total_bytes: u64,
    hits: u64,
    misses: u64,
    evictions: u64,
}

#[derive(Debug)]
struct ContractCacheEntry {
    snapshot: Arc<ContractSnapshot>,
    bytes_estimate: u64,
}

impl ContractCache {
    pub fn new(max_bytes: u64) -> Self {
        Self {
            max_bytes,
            state: Arc::new(Mutex::new(ContractCacheState::default())),
        }
    }

    pub fn from_env() -> Self {
        let max_bytes = std::env::var("GREENTIC_CONTRACT_CACHE_MAX_BYTES")
            .ok()
            .and_then(|raw| raw.trim().parse::<u64>().ok())
            .unwrap_or(DEFAULT_CONTRACT_CACHE_MAX_BYTES);
        Self::new(max_bytes)
    }

    pub fn get(&self, key: &str) -> Option<Arc<ContractSnapshot>> {
        let mut state = self.state.lock();
        if state.entries.contains_key(key) {
            state.hits = state.hits.saturating_add(1);
            let snapshot = state
                .entries
                .get(key)
                .map(|entry| Arc::clone(&entry.snapshot));
            touch_lru(&mut state.lru, key);
            return snapshot;
        }
        state.misses = state.misses.saturating_add(1);
        None
    }

    pub fn insert(&self, key: String, snapshot: Arc<ContractSnapshot>) {
        let mut state = self.state.lock();
        if let Some(existing) = state.entries.remove(&key) {
            state.total_bytes = state.total_bytes.saturating_sub(existing.bytes_estimate);
            remove_lru(&mut state.lru, &key);
        }
        let bytes_estimate = snapshot.estimated_bytes();
        state.entries.insert(
            key.clone(),
            ContractCacheEntry {
                snapshot,
                bytes_estimate,
            },
        );
        state.total_bytes = state.total_bytes.saturating_add(bytes_estimate);
        state.lru.push_front(key);
        self.evict_if_needed(&mut state);
    }

    pub fn stats(&self) -> ContractCacheStats {
        let state = self.state.lock();
        ContractCacheStats {
            hits: state.hits,
            misses: state.misses,
            evictions: state.evictions,
            entries: state.entries.len() as u64,
            total_bytes: state.total_bytes,
        }
    }

    fn evict_if_needed(&self, state: &mut ContractCacheState) {
        if self.max_bytes == 0 {
            return;
        }
        while state.total_bytes > self.max_bytes {
            let Some(candidate) = state.lru.pop_back() else {
                break;
            };
            if let Some(entry) = state.entries.remove(&candidate) {
                state.total_bytes = state.total_bytes.saturating_sub(entry.bytes_estimate);
                state.evictions = state.evictions.saturating_add(1);
            }
        }
    }
}

fn touch_lru(lru: &mut VecDeque<String>, key: &str) {
    if let Some(pos) = lru.iter().position(|item| item == key) {
        lru.remove(pos);
        lru.push_front(key.to_string());
    }
}

fn remove_lru(lru: &mut VecDeque<String>, key: &str) {
    if let Some(pos) = lru.iter().position(|item| item == key) {
        lru.remove(pos);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cache_tracks_hits_and_lru_eviction() {
        let cache = ContractCache::new(256);
        let key_a = "sha256:a::component.alpha::run".to_string();
        let key_b = "sha256:b::component.beta::run".to_string();

        cache.insert(
            key_a.clone(),
            Arc::new(ContractSnapshot::new(
                "sha256:a".to_string(),
                "component.alpha".to_string(),
                "run".to_string(),
                true,
                true,
            )),
        );
        assert!(cache.get(&key_a).is_some());
        cache.insert(
            key_b.clone(),
            Arc::new(ContractSnapshot::new(
                "sha256:b".to_string(),
                "component.beta".to_string(),
                "run".to_string(),
                true,
                true,
            )),
        );
        let stats = cache.stats();
        assert!(stats.hits >= 1);
        assert!(stats.entries >= 1);
        assert!(cache.get(&key_b).is_some());
    }
}