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