use crate::core::ribosome::real_ribosome::WasmBackend;
use holo_hash::WasmHash;
use holochain_state::wasm::WasmStore;
use holochain_wasmer_host::module::ModuleBuilder;
use std::fmt::Debug;
use std::sync::Arc;
use wasmer::Engine;
pub(crate) fn make_module_cache(backend: WasmBackend, wasm_store: WasmStore) -> ModuleCache {
match backend {
#[cfg(feature = "wasmer-sys-cranelift")]
WasmBackend::Cranelift => ModuleCache::new(
wasm_store,
holochain_wasmer_host::module::sys::make_cranelift_engine,
holochain_wasmer_host::module::sys::make_runtime_engine,
),
#[cfg(feature = "wasmer-sys-llvm")]
WasmBackend::Llvm => ModuleCache::new(
wasm_store,
holochain_wasmer_host::module::sys::make_llvm_engine,
holochain_wasmer_host::module::sys::make_runtime_engine,
),
#[cfg(feature = "wasmer-wasmi")]
WasmBackend::Wasmi => ModuleCache::new_with_builder(wasm_store, None),
}
}
#[derive(Debug)]
pub struct ModuleCache {
cache: moka::future::Cache<WasmHash, Arc<wasmer::Module>>,
wasm_store: WasmStore,
builder: Option<Arc<ModuleBuilder>>,
}
impl ModuleCache {
#[cfg_attr(feature = "wasmer-wasmi", allow(unused))]
fn new(
wasm_store: WasmStore,
make_engine: fn() -> Engine,
make_runtime_engine: fn() -> Engine,
) -> Self {
Self::new_with_builder(
wasm_store,
Some(Arc::new(ModuleBuilder::new(
make_engine,
make_runtime_engine,
))),
)
}
fn new_with_builder(wasm_store: WasmStore, builder: Option<Arc<ModuleBuilder>>) -> Self {
let cache = moka::future::CacheBuilder::new(64)
.time_to_idle(std::time::Duration::from_hours(1))
.build();
Self {
cache,
wasm_store,
builder,
}
}
pub(super) async fn get(
&self,
wasm_hash: &WasmHash,
) -> Result<Arc<wasmer::Module>, wasmer::RuntimeError> {
self.cache.try_get_with(wasm_hash.clone(), async {
if let Some(builder) = self.builder.clone() {
let maybe_serialized = self
.wasm_store
.as_read()
.get_compiled(wasm_hash)
.await
.map_err(|err| {
tracing::error!(?err, "Failed to read compiled module from the database");
wasmer::RuntimeError::new(format!("Failed to read compiled module from the database: {}", err))
})?;
if let Some(serialized) = maybe_serialized {
match builder.from_serialized_module(serialized) {
Ok(module) => {
self.cache.insert(wasm_hash.clone(), module.clone()).await;
return Ok(module);
}
Err(err) => {
tracing::info!(?err, ?wasm_hash, "Invalid compiled, serialized module found in the database. Attempting recovery");
}
}
}
self.get_from_source(wasm_hash).await
} else {
#[cfg(not(feature = "wasmer-wasmi"))]
panic!("Missing builder");
#[cfg(feature = "wasmer-wasmi")]
self.get_interpreted(wasm_hash).await
}
}).await.map_err(|e| (*e).clone())
}
pub(super) async fn evict_from_memory_cache(&self, wasm_hash: &WasmHash) {
self.cache.invalidate(wasm_hash).await;
}
#[cfg(feature = "wasmer-wasmi")]
async fn get_interpreted(
&self,
wasm_hash: &WasmHash,
) -> Result<Arc<wasmer::Module>, wasmer::RuntimeError> {
let maybe_code = self
.wasm_store
.as_read()
.get(wasm_hash)
.await
.map_err(|err| {
tracing::error!(?err, "Failed to read WASM source code from the database");
wasmer::RuntimeError::new(format!(
"Failed to read WASM source code from the database: {err}"
))
})?;
let Some(source) = maybe_code else {
tracing::warn!(
?wasm_hash,
"No source code found in the database, cannot populate the cache"
);
return Err(wasmer::RuntimeError::new("Missing WASM source"));
};
let module = holochain_wasmer_host::module::wasmi::build_module(&source.content.code)?;
self.cache.insert(wasm_hash.clone(), module.clone()).await;
Ok(module)
}
#[cfg(feature = "test_utils")]
pub fn is_in_memory_cache(&self, wasm_hash: &WasmHash) -> bool {
self.cache.contains_key(wasm_hash)
}
#[cfg(feature = "test_utils")]
pub async fn is_compiled_wasm_stored(
&self,
wasm_hash: &WasmHash,
) -> holochain_state::prelude::StateQueryResult<bool> {
self.wasm_store.as_read().contains_compiled(wasm_hash).await
}
async fn get_from_source(
&self,
wasm_hash: &WasmHash,
) -> Result<Arc<wasmer::Module>, wasmer::RuntimeError> {
let maybe_code = self
.wasm_store
.as_read()
.get(wasm_hash)
.await
.map_err(|err| {
tracing::error!(?err, "Failed to read WASM source code from the database");
wasmer::RuntimeError::new(format!(
"Failed to read WASM source code from the database: {err}"
))
})?;
let Some(source) = maybe_code else {
tracing::warn!(
?wasm_hash,
"No source code found in the database, cannot populate the cache"
);
return Err(wasmer::RuntimeError::new("Missing WASM source"));
};
let module = match tokio::task::spawn_blocking({
let builder = self.builder.clone().expect("Missing builder");
move || {
builder.from_binary(&source.code)
}
})
.await
{
Ok(Ok(module)) => module,
Ok(Err(err)) => return Err(err),
Err(err) => {
tracing::error!(
?err,
"Blocking call for the module compile operation failed"
);
return Err(wasmer::RuntimeError::new(
"Blocking call for the module compile operation failed",
));
}
};
let serialized = match module.serialize() {
Ok(serialized) => {
if let Err(err) = self
.wasm_store
.put_compiled(wasm_hash.clone(), serialized.clone())
.await
{
tracing::error!(
?err,
?wasm_hash,
"Failed to write WASM source code to database"
);
}
serialized
}
Err(err) => {
tracing::error!(?err, "Failed to serialize WASM module after building, the module will be rebuilt again next time it is requested");
return Err(wasmer::RuntimeError::new(
"Failed to build WASM source code",
));
}
};
let builder = self.builder.clone().expect("Missing builder");
let module = builder.from_serialized_module(serialized)?;
self.cache.insert(wasm_hash.clone(), module.clone()).await;
Ok(module)
}
}
#[cfg(all(test, feature = "test_utils"))]
mod tests {
use super::*;
use holo_hash::HasHash;
use holochain_types::prelude::{DnaWasm, DnaWasmHashed};
use holochain_wasm_test_utils::TestWasm;
#[cfg(any(feature = "wasmer-sys-cranelift", feature = "wasmer-sys-llvm"))]
#[tokio::test(flavor = "multi_thread")]
async fn cache_real_wasm() {
let store = WasmStore::test_new();
let dna_wasm: DnaWasm = TestWasm::Foo.into();
let dna_wasm = DnaWasmHashed::from_content(dna_wasm).await;
let wasm_hash = dna_wasm.as_hash().clone();
store.put(dna_wasm).await.unwrap();
let cache = make_module_cache(WasmBackend::new(), store);
let in_memory_cache = cache.is_in_memory_cache(&wasm_hash);
assert!(!in_memory_cache);
let compiled_stored = cache.is_compiled_wasm_stored(&wasm_hash).await.unwrap();
assert!(!compiled_stored);
cache.get(&wasm_hash).await.unwrap();
let in_memory_cache = cache.is_in_memory_cache(&wasm_hash);
assert!(in_memory_cache);
let compiled_stored = cache.is_compiled_wasm_stored(&wasm_hash).await.unwrap();
assert!(compiled_stored);
}
#[cfg(any(feature = "wasmer-sys-cranelift", feature = "wasmer-sys-llvm"))]
#[tokio::test(flavor = "multi_thread")]
async fn invalid_wasm_in_database() {
let store = WasmStore::test_new();
let dna_wasm = DnaWasm::new_invalid();
let dna_wasm = DnaWasmHashed::from_content(dna_wasm).await;
let wasm_hash = dna_wasm.as_hash().clone();
store.put(dna_wasm).await.unwrap();
let cache = make_module_cache(WasmBackend::new(), store);
let result = cache.get(&wasm_hash).await;
assert!(result.is_err());
}
#[cfg(any(feature = "wasmer-sys-cranelift", feature = "wasmer-sys-llvm"))]
#[tokio::test(flavor = "multi_thread")]
async fn invalid_built_wasm() {
let store = WasmStore::test_new();
let dna_wasm: DnaWasm = TestWasm::Foo.into();
let dna_wasm = DnaWasmHashed::from_content(dna_wasm).await;
let wasm_hash = dna_wasm.as_hash().clone();
store.put(dna_wasm).await.unwrap();
store
.put_compiled(wasm_hash.clone(), bytes::Bytes::new())
.await
.unwrap();
let cache = make_module_cache(WasmBackend::new(), store);
cache.get(&wasm_hash).await.unwrap();
let compiled = cache
.wasm_store
.as_read()
.get_compiled(&wasm_hash)
.await
.unwrap()
.unwrap();
assert!(!compiled.is_empty());
}
#[cfg(feature = "wasmer-wasmi")]
#[tokio::test(flavor = "multi_thread")]
async fn cache_wasmi_module() {
let store = WasmStore::test_new();
let dna_wasm: DnaWasm = TestWasm::Foo.into();
let dna_wasm = DnaWasmHashed::from_content(dna_wasm).await;
let wasm_hash = dna_wasm.as_hash().clone();
store.put(dna_wasm).await.unwrap();
let cache = make_module_cache(WasmBackend::new(), store);
cache.get(&wasm_hash).await.unwrap();
let in_memory_cached = cache.is_in_memory_cache(&wasm_hash);
assert!(in_memory_cached);
let compiled_stored = cache.is_compiled_wasm_stored(&wasm_hash).await.unwrap();
assert!(!compiled_stored);
}
}