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, 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 type conversion failed: {0}")]
62 TypeConversion(String),
63
64 #[error("WASM I/O error: {0}")]
65 Io(String),
66
67 #[error("WASM configuration error: {0}")]
68 Config(String),
69
70 #[error("WASM plugin '{plugin}' timed out after {timeout_secs}s")]
72 Timeout { plugin: String, timeout_secs: u64 },
73
74 #[error("WASM plugin '{plugin}' trapped: {reason}")]
75 Trap { plugin: String, reason: TrapReason },
76
77 #[error("WASM plugin '{plugin}' exceeded memory limit ({max_memory_bytes} bytes)")]
78 OutOfMemory {
79 plugin: String,
80 max_memory_bytes: u64,
81 },
82
83 #[error("WASM plugin '{plugin}' is unhealthy: {detail}")]
84 Unhealthy { plugin: String, detail: String },
85}
86
87impl WasmError {
88 pub fn classify_trap(trap: &wasmtime::Trap) -> TrapReason {
96 match trap {
97 wasmtime::Trap::StackOverflow => TrapReason::StackOverflow,
98 wasmtime::Trap::MemoryOutOfBounds => TrapReason::OutOfMemory,
99 wasmtime::Trap::UnreachableCodeReached => TrapReason::Unreachable,
100 wasmtime::Trap::Interrupt => TrapReason::Timeout,
101 other => {
103 let msg = other.to_string();
104 if msg.contains("epoch") {
105 TrapReason::Timeout
106 } else {
107 TrapReason::Other(msg)
108 }
109 }
110 }
111 }
112
113 pub fn plugin_name(&self) -> Option<&str> {
115 match self {
116 Self::Timeout { plugin, .. } => Some(plugin),
117 Self::Trap { plugin, .. } => Some(plugin),
118 Self::OutOfMemory { plugin, .. } => Some(plugin),
119 Self::Unhealthy { plugin, .. } => Some(plugin),
120 _ => None,
121 }
122 }
123}
124
125impl From<WasmError> for CamelError {
126 fn from(err: WasmError) -> Self {
127 match &err {
128 WasmError::GuestPanic(msg) => CamelError::ProcessorError(format!("wasm trap: {msg}")),
129 WasmError::TypeConversion(msg) => CamelError::TypeConversionFailed(msg.clone()),
130 WasmError::ModuleNotFound(msg) => CamelError::ComponentNotFound(msg.clone()),
131 WasmError::CompilationFailed(msg) => {
132 CamelError::Config(format!("wasm compilation failed: {msg}"))
133 }
134 WasmError::InstantiationFailed(msg) => {
135 CamelError::Config(format!("wasm instantiation failed: {msg}"))
136 }
137 WasmError::Io(msg) => CamelError::Io(msg.clone()),
138 WasmError::Config(msg) => CamelError::Config(msg.clone()),
139 WasmError::Timeout {
141 plugin,
142 timeout_secs,
143 } => CamelError::ProcessorError(format!(
144 "WASM plugin '{}' timed out after {}s",
145 plugin, timeout_secs
146 )),
147 WasmError::Trap { plugin, reason } => {
148 CamelError::ProcessorError(format!("wasm trap: plugin '{}': {}", plugin, reason))
149 }
150 WasmError::OutOfMemory {
151 plugin,
152 max_memory_bytes,
153 } => CamelError::ProcessorError(format!(
154 "WASM plugin '{}' exceeded memory limit ({} bytes)",
155 plugin, max_memory_bytes
156 )),
157 WasmError::Unhealthy { plugin, detail } => CamelError::ProcessorError(format!(
158 "WASM plugin '{}' is unhealthy: {}",
159 plugin, detail
160 )),
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_trap_reason_display() {
171 assert_eq!(TrapReason::Timeout.to_string(), "execution timeout");
172 assert_eq!(TrapReason::OutOfMemory.to_string(), "out of memory");
173 assert_eq!(
174 TrapReason::Unreachable.to_string(),
175 "unreachable instruction"
176 );
177 assert_eq!(TrapReason::StackOverflow.to_string(), "stack overflow");
178 assert_eq!(
179 TrapReason::Other("custom".to_string()).to_string(),
180 "custom"
181 );
182 }
183
184 #[test]
185 fn test_classify_trap_stack_overflow() {
186 let trap = wasmtime::Trap::StackOverflow;
187 assert!(matches!(
188 WasmError::classify_trap(&trap),
189 TrapReason::StackOverflow
190 ));
191 }
192
193 #[test]
194 fn test_classify_trap_memory_out_of_bounds() {
195 let trap = wasmtime::Trap::MemoryOutOfBounds;
196 assert!(matches!(
197 WasmError::classify_trap(&trap),
198 TrapReason::OutOfMemory
199 ));
200 }
201
202 #[test]
203 fn test_classify_trap_unreachable() {
204 let trap = wasmtime::Trap::UnreachableCodeReached;
205 assert!(matches!(
206 WasmError::classify_trap(&trap),
207 TrapReason::Unreachable
208 ));
209 }
210
211 #[test]
212 fn test_wasm_error_timeout_display() {
213 let err = WasmError::Timeout {
214 plugin: "my_plugin".to_string(),
215 timeout_secs: 30,
216 };
217 let msg = err.to_string();
218 assert!(msg.contains("my_plugin"));
219 assert!(msg.contains("30"));
220 assert!(msg.contains("timed out"));
221 }
222
223 #[test]
224 fn test_wasm_error_trap_display() {
225 let err = WasmError::Trap {
226 plugin: "my_plugin".to_string(),
227 reason: TrapReason::StackOverflow,
228 };
229 let msg = err.to_string();
230 assert!(msg.contains("my_plugin"));
231 assert!(msg.contains("stack overflow"));
232 }
233
234 #[test]
235 fn test_wasm_error_out_of_memory_display() {
236 let err = WasmError::OutOfMemory {
237 plugin: "my_plugin".to_string(),
238 max_memory_bytes: 52428800,
239 };
240 let msg = err.to_string();
241 assert!(msg.contains("my_plugin"));
242 assert!(msg.contains("52428800"));
243 }
244
245 #[test]
246 fn test_wasm_error_unhealthy_display() {
247 let err = WasmError::Unhealthy {
248 plugin: "my_plugin".to_string(),
249 detail: "consecutive failures".to_string(),
250 };
251 let msg = err.to_string();
252 assert!(msg.contains("my_plugin"));
253 assert!(msg.contains("consecutive failures"));
254 }
255
256 #[test]
257 fn test_wasm_error_to_camel_error_timeout() {
258 let err = WasmError::Timeout {
259 plugin: "p".to_string(),
260 timeout_secs: 10,
261 };
262 let camel: CamelError = err.into();
263 let msg = camel.to_string();
264 assert!(msg.contains("timed out"));
265 assert!(msg.contains("10"));
266 }
267
268 #[test]
269 fn test_wasm_error_to_camel_error_trap() {
270 let err = WasmError::Trap {
271 plugin: "p".to_string(),
272 reason: TrapReason::Unreachable,
273 };
274 let camel: CamelError = err.into();
275 let msg = camel.to_string();
276 assert!(msg.contains("unreachable"));
277 }
278
279 #[test]
280 fn test_wasm_error_to_camel_error_out_of_memory() {
281 let err = WasmError::OutOfMemory {
282 plugin: "p".to_string(),
283 max_memory_bytes: 1024,
284 };
285 let camel: CamelError = err.into();
286 let msg = camel.to_string();
287 assert!(msg.contains("memory"));
288 }
289
290 #[test]
291 fn test_wasm_error_to_camel_error_unhealthy() {
292 let err = WasmError::Unhealthy {
293 plugin: "p".to_string(),
294 detail: "broken".to_string(),
295 };
296 let camel: CamelError = err.into();
297 assert!(matches!(camel, CamelError::ProcessorError(_)));
298 }
299
300 #[test]
301 fn test_guest_panic_still_maps_to_processor_error() {
302 let err = WasmError::GuestPanic("boom".to_string());
303 let camel: CamelError = err.into();
304 assert!(matches!(camel, CamelError::ProcessorError(_)));
305 assert!(camel.to_string().contains("wasm trap"));
306 }
307
308 #[test]
309 fn test_instantiation_maps_to_config_error() {
310 let err = WasmError::InstantiationFailed("bad import".to_string());
311 let camel: CamelError = err.into();
312 assert!(matches!(camel, CamelError::Config(_)));
313 assert!(camel.to_string().contains("instantiation failed"));
314 }
315
316 #[test]
317 fn test_plugin_name() {
318 let err = WasmError::Timeout {
319 plugin: "test".to_string(),
320 timeout_secs: 5,
321 };
322 assert_eq!(err.plugin_name(), Some("test"));
323
324 let err = WasmError::GuestPanic("msg".to_string());
325 assert_eq!(err.plugin_name(), None);
326 }
327}