1use anyhow::Result;
9use base64::Engine;
10use log::{debug, trace, warn};
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use std::hash::Hash;
14use std::hash::Hasher;
15use std::io::Write;
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
19use std::time::Duration;
20use std::{fs, io};
21
22#[macro_use] mod config;
24mod worker;
25
26pub use config::{CacheConfig, create_new_config};
27use worker::Worker;
28
29#[derive(Debug, Clone)]
31pub struct Cache {
32 config: CacheConfig,
33 worker: Worker,
34 state: Arc<CacheState>,
35}
36
37macro_rules! generate_config_setting_getter {
38 ($setting:ident: $setting_type:ty) => {
39 #[doc = concat!("Returns ", "`", stringify!($setting), "`.")]
40 pub fn $setting(&self) -> $setting_type {
43 self.config.$setting()
44 }
45 };
46}
47
48impl Cache {
49 pub fn new(mut config: CacheConfig) -> Result<Self> {
57 config.validate()?;
58 Ok(Self {
59 worker: Worker::start_new(&config),
60 config,
61 state: Default::default(),
62 })
63 }
64
65 pub fn from_file(path: Option<&Path>) -> Result<Self> {
83 let config = CacheConfig::from_file(path)?;
84 Self::new(config)
85 }
86
87 generate_config_setting_getter!(worker_event_queue_size: u64);
88 generate_config_setting_getter!(baseline_compression_level: i32);
89 generate_config_setting_getter!(optimized_compression_level: i32);
90 generate_config_setting_getter!(optimized_compression_usage_counter_threshold: u64);
91 generate_config_setting_getter!(cleanup_interval: Duration);
92 generate_config_setting_getter!(optimizing_compression_task_timeout: Duration);
93 generate_config_setting_getter!(allowed_clock_drift_for_files_from_future: Duration);
94 generate_config_setting_getter!(file_count_soft_limit: u64);
95 generate_config_setting_getter!(files_total_size_soft_limit: u64);
96 generate_config_setting_getter!(file_count_limit_percent_if_deleting: u8);
97 generate_config_setting_getter!(files_total_size_limit_percent_if_deleting: u8);
98
99 pub fn directory(&self) -> &PathBuf {
103 &self.config.directory()
104 }
105
106 #[cfg(test)]
107 fn worker(&self) -> &Worker {
108 &self.worker
109 }
110
111 pub fn cache_hits(&self) -> usize {
113 self.state.hits.load(SeqCst)
114 }
115
116 pub fn cache_misses(&self) -> usize {
118 self.state.misses.load(SeqCst)
119 }
120
121 pub(crate) fn on_cache_get_async(&self, path: impl AsRef<Path>) {
122 self.state.hits.fetch_add(1, SeqCst);
123 self.worker.on_cache_get_async(path)
124 }
125
126 pub(crate) fn on_cache_update_async(&self, path: impl AsRef<Path>) {
127 self.state.misses.fetch_add(1, SeqCst);
128 self.worker.on_cache_update_async(path)
129 }
130}
131
132#[derive(Default, Debug)]
133struct CacheState {
134 hits: AtomicUsize,
135 misses: AtomicUsize,
136}
137
138pub struct ModuleCacheEntry<'cache>(Option<ModuleCacheEntryInner<'cache>>);
140
141struct ModuleCacheEntryInner<'cache> {
142 root_path: PathBuf,
143 cache: &'cache Cache,
144}
145
146struct Sha256Hasher(Sha256);
147
148impl<'cache> ModuleCacheEntry<'cache> {
149 pub fn new(compiler_name: &str, cache: Option<&'cache Cache>) -> Self {
151 Self(cache.map(|cache| ModuleCacheEntryInner::new(compiler_name, cache)))
152 }
153
154 #[cfg(test)]
155 fn from_inner(inner: ModuleCacheEntryInner<'cache>) -> Self {
156 Self(Some(inner))
157 }
158
159 pub fn get_data<T, U, E>(&self, state: T, compute: fn(&T) -> Result<U, E>) -> Result<U, E>
163 where
164 T: Hash,
165 U: Serialize + for<'a> Deserialize<'a>,
166 {
167 self.get_data_raw(
168 &state,
169 compute,
170 |_state, data| postcard::to_allocvec(data).ok(),
171 |_state, data| postcard::from_bytes(&data).ok(),
172 )
173 }
174
175 pub fn get_data_raw<T, U, E>(
183 &self,
184 state: &T,
185 compute: fn(&T) -> Result<U, E>,
188 serialize: fn(&T, &U) -> Option<Vec<u8>>,
189 deserialize: fn(&T, Vec<u8>) -> Option<U>,
190 ) -> Result<U, E>
191 where
192 T: Hash,
193 {
194 let inner = match &self.0 {
195 Some(inner) => inner,
196 None => return compute(state),
197 };
198
199 let mut hasher = Sha256Hasher(Sha256::new());
200 state.hash(&mut hasher);
201 let hash: [u8; 32] = hasher.0.finalize().into();
202 let hash = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&hash);
204
205 if let Some(cached_val) = inner.get_data(&hash) {
206 if let Some(val) = deserialize(state, cached_val) {
207 let mod_cache_path = inner.root_path.join(&hash);
208 inner.cache.on_cache_get_async(&mod_cache_path); return Ok(val);
210 }
211 }
212 let val_to_cache = compute(state)?;
213 if let Some(bytes) = serialize(state, &val_to_cache) {
214 if inner.update_data(&hash, &bytes).is_some() {
215 let mod_cache_path = inner.root_path.join(&hash);
216 inner.cache.on_cache_update_async(&mod_cache_path); }
218 }
219 Ok(val_to_cache)
220 }
221}
222
223impl<'cache> ModuleCacheEntryInner<'cache> {
224 fn new(compiler_name: &str, cache: &'cache Cache) -> Self {
225 let compiler_dir = if cfg!(debug_assertions) {
235 fn self_mtime() -> Option<String> {
236 let path = std::env::current_exe().ok()?;
237 let metadata = path.metadata().ok()?;
238 let mtime = metadata.modified().ok()?;
239 Some(match mtime.duration_since(std::time::UNIX_EPOCH) {
240 Ok(dur) => format!("{}", dur.as_millis()),
241 Err(err) => format!("m{}", err.duration().as_millis()),
242 })
243 }
244 let self_mtime = self_mtime().unwrap_or("no-mtime".to_string());
245 format!(
246 "{comp_name}-{comp_ver}-{comp_mtime}",
247 comp_name = compiler_name,
248 comp_ver = env!("GIT_REV"),
249 comp_mtime = self_mtime,
250 )
251 } else {
252 format!(
253 "{comp_name}-{comp_ver}",
254 comp_name = compiler_name,
255 comp_ver = env!("GIT_REV"),
256 )
257 };
258 let root_path = cache.directory().join("modules").join(compiler_dir);
259
260 Self { root_path, cache }
261 }
262
263 fn get_data(&self, hash: &str) -> Option<Vec<u8>> {
264 let mod_cache_path = self.root_path.join(hash);
265 trace!("get_data() for path: {}", mod_cache_path.display());
266 let compressed_cache_bytes = fs::read(&mod_cache_path).ok()?;
267 let cache_bytes = zstd::decode_all(&compressed_cache_bytes[..])
268 .map_err(|err| warn!("Failed to decompress cached code: {}", err))
269 .ok()?;
270 Some(cache_bytes)
271 }
272
273 fn update_data(&self, hash: &str, serialized_data: &[u8]) -> Option<()> {
274 let mod_cache_path = self.root_path.join(hash);
275 trace!("update_data() for path: {}", mod_cache_path.display());
276 let compressed_data = zstd::encode_all(
277 &serialized_data[..],
278 self.cache.baseline_compression_level(),
279 )
280 .map_err(|err| warn!("Failed to compress cached code: {}", err))
281 .ok()?;
282
283 if fs_write_atomic(&mod_cache_path, "mod", &compressed_data).is_ok() {
286 return Some(());
287 }
288
289 debug!(
290 "Attempting to create the cache directory, because \
291 failed to write cached code to disk, path: {}",
292 mod_cache_path.display(),
293 );
294
295 let cache_dir = mod_cache_path.parent().unwrap();
296 fs::create_dir_all(cache_dir)
297 .map_err(|err| {
298 warn!(
299 "Failed to create cache directory, path: {}, message: {}",
300 cache_dir.display(),
301 err
302 )
303 })
304 .ok()?;
305
306 match fs_write_atomic(&mod_cache_path, "mod", &compressed_data) {
307 Ok(_) => Some(()),
308 Err(err) => {
309 warn!(
310 "Failed to write file with rename, target path: {}, err: {}",
311 mod_cache_path.display(),
312 err
313 );
314 None
315 }
316 }
317 }
318}
319
320impl Hasher for Sha256Hasher {
321 fn finish(&self) -> u64 {
322 panic!("Sha256Hasher doesn't support finish!");
323 }
324
325 fn write(&mut self, bytes: &[u8]) {
326 self.0.update(bytes);
327 }
328}
329
330fn fs_write_atomic(path: &Path, reason: &str, contents: &[u8]) -> io::Result<()> {
334 let lock_path = path.with_extension(format!("wip-atomic-write-{reason}"));
335 fs::OpenOptions::new()
336 .create_new(true) .write(true)
338 .open(&lock_path)
339 .and_then(|mut file| file.write_all(contents))
340 .and_then(|()| fs::rename(&lock_path, &path)) }
343
344#[cfg(test)]
345mod tests;