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