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 the `compute`. 46 pub fn get_data<T, U, E>(&self, state: T, compute: fn(T) -> Result<U, E>) -> Result<U, E> 47 where 48 T: Hash, 49 U: Serialize + for<'a> Deserialize<'a>, 50 { 51 let inner = match &self.0 { 52 Some(inner) => inner, 53 None => return compute(state), 54 }; 55 56 let mut hasher = Sha256Hasher(Sha256::new()); 57 state.hash(&mut hasher); 58 let hash: [u8; 32] = hasher.0.finalize().into(); 59 // standard encoding uses '/' which can't be used for filename 60 let hash = base64::encode_config(&hash, base64::URL_SAFE_NO_PAD); 61 62 if let Some(cached_val) = inner.get_data(&hash) { 63 let mod_cache_path = inner.root_path.join(&hash); 64 inner.cache_config.on_cache_get_async(&mod_cache_path); // call on success 65 return Ok(cached_val); 66 } 67 let val_to_cache = compute(state)?; 68 if inner.update_data(&hash, &val_to_cache).is_some() { 69 let mod_cache_path = inner.root_path.join(&hash); 70 inner.cache_config.on_cache_update_async(&mod_cache_path); // call on success 71 } 72 Ok(val_to_cache) 73 } 74 } 75 76 impl<'config> ModuleCacheEntryInner<'config> { 77 fn new<'data>(compiler_name: &str, cache_config: &'config CacheConfig) -> Self { 78 // If debug assertions are enabled then assume that we're some sort of 79 // local build. We don't want local builds to stomp over caches between 80 // builds, so just use a separate cache directory based on the mtime of 81 // our executable, which should roughly correlate with "you changed the 82 // source code so you get a different directory". 83 // 84 // Otherwise if this is a release build we use the `GIT_REV` env var 85 // which is either the git rev if installed from git or the crate 86 // version if installed from crates.io. 87 let compiler_dir = if cfg!(debug_assertions) { 88 fn self_mtime() -> Option<String> { 89 let path = std::env::current_exe().ok()?; 90 let metadata = path.metadata().ok()?; 91 let mtime = metadata.modified().ok()?; 92 Some(match mtime.duration_since(std::time::UNIX_EPOCH) { 93 Ok(dur) => format!("{}", dur.as_millis()), 94 Err(err) => format!("m{}", err.duration().as_millis()), 95 }) 96 } 97 let self_mtime = self_mtime().unwrap_or("no-mtime".to_string()); 98 format!( 99 "{comp_name}-{comp_ver}-{comp_mtime}", 100 comp_name = compiler_name, 101 comp_ver = env!("GIT_REV"), 102 comp_mtime = self_mtime, 103 ) 104 } else { 105 format!( 106 "{comp_name}-{comp_ver}", 107 comp_name = compiler_name, 108 comp_ver = env!("GIT_REV"), 109 ) 110 }; 111 let root_path = cache_config.directory().join("modules").join(compiler_dir); 112 113 Self { 114 root_path, 115 cache_config, 116 } 117 } 118 119 fn get_data<T>(&self, hash: &str) -> Option<T> 120 where 121 T: for<'a> Deserialize<'a>, 122 { 123 let mod_cache_path = self.root_path.join(hash); 124 trace!("get_data() for path: {}", mod_cache_path.display()); 125 let compressed_cache_bytes = fs::read(&mod_cache_path).ok()?; 126 let cache_bytes = zstd::decode_all(&compressed_cache_bytes[..]) 127 .map_err(|err| warn!("Failed to decompress cached code: {}", err)) 128 .ok()?; 129 bincode::deserialize(&cache_bytes[..]) 130 .map_err(|err| warn!("Failed to deserialize cached code: {}", err)) 131 .ok() 132 } 133 134 fn update_data<T: Serialize>(&self, hash: &str, data: &T) -> Option<()> { 135 let mod_cache_path = self.root_path.join(hash); 136 trace!("update_data() for path: {}", mod_cache_path.display()); 137 let serialized_data = bincode::serialize(&data) 138 .map_err(|err| warn!("Failed to serialize cached code: {}", err)) 139 .ok()?; 140 let compressed_data = zstd::encode_all( 141 &serialized_data[..], 142 self.cache_config.baseline_compression_level(), 143 ) 144 .map_err(|err| warn!("Failed to compress cached code: {}", err)) 145 .ok()?; 146 147 // Optimize syscalls: first, try writing to disk. It should succeed in most cases. 148 // Otherwise, try creating the cache directory and retry writing to the file. 149 if fs_write_atomic(&mod_cache_path, "mod", &compressed_data) { 150 return Some(()); 151 } 152 153 debug!( 154 "Attempting to create the cache directory, because \ 155 failed to write cached code to disk, path: {}", 156 mod_cache_path.display(), 157 ); 158 159 let cache_dir = mod_cache_path.parent().unwrap(); 160 fs::create_dir_all(cache_dir) 161 .map_err(|err| { 162 warn!( 163 "Failed to create cache directory, path: {}, message: {}", 164 cache_dir.display(), 165 err 166 ) 167 }) 168 .ok()?; 169 170 if fs_write_atomic(&mod_cache_path, "mod", &compressed_data) { 171 Some(()) 172 } else { 173 None 174 } 175 } 176 } 177 178 impl Hasher for Sha256Hasher { 179 fn finish(&self) -> u64 { 180 panic!("Sha256Hasher doesn't support finish!"); 181 } 182 183 fn write(&mut self, bytes: &[u8]) { 184 self.0.update(bytes); 185 } 186 } 187 188 // Assumption: path inside cache directory. 189 // Then, we don't have to use sound OS-specific exclusive file access. 190 // Note: there's no need to remove temporary file here - cleanup task will do it later. 191 fn fs_write_atomic(path: &Path, reason: &str, contents: &[u8]) -> bool { 192 let lock_path = path.with_extension(format!("wip-atomic-write-{}", reason)); 193 fs::OpenOptions::new() 194 .create_new(true) // atomic file creation (assumption: no one will open it without this flag) 195 .write(true) 196 .open(&lock_path) 197 .and_then(|mut file| file.write_all(contents)) 198 // file should go out of scope and be closed at this point 199 .and_then(|()| fs::rename(&lock_path, &path)) // atomic file rename 200 .map_err(|err| { 201 warn!( 202 "Failed to write file with rename, lock path: {}, target path: {}, err: {}", 203 lock_path.display(), 204 path.display(), 205 err 206 ) 207 }) 208 .is_ok() 209 } 210 211 #[cfg(test)] 212 mod tests; 213