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