Skip to main content

camel_proto_compiler/
cache.rs

1use std::collections::HashMap;
2use std::collections::VecDeque;
3use std::path::Path;
4use std::sync::Mutex;
5
6use prost_reflect::DescriptorPool;
7
8use crate::compiler::compile_proto;
9use crate::{ProtoCompileError, hash_proto_content};
10
11const DEFAULT_MAX_ENTRIES: usize = 1000;
12
13pub struct ProtoCache {
14    pools: Mutex<HashMap<String, DescriptorPool>>,
15    order: Mutex<VecDeque<String>>,
16    max_entries: usize,
17}
18
19impl Default for ProtoCache {
20    fn default() -> Self {
21        Self {
22            pools: Mutex::new(HashMap::new()),
23            order: Mutex::new(VecDeque::new()),
24            max_entries: DEFAULT_MAX_ENTRIES,
25        }
26    }
27}
28
29impl ProtoCache {
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Create a cache with a custom maximum number of entries.
35    ///
36    /// When the cache reaches capacity, the oldest entry is evicted (FIFO).
37    pub fn with_max_entries(max_entries: usize) -> Self {
38        Self {
39            pools: Mutex::new(HashMap::new()),
40            order: Mutex::new(VecDeque::with_capacity(max_entries)),
41            max_entries: max_entries.max(1),
42        }
43    }
44
45    fn insert_entry(&self, key: String, pool: DescriptorPool) {
46        let mut pools = self.pools.lock().expect("mutex poisoned"); // allow-unwrap
47        let mut order = self.order.lock().expect("mutex poisoned"); // allow-unwrap
48
49        // If key already exists, just update the pool (no eviction needed).
50        // We cannot use the entry API here because we need the key for the
51        // order queue below, and entry() takes ownership.
52        #[allow(clippy::map_entry)]
53        if pools.contains_key(&key) {
54            pools.insert(key, pool);
55            return;
56        }
57
58        // Evict oldest entries until we have room.
59        while pools.len() >= self.max_entries {
60            if let Some(old_key) = order.pop_front() {
61                pools.remove(&old_key);
62            } else {
63                break;
64            }
65        }
66
67        order.push_back(key.clone());
68        pools.insert(key, pool);
69    }
70
71    pub fn get_or_compile<P, I>(
72        &self,
73        proto_path: P,
74        includes: I,
75    ) -> Result<DescriptorPool, ProtoCompileError>
76    where
77        P: AsRef<Path>,
78        I: IntoIterator,
79        I::Item: AsRef<Path>,
80    {
81        let proto_path = proto_path.as_ref();
82        let include_paths = includes
83            .into_iter()
84            .map(|p| p.as_ref().to_path_buf())
85            .collect::<Vec<_>>();
86
87        let key = format!(
88            "{}:{}",
89            proto_path.display(),
90            hash_proto_content(proto_path)?
91        );
92
93        if let Some(pool) = self
94            .pools
95            .lock()
96            .expect("mutex poisoned") // allow-unwrap
97            .get(&key)
98            .cloned()
99        {
100            return Ok(pool);
101        }
102
103        let pool = compile_proto(proto_path, &include_paths)?;
104        self.insert_entry(key, pool.clone());
105        Ok(pool)
106    }
107
108    pub fn len(&self) -> usize {
109        self.pools.lock().expect("mutex poisoned").len() // allow-unwrap
110    }
111
112    pub fn is_empty(&self) -> bool {
113        self.pools.lock().expect("mutex poisoned").is_empty() // allow-unwrap
114    }
115}