xref: /wasmtime-44.0.1/crates/cache/src/lib.rs (revision 08b7c877)
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