1 use log::{debug, trace, warn}; 2 use serde::{Deserialize, Serialize}; 3 use sha2::{Digest, Sha256}; 4 use std::fs; 5 use std::hash::Hash; 6 use std::hash::Hasher; 7 use std::io::Write; 8 use std::path::{Path, PathBuf}; 9 10 #[macro_use] // for tests 11 mod config; 12 mod worker; 13 14 pub use config::{create_new_config, CacheConfig}; 15 use worker::Worker; 16 17 /// Module level cache entry. 18 pub struct ModuleCacheEntry<'config>(Option<ModuleCacheEntryInner<'config>>); 19 20 struct ModuleCacheEntryInner<'config> { 21 root_path: PathBuf, 22 cache_config: &'config CacheConfig, 23 } 24 25 struct Sha256Hasher(Sha256); 26 27 impl<'config> ModuleCacheEntry<'config> { 28 /// Create the cache entry. 29 pub fn new<'data>(compiler_name: &str, cache_config: &'config CacheConfig) -> Self { 30 if cache_config.enabled() { 31 Self(Some(ModuleCacheEntryInner::new( 32 compiler_name, 33 cache_config, 34 ))) 35 } else { 36 Self(None) 37 } 38 } 39 40 #[cfg(test)] 41 fn from_inner(inner: ModuleCacheEntryInner<'config>) -> Self { 42 Self(Some(inner)) 43 } 44 45 /// Gets cached data if state matches, otherwise calls `compute`. 46 /// 47 /// Data is automatically serialized/deserialized with `bincode`. 48 pub fn get_data<T, U, E>(&self, state: T, compute: fn(&T) -> Result<U, E>) -> Result<U, E> 49 where 50 T: Hash, 51 U: Serialize + for<'a> Deserialize<'a>, 52 { 53 self.get_data_raw( 54 &state, 55 compute, 56 |_state, data| bincode::serialize(data).ok(), 57 |_state, data| bincode::deserialize(&data).ok(), 58 ) 59 } 60 61 /// Gets cached data if state matches, otherwise calls `compute`. 62 /// 63 /// If the cache is disabled or no cached data is found then `compute` is 64 /// called to calculate the data. If the data was found in cache it is 65 /// passed to `deserialize`, which if successful will be the returned value. 66 /// When computed the `serialize` function is used to generate the bytes 67 /// from the returned value. 68 pub fn get_data_raw<T, U, E>( 69 &self, 70 state: &T, 71 // NOTE: These are function pointers instead of closures so that they 72 // don't accidentally close over something not accounted in the cache. 73 compute: fn(&T) -> Result<U, E>, 74 serialize: fn(&T, &U) -> Option<Vec<u8>>, 75 deserialize: fn(&T, Vec<u8>) -> Option<U>, 76 ) -> Result<U, E> 77 where 78 T: Hash, 79 { 80 let inner = match &self.0 { 81 Some(inner) => inner, 82 None => return compute(state), 83 }; 84 85 let mut hasher = Sha256Hasher(Sha256::new()); 86 state.hash(&mut hasher); 87 let hash: [u8; 32] = hasher.0.finalize().into(); 88 // standard encoding uses '/' which can't be used for filename 89 let hash = base64::encode_config(&hash, base64::URL_SAFE_NO_PAD); 90 91 if let Some(cached_val) = inner.get_data(&hash) { 92 if let Some(val) = deserialize(state, cached_val) { 93 let mod_cache_path = inner.root_path.join(&hash); 94 inner.cache_config.on_cache_get_async(&mod_cache_path); // call on success 95 return Ok(val); 96 } 97 } 98 let val_to_cache = compute(state)?; 99 if let Some(bytes) = serialize(state, &val_to_cache) { 100 if inner.update_data(&hash, &bytes).is_some() { 101 let mod_cache_path = inner.root_path.join(&hash); 102 inner.cache_config.on_cache_update_async(&mod_cache_path); // call on success 103 } 104 } 105 Ok(val_to_cache) 106 } 107 } 108 109 impl<'config> ModuleCacheEntryInner<'config> { 110 fn new<'data>(compiler_name: &str, cache_config: &'config CacheConfig) -> Self { 111 // If debug assertions are enabled then assume that we're some sort of 112 // local build. We don't want local builds to stomp over caches between 113 // builds, so just use a separate cache directory based on the mtime of 114 // our executable, which should roughly correlate with "you changed the 115 // source code so you get a different directory". 116 // 117 // Otherwise if this is a release build we use the `GIT_REV` env var 118 // which is either the git rev if installed from git or the crate 119 // version if installed from crates.io. 120 let compiler_dir = if cfg!(debug_assertions) { 121 fn self_mtime() -> Option<String> { 122 let path = std::env::current_exe().ok()?; 123 let metadata = path.metadata().ok()?; 124 let mtime = metadata.modified().ok()?; 125 Some(match mtime.duration_since(std::time::UNIX_EPOCH) { 126 Ok(dur) => format!("{}", dur.as_millis()), 127 Err(err) => format!("m{}", err.duration().as_millis()), 128 }) 129 } 130 let self_mtime = self_mtime().unwrap_or("no-mtime".to_string()); 131 format!( 132 "{comp_name}-{comp_ver}-{comp_mtime}", 133 comp_name = compiler_name, 134 comp_ver = env!("GIT_REV"), 135 comp_mtime = self_mtime, 136 ) 137 } else { 138 format!( 139 "{comp_name}-{comp_ver}", 140 comp_name = compiler_name, 141 comp_ver = env!("GIT_REV"), 142 ) 143 }; 144 let root_path = cache_config.directory().join("modules").join(compiler_dir); 145 146 Self { 147 root_path, 148 cache_config, 149 } 150 } 151 152 fn get_data(&self, hash: &str) -> Option<Vec<u8>> { 153 let mod_cache_path = self.root_path.join(hash); 154 trace!("get_data() for path: {}", mod_cache_path.display()); 155 let compressed_cache_bytes = fs::read(&mod_cache_path).ok()?; 156 let cache_bytes = zstd::decode_all(&compressed_cache_bytes[..]) 157 .map_err(|err| warn!("Failed to decompress cached code: {}", err)) 158 .ok()?; 159 Some(cache_bytes) 160 } 161 162 fn update_data(&self, hash: &str, serialized_data: &[u8]) -> Option<()> { 163 let mod_cache_path = self.root_path.join(hash); 164 trace!("update_data() for path: {}", mod_cache_path.display()); 165 let compressed_data = zstd::encode_all( 166 &serialized_data[..], 167 self.cache_config.baseline_compression_level(), 168 ) 169 .map_err(|err| warn!("Failed to compress cached code: {}", err)) 170 .ok()?; 171 172 // Optimize syscalls: first, try writing to disk. It should succeed in most cases. 173 // Otherwise, try creating the cache directory and retry writing to the file. 174 if fs_write_atomic(&mod_cache_path, "mod", &compressed_data) { 175 return Some(()); 176 } 177 178 debug!( 179 "Attempting to create the cache directory, because \ 180 failed to write cached code to disk, path: {}", 181 mod_cache_path.display(), 182 ); 183 184 let cache_dir = mod_cache_path.parent().unwrap(); 185 fs::create_dir_all(cache_dir) 186 .map_err(|err| { 187 warn!( 188 "Failed to create cache directory, path: {}, message: {}", 189 cache_dir.display(), 190 err 191 ) 192 }) 193 .ok()?; 194 195 if fs_write_atomic(&mod_cache_path, "mod", &compressed_data) { 196 Some(()) 197 } else { 198 None 199 } 200 } 201 } 202 203 impl Hasher for Sha256Hasher { 204 fn finish(&self) -> u64 { 205 panic!("Sha256Hasher doesn't support finish!"); 206 } 207 208 fn write(&mut self, bytes: &[u8]) { 209 self.0.update(bytes); 210 } 211 } 212 213 // Assumption: path inside cache directory. 214 // Then, we don't have to use sound OS-specific exclusive file access. 215 // Note: there's no need to remove temporary file here - cleanup task will do it later. 216 fn fs_write_atomic(path: &Path, reason: &str, contents: &[u8]) -> bool { 217 let lock_path = path.with_extension(format!("wip-atomic-write-{}", reason)); 218 fs::OpenOptions::new() 219 .create_new(true) // atomic file creation (assumption: no one will open it without this flag) 220 .write(true) 221 .open(&lock_path) 222 .and_then(|mut file| file.write_all(contents)) 223 // file should go out of scope and be closed at this point 224 .and_then(|()| fs::rename(&lock_path, &path)) // atomic file rename 225 .map_err(|err| { 226 warn!( 227 "Failed to write file with rename, lock path: {}, target path: {}, err: {}", 228 lock_path.display(), 229 path.display(), 230 err 231 ) 232 }) 233 .is_ok() 234 } 235 236 #[cfg(test)] 237 mod tests; 238