1use camel_api::CamelError;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum TrapReason {
20 Timeout,
22 OutOfMemory,
24 Unreachable,
26 StackOverflow,
28 Other(String),
30}
31
32impl std::fmt::Display for TrapReason {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Self::Timeout => write!(f, "execution timeout"),
36 Self::OutOfMemory => write!(f, "out of memory"),
37 Self::Unreachable => write!(f, "unreachable instruction"),
38 Self::StackOverflow => write!(f, "stack overflow"),
39 Self::Other(msg) => write!(f, "{msg}"),
40 }
41 }
42}
43
44#[derive(Debug, Clone, thiserror::Error)]
48pub enum WasmError {
49 #[error("WASM module not found: {0}")]
50 ModuleNotFound(String),
51
52 #[error("WASM compilation failed: {0}")]
53 CompilationFailed(String),
54
55 #[error("WASM instantiation failed: {0}")]
56 InstantiationFailed(String),
57
58 #[error("WASM guest panicked (trap): {0}")]
59 GuestPanic(String),
60
61 #[error("WASM invoke cancelled: {0}")]
62 Cancelled(String),
63
64 #[error("WASM type conversion failed: {0}")]
65 TypeConversion(String),
66
67 #[error("WASM I/O error: {0}")]
68 Io(String),
69
70 #[error("WASM configuration error: {0}")]
71 Config(String),
72
73 #[error("WASM plugin '{plugin}' timed out after {timeout_secs}s")]
75 Timeout { plugin: String, timeout_secs: u64 },
76
77 #[error("WASM plugin '{plugin}' trapped: {reason}")]
78 Trap { plugin: String, reason: TrapReason },
79
80 #[error("WASM plugin '{plugin}' exceeded memory limit ({max_memory_bytes} bytes)")]
81 OutOfMemory {
82 plugin: String,
83 max_memory_bytes: u64,
84 },
85
86 #[error("WASM plugin '{plugin}' is unhealthy: {detail}")]
87 Unhealthy { plugin: String, detail: String },
88}
89
90impl WasmError {
91 pub fn classify_trap(trap: &wasmtime::Trap) -> TrapReason {
99 match trap {
100 wasmtime::Trap::StackOverflow => TrapReason::StackOverflow,
101 wasmtime::Trap::MemoryOutOfBounds => TrapReason::OutOfMemory,
102 wasmtime::Trap::UnreachableCodeReached => TrapReason::Unreachable,
103 wasmtime::Trap::Interrupt => TrapReason::Timeout,
104 other => {
106 let msg = other.to_string();
107 if msg.contains("epoch") {
108 TrapReason::Timeout
109 } else {
110 TrapReason::Other(msg)
111 }
112 }
113 }
114 }
115
116 pub fn plugin_name(&self) -> Option<&str> {
118 match self {
119 Self::Timeout { plugin, .. } => Some(plugin),
120 Self::Trap { plugin, .. } => Some(plugin),
121 Self::OutOfMemory { plugin, .. } => Some(plugin),
122 Self::Unhealthy { plugin, .. } => Some(plugin),
123 _ => None,
124 }
125 }
126}
127
128impl From<WasmError> for CamelError {
129 fn from(err: WasmError) -> Self {
130 match &err {
131 WasmError::GuestPanic(msg) => CamelError::ProcessorError(format!("wasm trap: {msg}")),
132 WasmError::Cancelled(msg) => {
133 CamelError::ProcessorError(format!("wasm invoke cancelled: {msg}"))
134 }
135 WasmError::TypeConversion(msg) => CamelError::TypeConversionFailed(msg.clone()),
136 WasmError::ModuleNotFound(msg) => CamelError::ComponentNotFound(msg.clone()),
137 WasmError::CompilationFailed(msg) => {
138 CamelError::Config(format!("wasm compilation failed: {msg}"))
139 }
140 WasmError::InstantiationFailed(msg) => {
141 CamelError::Config(format!("wasm instantiation failed: {msg}"))
142 }
143 WasmError::Io(msg) => CamelError::Io(msg.clone()),
144 WasmError::Config(msg) => CamelError::Config(msg.clone()),
145 WasmError::Timeout {
147 plugin,
148 timeout_secs,
149 } => CamelError::ProcessorError(format!(
150 "WASM plugin '{}' timed out after {}s",
151 plugin, timeout_secs
152 )),
153 WasmError::Trap { plugin, reason } => {
154 CamelError::ProcessorError(format!("wasm trap: plugin '{}': {}", plugin, reason))
155 }
156 WasmError::OutOfMemory {
157 plugin,
158 max_memory_bytes,
159 } => CamelError::ProcessorError(format!(
160 "WASM plugin '{}' exceeded memory limit ({} bytes)",
161 plugin, max_memory_bytes
162 )),
163 WasmError::Unhealthy { plugin, detail } => CamelError::ProcessorError(format!(
164 "WASM plugin '{}' is unhealthy: {}",
165 plugin, detail
166 )),
167 }
168 }
169}
170
171impl From<crate::bindings::camel::plugin::types::WasmError> for CamelError {
172 fn from(err: crate::bindings::camel::plugin::types::WasmError) -> Self {
173 map_plugin_error(err).into()
174 }
175}
176
177impl From<crate::bean_bindings::camel::plugin::types::WasmError> for CamelError {
178 fn from(err: crate::bean_bindings::camel::plugin::types::WasmError) -> Self {
179 map_bean_error(err).into()
180 }
181}
182
183impl From<crate::source_bindings::camel::plugin::types::WasmError> for CamelError {
184 fn from(err: crate::source_bindings::camel::plugin::types::WasmError) -> Self {
185 map_source_error(err).into()
186 }
187}
188
189pub fn map_plugin_error(wasm_err: crate::bindings::camel::plugin::types::WasmError) -> WasmError {
197 use crate::bindings::camel::plugin::types::WasmError as PluginWasmError;
198 match wasm_err {
199 PluginWasmError::ProcessorError(s) => WasmError::GuestPanic(s),
200 PluginWasmError::TypeConversion(s) => WasmError::TypeConversion(s),
201 PluginWasmError::Io(s) => WasmError::Io(s),
202 PluginWasmError::Timeout => WasmError::GuestPanic("guest timeout".into()),
203 }
204}
205
206pub fn map_bean_error(
210 wasm_err: crate::bean_bindings::camel::plugin::types::WasmError,
211) -> WasmError {
212 use crate::bean_bindings::camel::plugin::types::WasmError as BeanWasmError;
213 match wasm_err {
214 BeanWasmError::ProcessorError(s) => WasmError::GuestPanic(s),
215 BeanWasmError::TypeConversion(s) => WasmError::TypeConversion(s),
216 BeanWasmError::Io(s) => WasmError::Io(s),
217 BeanWasmError::Timeout => WasmError::GuestPanic("guest timeout".into()),
218 }
219}
220
221pub fn map_source_error(
227 wasm_err: crate::source_bindings::camel::plugin::types::WasmError,
228) -> WasmError {
229 use crate::source_bindings::camel::plugin::types::WasmError as SourceWasmError;
230 match wasm_err {
231 SourceWasmError::ProcessorError(s) => WasmError::GuestPanic(s),
232 SourceWasmError::TypeConversion(s) => WasmError::TypeConversion(s),
233 SourceWasmError::Io(s) => WasmError::Io(s),
234 SourceWasmError::Timeout => WasmError::GuestPanic("guest timeout".into()),
235 }
236}
237
238impl From<wasmtime::Error> for WasmError {
244 fn from(err: wasmtime::Error) -> Self {
245 WasmError::GuestPanic(err.to_string())
246 }
247}
248
249pub fn peel_concurrent<T, Inner, E>(
278 result: Result<Result<T, Inner>, wasmtime::Error>,
279 map_outer: impl FnOnce(wasmtime::Error) -> E,
280 map_inner: impl FnOnce(Inner) -> E,
281) -> Result<T, E> {
282 match result {
283 Ok(Ok(v)) => Ok(v),
284 Ok(Err(inner)) => Err(map_inner(inner)),
285 Err(outer) => Err(map_outer(outer)),
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_trap_reason_display() {
295 assert_eq!(TrapReason::Timeout.to_string(), "execution timeout");
296 assert_eq!(TrapReason::OutOfMemory.to_string(), "out of memory");
297 assert_eq!(
298 TrapReason::Unreachable.to_string(),
299 "unreachable instruction"
300 );
301 assert_eq!(TrapReason::StackOverflow.to_string(), "stack overflow");
302 assert_eq!(
303 TrapReason::Other("custom".to_string()).to_string(),
304 "custom"
305 );
306 }
307
308 #[test]
309 fn test_classify_trap_stack_overflow() {
310 let trap = wasmtime::Trap::StackOverflow;
311 assert!(matches!(
312 WasmError::classify_trap(&trap),
313 TrapReason::StackOverflow
314 ));
315 }
316
317 #[test]
318 fn test_classify_trap_memory_out_of_bounds() {
319 let trap = wasmtime::Trap::MemoryOutOfBounds;
320 assert!(matches!(
321 WasmError::classify_trap(&trap),
322 TrapReason::OutOfMemory
323 ));
324 }
325
326 #[test]
327 fn test_classify_trap_unreachable() {
328 let trap = wasmtime::Trap::UnreachableCodeReached;
329 assert!(matches!(
330 WasmError::classify_trap(&trap),
331 TrapReason::Unreachable
332 ));
333 }
334
335 #[test]
336 fn test_wasm_error_timeout_display() {
337 let err = WasmError::Timeout {
338 plugin: "my_plugin".to_string(),
339 timeout_secs: 30,
340 };
341 let msg = err.to_string();
342 assert!(msg.contains("my_plugin"));
343 assert!(msg.contains("30"));
344 assert!(msg.contains("timed out"));
345 }
346
347 #[test]
348 fn test_wasm_error_trap_display() {
349 let err = WasmError::Trap {
350 plugin: "my_plugin".to_string(),
351 reason: TrapReason::StackOverflow,
352 };
353 let msg = err.to_string();
354 assert!(msg.contains("my_plugin"));
355 assert!(msg.contains("stack overflow"));
356 }
357
358 #[test]
359 fn test_wasm_error_out_of_memory_display() {
360 let err = WasmError::OutOfMemory {
361 plugin: "my_plugin".to_string(),
362 max_memory_bytes: 52428800,
363 };
364 let msg = err.to_string();
365 assert!(msg.contains("my_plugin"));
366 assert!(msg.contains("52428800"));
367 }
368
369 #[test]
370 fn test_wasm_error_unhealthy_display() {
371 let err = WasmError::Unhealthy {
372 plugin: "my_plugin".to_string(),
373 detail: "consecutive failures".to_string(),
374 };
375 let msg = err.to_string();
376 assert!(msg.contains("my_plugin"));
377 assert!(msg.contains("consecutive failures"));
378 }
379
380 #[test]
381 fn test_wasm_error_to_camel_error_timeout() {
382 let err = WasmError::Timeout {
383 plugin: "p".to_string(),
384 timeout_secs: 10,
385 };
386 let camel: CamelError = err.into();
387 let msg = camel.to_string();
388 assert!(msg.contains("timed out"));
389 assert!(msg.contains("10"));
390 }
391
392 #[test]
393 fn test_wasm_error_to_camel_error_trap() {
394 let err = WasmError::Trap {
395 plugin: "p".to_string(),
396 reason: TrapReason::Unreachable,
397 };
398 let camel: CamelError = err.into();
399 let msg = camel.to_string();
400 assert!(msg.contains("unreachable"));
401 }
402
403 #[test]
404 fn test_wasm_error_to_camel_error_out_of_memory() {
405 let err = WasmError::OutOfMemory {
406 plugin: "p".to_string(),
407 max_memory_bytes: 1024,
408 };
409 let camel: CamelError = err.into();
410 let msg = camel.to_string();
411 assert!(msg.contains("memory"));
412 }
413
414 #[test]
415 fn test_wasm_error_to_camel_error_unhealthy() {
416 let err = WasmError::Unhealthy {
417 plugin: "p".to_string(),
418 detail: "broken".to_string(),
419 };
420 let camel: CamelError = err.into();
421 assert!(matches!(camel, CamelError::ProcessorError(_)));
422 }
423
424 #[test]
425 fn test_guest_panic_still_maps_to_processor_error() {
426 let err = WasmError::GuestPanic("boom".to_string());
427 let camel: CamelError = err.into();
428 assert!(matches!(camel, CamelError::ProcessorError(_)));
429 assert!(camel.to_string().contains("wasm trap"));
430 }
431
432 #[test]
433 fn test_instantiation_maps_to_config_error() {
434 let err = WasmError::InstantiationFailed("bad import".to_string());
435 let camel: CamelError = err.into();
436 assert!(matches!(camel, CamelError::Config(_)));
437 assert!(camel.to_string().contains("instantiation failed"));
438 }
439
440 #[test]
441 fn test_plugin_name() {
442 let err = WasmError::Timeout {
443 plugin: "test".to_string(),
444 timeout_secs: 5,
445 };
446 assert_eq!(err.plugin_name(), Some("test"));
447
448 let err = WasmError::GuestPanic("msg".to_string());
449 assert_eq!(err.plugin_name(), None);
450 }
451
452 #[test]
455 fn peel_concurrent_ok_outer_ok_inner_returns_t() {
456 let r: Result<Result<i32, String>, wasmtime::Error> = Ok(Ok(42));
457 let result = peel_concurrent(r, |_| 0, |_| -1);
458 assert_eq!(result, Ok(42));
459 }
460
461 #[test]
462 fn peel_concurrent_ok_outer_err_inner_runs_map_inner() {
463 let r: Result<Result<i32, String>, wasmtime::Error> = Ok(Err("inner".to_string()));
464 let result = peel_concurrent(r, |_| "outer".to_string(), |s| s.to_uppercase());
465 assert_eq!(result, Err("INNER".to_string()));
466 }
467
468 #[test]
469 fn peel_concurrent_err_outer_runs_map_outer() {
470 let wt_err = wasmtime::Error::msg("outer trap");
472 let r: Result<Result<i32, String>, wasmtime::Error> = Err(wt_err);
473 let result = peel_concurrent(r, |_| "outer-mapped".to_string(), |_| "inner".to_string());
474 assert_eq!(result, Err("outer-mapped".to_string()));
475 }
476}