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