wasmer_wasix/runtime/module_cache/
filesystem.rs1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use tempfile::NamedTempFile;
5use tokio::io::AsyncWriteExt;
6use wasmer::{Engine, Module};
7
8use crate::runtime::module_cache::{CacheError, ModuleCache, ModuleHash};
9use crate::runtime::task_manager::tokio::TokioTaskManager;
10
11#[derive(Debug, Clone)]
14pub struct FileSystemCache {
15 cache_dir: PathBuf,
16 task_manager: Arc<TokioTaskManager>,
17}
18
19impl FileSystemCache {
20 pub fn new(cache_dir: impl Into<PathBuf>, task_manager: Arc<TokioTaskManager>) -> Self {
21 FileSystemCache {
22 cache_dir: cache_dir.into(),
23 task_manager,
24 }
25 }
26
27 pub fn cache_dir(&self) -> &Path {
28 &self.cache_dir
29 }
30
31 fn path(&self, key: ModuleHash, deterministic_id: &str, artifact_format: &str) -> PathBuf {
32 let artifact_version = wasmer_types::MetadataHeader::CURRENT_VERSION;
33 self.cache_dir
34 .join(format!(
35 "{deterministic_id}-{artifact_format}-v{artifact_version}"
36 ))
37 .join(key.to_string())
38 .with_extension("bin")
39 }
40}
41
42#[tracing::instrument(level = "debug", skip_all, fields(? path))]
46async fn tokio_load(path: PathBuf, engine: Engine) -> Result<Module, CacheError> {
47 let artifact_path = path.clone();
48 let deserialized =
49 tokio::task::spawn_blocking(move || deserialize_file(&artifact_path, &engine))
50 .await
51 .unwrap();
52 match deserialized {
53 Ok(m) => {
54 tracing::debug!("Cache hit!");
55 Ok(m)
56 }
57 Err(e) => {
58 tracing::debug!(
59 path=%path.display(),
60 error=&e as &dyn std::error::Error,
61 "Deleting the cache file because the artifact couldn't be deserialized",
62 );
63
64 if let Err(e) = std::fs::remove_file(&path) {
65 tracing::warn!(
66 path=%path.display(),
67 error=&e as &dyn std::error::Error,
68 "Unable to remove the corrupted cache file",
69 );
70 }
71 Err(e)
72 }
73 }
74}
75
76async fn tokio_contains(path: PathBuf) -> Result<bool, CacheError> {
80 tokio::fs::try_exists(&path)
81 .await
82 .map_err(|e| CacheError::FileRead {
83 path: path.clone(),
84 error: e,
85 })
86}
87
88#[tracing::instrument(level = "debug", skip_all, fields(? path))]
92async fn tokio_save(path: PathBuf, module: Module) -> Result<(), CacheError> {
93 let parent = path
94 .parent()
95 .expect("Unreachable - always created by joining onto cache_dir");
96
97 if let Err(e) = tokio::fs::create_dir_all(parent).await {
98 tracing::warn!(
99 dir=%parent.display(),
100 error=&e as &dyn std::error::Error,
101 "Unable to create the cache directory",
102 );
103 }
104
105 let (file, temp) = NamedTempFile::new_in(parent)
109 .map_err(CacheError::other)?
110 .into_parts();
111
112 let mut file = tokio::fs::File::from_std(file);
113
114 let serialized = tokio::task::spawn_blocking(move || module.serialize())
115 .await
116 .unwrap()?;
117
118 let mut writer = tokio::io::BufWriter::new(&mut file);
119 if let Err(error) = writer.write_all(&serialized).await {
120 return Err(CacheError::FileWrite { path, error });
121 }
122 if let Err(error) = writer.flush().await {
123 return Err(CacheError::FileWrite { path, error });
124 }
125
126 temp.persist(&path).map_err(CacheError::other)?;
127 tracing::debug!(path=%path.display(), "Saved to disk");
128
129 Ok(())
130}
131
132#[async_trait::async_trait]
133impl ModuleCache for FileSystemCache {
134 #[tracing::instrument(level = "debug", skip_all, fields(% key))]
135 async fn load(&self, key: ModuleHash, engine: &Engine) -> Result<Module, CacheError> {
136 let path = self.path(key, &engine.deterministic_id(), &engine.artifact_format());
137 let engine = engine.clone();
138
139 self.task_manager
142 .runtime_handle()
143 .spawn(tokio_load(path, engine))
144 .await
145 .unwrap()
146 }
147
148 async fn contains(&self, key: ModuleHash, engine: &Engine) -> Result<bool, CacheError> {
149 let path = self.path(key, &engine.deterministic_id(), &engine.artifact_format());
150
151 self.task_manager
154 .runtime_handle()
155 .spawn(tokio_contains(path))
156 .await
157 .unwrap()
158 }
159
160 #[tracing::instrument(level = "debug", skip_all, fields(% key))]
161 async fn save(
162 &self,
163 key: ModuleHash,
164 engine: &Engine,
165 module: &Module,
166 ) -> Result<(), CacheError> {
167 let path = self.path(key, &engine.deterministic_id(), &engine.artifact_format());
168 let module = module.clone();
169
170 self.task_manager
173 .runtime_handle()
174 .spawn(tokio_save(path, module))
175 .await
176 .unwrap()
177 }
178}
179
180fn deserialize_file(path: &Path, engine: &Engine) -> Result<Module, CacheError> {
181 match unsafe { Module::deserialize_from_file(engine, path) } {
197 Ok(m) => Ok(m),
199 Err(wasmer::DeserializeError::Io(error))
200 if error.kind() == std::io::ErrorKind::NotFound =>
201 {
202 Err(CacheError::NotFound)
203 }
204 Err(wasmer::DeserializeError::Io(error)) => Err(CacheError::FileRead {
205 path: path.to_path_buf(),
206 error,
207 }),
208 Err(e) => Err(CacheError::Deserialize(e)),
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use crate::runtime::task_manager::tokio::TokioTaskManager;
215 use tempfile::TempDir;
216
217 use super::*;
218
219 const ADD_WAT: &[u8] = br#"(
220 module
221 (func
222 (export "add")
223 (param $x i64)
224 (param $y i64)
225 (result i64)
226 (i64.add (local.get $x) (local.get $y)))
227 )"#;
228
229 fn create_tokio_task_manager() -> Arc<TokioTaskManager> {
230 Arc::new(TokioTaskManager::new(tokio::runtime::Handle::current()))
231 }
232
233 #[tokio::test]
234 async fn save_to_disk() {
235 let temp = TempDir::new().unwrap();
236 let engine = Engine::default();
237 let module = Module::new(&engine, ADD_WAT).unwrap();
238 let cache = FileSystemCache::new(temp.path(), create_tokio_task_manager());
239 let key = ModuleHash::from_bytes([0; _]);
240 let expected_path = cache.path(key, &engine.deterministic_id(), &engine.artifact_format());
241
242 cache.save(key, &engine, &module).await.unwrap();
243
244 assert!(expected_path.exists());
245 }
246
247 #[tokio::test]
248 async fn create_cache_dir_automatically() {
249 let temp = TempDir::new().unwrap();
250 let engine = Engine::default();
251 let module = Module::new(&engine, ADD_WAT).unwrap();
252 let cache_dir = temp.path().join("this").join("doesn't").join("exist");
253 assert!(!cache_dir.exists());
254 let cache = FileSystemCache::new(&cache_dir, create_tokio_task_manager());
255 let key = ModuleHash::from_bytes([0; _]);
256
257 cache.save(key, &engine, &module).await.unwrap();
258
259 assert!(cache_dir.is_dir());
260 }
261
262 #[tokio::test]
263 async fn missing_file() {
264 let temp = TempDir::new().unwrap();
265 let engine = Engine::default();
266 let key = ModuleHash::from_bytes([0; _]);
267 let cache = FileSystemCache::new(temp.path(), create_tokio_task_manager());
268
269 let err = cache.load(key, &engine).await.unwrap_err();
270
271 assert!(matches!(err, CacheError::NotFound));
272 }
273
274 #[tokio::test]
275 async fn load_from_disk() {
276 let temp = TempDir::new().unwrap();
277 let engine = Engine::default();
278 let module = Module::new(&engine, ADD_WAT).unwrap();
279 let key = ModuleHash::from_bytes([0; _]);
280 let cache = FileSystemCache::new(temp.path(), create_tokio_task_manager());
281 let expected_path = cache.path(key, &engine.deterministic_id(), &engine.artifact_format());
282 std::fs::create_dir_all(expected_path.parent().unwrap()).unwrap();
283 let serialized = module.serialize().unwrap();
284 std::fs::write(&expected_path, &serialized).unwrap();
285
286 let module = cache.load(key, &engine).await.unwrap();
287
288 let exports: Vec<_> = module
289 .exports()
290 .map(|export| export.name().to_string())
291 .collect();
292 assert_eq!(exports, ["add"]);
293 }
294}