diff --git a/packages/fuel-indexer-lib/src/lib.rs b/packages/fuel-indexer-lib/src/lib.rs index 717f30a1d..69bebe5c0 100644 --- a/packages/fuel-indexer-lib/src/lib.rs +++ b/packages/fuel-indexer-lib/src/lib.rs @@ -51,6 +51,7 @@ pub enum WasmIndexerError { GetStringFailed, AllocMissing, AllocFailed, + Panic, GeneralError, } @@ -70,6 +71,7 @@ impl From for WasmIndexerError { 10 => Self::GetStringFailed, 11 => Self::AllocMissing, 12 => Self::AllocFailed, + 13 => Self::Panic, _ => Self::GeneralError, } } @@ -79,10 +81,10 @@ impl std::fmt::Display for WasmIndexerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::SerializationError => { - write!(f, "Failed to serialize object.") + write!(f, "Failed to serialize object") } Self::DeserializationError => { - write!(f, "Failed to deserialize object.") + write!(f, "Failed to deserialize object") } Self::UnableToSaveListType => { write!(f, "Failed to save list") @@ -96,7 +98,7 @@ impl std::fmt::Display for WasmIndexerError { Self::KillSwitch => { write!( f, - "Indexer kill switch has been triggered. Indexer will halt." + "Indexer kill switch has been triggered. Indexer will halt" ) } Self::DatabaseError => { @@ -106,21 +108,24 @@ impl std::fmt::Display for WasmIndexerError { write!(f, "Some blocks are missing") } Self::InvalidLogLevel => { - write!(f, "Invalid log level.") + write!(f, "Invalid log level") } Self::GetObjectIdFailed => { - write!(f, "get_object_id failed.") + write!(f, "get_object_id failed") } Self::GetStringFailed => { - write!(f, "get_string failed.") + write!(f, "get_string failed") } Self::AllocMissing => { - write!(f, "alloc is missing.") + write!(f, "alloc is missing") } Self::AllocFailed => { - write!(f, "alloc failed.") + write!(f, "alloc failed") } - Self::GeneralError => write!(f, "Some unspecified WASM error occurred."), + Self::Panic => { + write!(f, "Panic") + } + Self::GeneralError => write!(f, "Some unspecified WASM error occurred"), } } } diff --git a/packages/fuel-indexer-macros/src/wasm.rs b/packages/fuel-indexer-macros/src/wasm.rs index 04494a2e6..416e9f305 100644 --- a/packages/fuel-indexer-macros/src/wasm.rs +++ b/packages/fuel-indexer-macros/src/wasm.rs @@ -6,12 +6,17 @@ pub fn handler_block_wasm( ) -> proc_macro2::TokenStream { let wasm_prelude = wasm_prelude(); + let panic_hook = panic_hook(); + quote! { #wasm_prelude + #panic_hook + #[no_mangle] fn handle_events(blob: *mut u8, len: usize) { + register_panic_hook(); use fuel_indexer_utils::plugin::deserialize; let bytes = unsafe { Vec::from_raw_parts(blob, len, len) }; let blocks: Vec = match deserialize(&bytes) { @@ -29,6 +34,43 @@ pub fn handler_block_wasm( } } +/// Panic hook for the indexer. +/// +/// When a panic occurs, the message is stored in a `static mut` `String` and a +/// `WasmIndexerError::Panic` error code is returned. The message is then +/// retrieved by the indexer service and logged. +fn panic_hook() -> proc_macro2::TokenStream { + quote! { + static mut PANIC_MESSAGE: String = String::new(); + + #[no_mangle] + fn get_panic_message_ptr() -> *const u8 { + unsafe { PANIC_MESSAGE.as_ptr() } + } + + #[no_mangle] + fn get_panic_message_len() -> u32 { + unsafe { PANIC_MESSAGE.len() as u32 } + } + + #[no_mangle] + fn register_panic_hook() { + use std::panic; + use std::sync::Once; + static SET_HOOK: Once = Once::new(); + + SET_HOOK.call_once(|| { + panic::set_hook(Box::new(|info| { + unsafe { + PANIC_MESSAGE = info.to_string(); + } + early_exit(WasmIndexerError::Panic); + })); + }); + } + } +} + /// Prelude imports for the _indexer_ module. /// /// These imports are placed below the top-level lib imports, so any diff --git a/packages/fuel-indexer/src/executor.rs b/packages/fuel-indexer/src/executor.rs index 898afc5b1..ca173554e 100644 --- a/packages/fuel-indexer/src/executor.rs +++ b/packages/fuel-indexer/src/executor.rs @@ -224,13 +224,21 @@ pub fn run_executor( if let Err(e) = result { if let IndexerError::RuntimeError(ref e) = e { - if let Some(&WasmIndexerError::MissingBlocksError) = - e.downcast_ref::() - { - return Err(anyhow::format_err!( - "Indexer({indexer_uid}) terminating due to missing blocks." - ) - .into()); + match e.downcast_ref::() { + Some(&WasmIndexerError::MissingBlocksError) => { + return Err(anyhow::format_err!( + "Indexer({indexer_uid}) terminating due to missing blocks." + ) + .into()); + } + Some(&WasmIndexerError::Panic) => { + let message = executor + .get_panic_message() + .await + .unwrap_or("unknown".to_string()); + return Err(anyhow::anyhow!("Indexer({indexer_uid}) terminating due to a panic:\n{message}").into()); + } + _ => (), } } // Run time metering is deterministic. There is no point in retrying. @@ -556,6 +564,8 @@ where fn manifest(&self) -> &Manifest; fn kill_switch(&self) -> &Arc; + + async fn get_panic_message(&self) -> IndexerResult; } /// WASM indexer runtime environment responsible for fetching/saving data to and from the database. @@ -689,6 +699,13 @@ where fn manifest(&self) -> &Manifest { &self.manifest } + + async fn get_panic_message(&self) -> IndexerResult { + return Err(anyhow::anyhow!( + "get_panic_message() not supported in native exetutor." + ) + .into()); + } } /// WASM executors are the primary means of execution. @@ -769,10 +786,11 @@ impl WasmIndexExecutor { // FunctionEnvMut and StoreMut must be scoped because they can't // be used across await { + let schema_version_from_wasm = ffi::get_version(&mut store, &instance)?; + let mut env_mut = env.clone().into_mut(&mut store); - let (data_mut, mut store_mut) = env_mut.data_and_store_mut(); - let schema_version_from_wasm = ffi::get_version(&mut store_mut, &instance)?; + let (data_mut, store_mut) = env_mut.data_and_store_mut(); if schema_version_from_wasm != schema_version { return Err(IndexerError::SchemaVersionMismatch(format!( @@ -973,4 +991,10 @@ impl Executor for WasmIndexExecutor { fn manifest(&self) -> &Manifest { &self.manifest } + + async fn get_panic_message(&self) -> IndexerResult { + let mut store = self.store.lock().await; + let result = ffi::get_panic_message(&mut store, &self.instance)?; + Ok(result) + } } diff --git a/packages/fuel-indexer/src/ffi.rs b/packages/fuel-indexer/src/ffi.rs index d0955a555..e950ff074 100644 --- a/packages/fuel-indexer/src/ffi.rs +++ b/packages/fuel-indexer/src/ffi.rs @@ -7,8 +7,8 @@ use fuel_indexer_types::ffi::{ use thiserror::Error; use tracing::{debug, error, info, trace, warn}; use wasmer::{ - ExportError, Exports, Function, FunctionEnvMut, Instance, MemoryView, RuntimeError, - Store, StoreMut, WasmPtr, + AsStoreMut, ExportError, Exports, Function, FunctionEnvMut, Instance, MemoryView, + RuntimeError, Store, WasmPtr, }; use wasmer_middlewares::metering::{ get_remaining_points, set_remaining_points, MeteringPoints, @@ -35,22 +35,45 @@ pub enum FFIError { None(String), } -/// Get the version of the indexer schema stored in the WASM instance. -pub fn get_version(store: &mut StoreMut, instance: &Instance) -> FFIResult { +/// Get a string stored in the WASM module. +fn get_string_from_instance( + store: &mut Store, + instance: &Instance, + ptr_fn_name: &str, + len_fn_name: &str, +) -> FFIResult { + let mut store = store.as_store_mut(); + let exports = &instance.exports; - let ptr = exports.get_function("get_version_ptr")?.call(store, &[])?[0] + let ptr = exports.get_function(ptr_fn_name)?.call(&mut store, &[])?[0] .i32() - .ok_or_else(|| FFIError::None("get_version".to_string()))? as u32; + .ok_or_else(|| FFIError::None(ptr_fn_name.to_string()))? as u32; - let len = exports.get_function("get_version_len")?.call(store, &[])?[0] + let len = exports.get_function(len_fn_name)?.call(&mut store, &[])?[0] .i32() - .ok_or_else(|| FFIError::None("get_version".to_string()))? as u32; + .ok_or_else(|| FFIError::None(len_fn_name.to_string()))? as u32; let memory = exports.get_memory("memory")?.view(store); - let version = get_string(&memory, ptr, len)?; - Ok(version) + let result = get_string(&memory, ptr, len)?; + + Ok(result) +} + +/// Get the version of the indexer schema stored in the WASM instance. +pub fn get_panic_message(store: &mut Store, instance: &Instance) -> FFIResult { + get_string_from_instance( + store, + instance, + "get_panic_message_ptr", + "get_panic_message_len", + ) +} + +/// Get the version of the indexer schema stored in the WASM instance. +pub fn get_version(store: &mut Store, instance: &Instance) -> FFIResult { + get_string_from_instance(store, instance, "get_version_ptr", "get_version_len") } /// Fetch the string at the given pointer from memory.