1use crate::error::ScriptError;
20use ff_core::engine_error::{
21 BugKind, ConflictKind, ContentionKind, EngineError, StateKind, ValidationKind,
22};
23use ff_core::error::ErrorClass;
24
25pub fn transport_script(err: ScriptError) -> EngineError {
29 EngineError::Transport {
30 backend: "valkey",
31 source: Box::new(err),
32 }
33}
34
35pub fn transport_script_ref(err: &EngineError) -> Option<&ScriptError> {
40 match err {
41 EngineError::Transport { source, .. } => source.downcast_ref::<ScriptError>(),
42 _ => None,
43 }
44}
45
46pub fn valkey_kind(err: &EngineError) -> Option<ferriskey::ErrorKind> {
52 match err {
53 EngineError::Transport { source, .. } => source
54 .downcast_ref::<ScriptError>()
55 .and_then(|s| s.valkey_kind()),
56 _ => None,
57 }
58}
59
60pub fn class(err: &EngineError) -> ErrorClass {
68 match err {
69 EngineError::Transport { source, .. } => source
70 .downcast_ref::<ScriptError>()
71 .map(|s| s.class())
72 .unwrap_or(ErrorClass::Terminal),
73 other => other.class(),
74 }
75}
76
77impl From<ScriptError> for EngineError {
78 fn from(err: ScriptError) -> Self {
79 use ScriptError as S;
80 match err {
81 S::ExecutionNotFound => Self::NotFound {
83 entity: "execution",
84 },
85 S::FlowNotFound => Self::NotFound { entity: "flow" },
86 S::AttemptNotFound => Self::NotFound { entity: "attempt" },
87 S::BudgetNotFound => Self::NotFound { entity: "budget" },
88 S::QuotaPolicyNotFound => Self::NotFound {
89 entity: "quota_policy",
90 },
91 S::StreamNotFound => Self::NotFound { entity: "stream" },
92
93 S::InvalidInput(d) => Self::Validation {
95 kind: ValidationKind::InvalidInput,
96 detail: d,
97 },
98 S::CapabilityMismatch(d) => Self::Validation {
99 kind: ValidationKind::CapabilityMismatch,
100 detail: d,
101 },
102 S::InvalidCapabilities(d) => Self::Validation {
103 kind: ValidationKind::InvalidCapabilities,
104 detail: d,
105 },
106 S::InvalidPolicyJson(d) => Self::Validation {
107 kind: ValidationKind::InvalidPolicyJson,
108 detail: d,
109 },
110 S::InvalidTagKey(d) => Self::Validation {
111 kind: ValidationKind::InvalidTagKey,
112 detail: d,
113 },
114 S::PayloadTooLarge => Self::Validation {
116 kind: ValidationKind::PayloadTooLarge,
117 detail: String::new(),
118 },
119 S::SignalLimitExceeded => Self::Validation {
120 kind: ValidationKind::SignalLimitExceeded,
121 detail: String::new(),
122 },
123 S::InvalidWaitpointKey => Self::Validation {
124 kind: ValidationKind::InvalidWaitpointKey,
125 detail: String::new(),
126 },
127 S::WaitpointNotTokenBound => Self::Validation {
128 kind: ValidationKind::WaitpointNotTokenBound,
129 detail: String::new(),
130 },
131 S::RetentionLimitExceeded => Self::Validation {
132 kind: ValidationKind::RetentionLimitExceeded,
133 detail: String::new(),
134 },
135 S::InvalidLeaseForSuspend => Self::Validation {
136 kind: ValidationKind::InvalidLeaseForSuspend,
137 detail: String::new(),
138 },
139 S::InvalidDependency => Self::Validation {
140 kind: ValidationKind::InvalidDependency,
141 detail: String::new(),
142 },
143 S::InvalidWaitpointForExecution => Self::Validation {
144 kind: ValidationKind::InvalidWaitpointForExecution,
145 detail: String::new(),
146 },
147 S::InvalidBlockingReason => Self::Validation {
148 kind: ValidationKind::InvalidBlockingReason,
149 detail: String::new(),
150 },
151 S::InvalidOffset => Self::Validation {
152 kind: ValidationKind::InvalidOffset,
153 detail: String::new(),
154 },
155 S::Unauthorized => Self::Validation {
156 kind: ValidationKind::Unauthorized,
157 detail: String::new(),
158 },
159 S::InvalidBudgetScope => Self::Validation {
160 kind: ValidationKind::InvalidBudgetScope,
161 detail: String::new(),
162 },
163 S::BudgetOverrideNotAllowed => Self::Validation {
164 kind: ValidationKind::BudgetOverrideNotAllowed,
165 detail: String::new(),
166 },
167 S::InvalidQuotaSpec => Self::Validation {
168 kind: ValidationKind::InvalidQuotaSpec,
169 detail: String::new(),
170 },
171 S::InvalidKid => Self::Validation {
172 kind: ValidationKind::InvalidKid,
173 detail: String::new(),
174 },
175 S::InvalidSecretHex => Self::Validation {
176 kind: ValidationKind::InvalidSecretHex,
177 detail: String::new(),
178 },
179 S::InvalidGraceMs => Self::Validation {
180 kind: ValidationKind::InvalidGraceMs,
181 detail: String::new(),
182 },
183 S::InvalidFrameType => Self::Validation {
184 kind: ValidationKind::InvalidFrameType,
185 detail: String::new(),
186 },
187
188 S::UseClaimResumedExecution => {
190 Self::Contention(ContentionKind::UseClaimResumedExecution)
191 }
192 S::NotAResumedExecution => Self::Contention(ContentionKind::NotAResumedExecution),
193 S::ExecutionNotLeaseable => Self::Contention(ContentionKind::ExecutionNotLeaseable),
194 S::LeaseConflict => Self::Contention(ContentionKind::LeaseConflict),
195 S::InvalidClaimGrant => Self::Contention(ContentionKind::InvalidClaimGrant),
196 S::ClaimGrantExpired => Self::Contention(ContentionKind::ClaimGrantExpired),
197 S::NoEligibleExecution => Self::Contention(ContentionKind::NoEligibleExecution),
198 S::WaitpointNotFound => Self::Contention(ContentionKind::WaitpointNotFound),
199 S::WaitpointPendingUseBufferScript => {
200 Self::Contention(ContentionKind::WaitpointPendingUseBufferScript)
201 }
202 S::StaleGraphRevision => Self::Contention(ContentionKind::StaleGraphRevision),
203 S::ExecutionNotActive {
204 terminal_outcome,
205 lease_epoch,
206 lifecycle_phase,
207 attempt_id,
208 } => Self::Contention(ContentionKind::ExecutionNotActive {
209 terminal_outcome,
210 lease_epoch,
211 lifecycle_phase,
212 attempt_id,
213 }),
214 S::ExecutionNotEligible => Self::Contention(ContentionKind::ExecutionNotEligible),
215 S::ExecutionNotInEligibleSet => {
216 Self::Contention(ContentionKind::ExecutionNotInEligibleSet)
217 }
218 S::ExecutionNotReclaimable => {
219 Self::Contention(ContentionKind::ExecutionNotReclaimable)
220 }
221 S::NoActiveLease => Self::Contention(ContentionKind::NoActiveLease),
222 S::RateLimitExceeded => Self::Contention(ContentionKind::RateLimitExceeded),
223 S::ConcurrencyLimitExceeded => {
224 Self::Contention(ContentionKind::ConcurrencyLimitExceeded)
225 }
226
227 S::DependencyAlreadyExists => transport_script(S::DependencyAlreadyExists),
235 S::CycleDetected => Self::Conflict(ConflictKind::CycleDetected),
236 S::SelfReferencingEdge => Self::Conflict(ConflictKind::SelfReferencingEdge),
237 S::ExecutionAlreadyInFlow => Self::Conflict(ConflictKind::ExecutionAlreadyInFlow),
238 S::WaitpointAlreadyExists => Self::Conflict(ConflictKind::WaitpointAlreadyExists),
239 S::BudgetAttachConflict => Self::Conflict(ConflictKind::BudgetAttachConflict),
240 S::QuotaAttachConflict => Self::Conflict(ConflictKind::QuotaAttachConflict),
241 S::RotationConflict(kid) => Self::Conflict(ConflictKind::RotationConflict(kid)),
242 S::ActiveAttemptExists => Self::Conflict(ConflictKind::ActiveAttemptExists),
243
244 S::StaleLease => Self::State(StateKind::StaleLease),
246 S::LeaseExpired => Self::State(StateKind::LeaseExpired),
247 S::LeaseRevoked => Self::State(StateKind::LeaseRevoked),
248 S::ExecutionNotSuspended => Self::State(StateKind::ExecutionNotSuspended),
249 S::AlreadySuspended => Self::State(StateKind::AlreadySuspended),
250 S::WaitpointClosed => Self::State(StateKind::WaitpointClosed),
251 S::TargetNotSignalable => Self::State(StateKind::TargetNotSignalable),
252 S::DuplicateSignal => Self::State(StateKind::DuplicateSignal),
253 S::ResumeConditionNotMet => Self::State(StateKind::ResumeConditionNotMet),
254 S::WaitpointNotPending => Self::State(StateKind::WaitpointNotPending),
255 S::PendingWaitpointExpired => Self::State(StateKind::PendingWaitpointExpired),
256 S::WaitpointNotOpen => Self::State(StateKind::WaitpointNotOpen),
257 S::ExecutionNotTerminal => Self::State(StateKind::ExecutionNotTerminal),
258 S::MaxReplaysExhausted => Self::State(StateKind::MaxReplaysExhausted),
259 S::StreamClosed => Self::State(StateKind::StreamClosed),
260 S::StaleOwnerCannotAppend => Self::State(StateKind::StaleOwnerCannotAppend),
261 S::GrantAlreadyExists => Self::State(StateKind::GrantAlreadyExists),
262 S::ExecutionNotInFlow => Self::State(StateKind::ExecutionNotInFlow),
263 S::FlowAlreadyTerminal => Self::State(StateKind::FlowAlreadyTerminal),
264 S::DepsNotSatisfied => Self::State(StateKind::DepsNotSatisfied),
265 S::NotBlockedByDeps => Self::State(StateKind::NotBlockedByDeps),
266 S::NotRunnable => Self::State(StateKind::NotRunnable),
267 S::Terminal => Self::State(StateKind::Terminal),
268 S::BudgetExceeded => Self::State(StateKind::BudgetExceeded),
269 S::BudgetSoftExceeded => Self::State(StateKind::BudgetSoftExceeded),
270 S::OkAlreadyApplied => Self::State(StateKind::OkAlreadyApplied),
271 S::AttemptNotStarted => Self::State(StateKind::AttemptNotStarted),
272 S::AttemptAlreadyTerminal => Self::State(StateKind::AttemptAlreadyTerminal),
273 S::ExecutionNotEligibleForAttempt => {
274 Self::State(StateKind::ExecutionNotEligibleForAttempt)
275 }
276 S::ReplayNotAllowed => Self::State(StateKind::ReplayNotAllowed),
277 S::MaxRetriesExhausted => Self::State(StateKind::MaxRetriesExhausted),
278 S::StreamAlreadyClosed => Self::State(StateKind::StreamAlreadyClosed),
279
280 S::AttemptNotInCreatedState => Self::Bug(BugKind::AttemptNotInCreatedState),
282
283 e @ (S::Parse { .. } | S::Valkey(_)) => transport_script(e),
285
286 other => transport_script(other),
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn not_found_mappings() {
304 assert!(matches!(
305 EngineError::from(ScriptError::ExecutionNotFound),
306 EngineError::NotFound { entity: "execution" }
307 ));
308 assert!(matches!(
309 EngineError::from(ScriptError::FlowNotFound),
310 EngineError::NotFound { entity: "flow" }
311 ));
312 }
313
314 #[test]
315 fn validation_detail_preserved() {
316 match EngineError::from(ScriptError::CapabilityMismatch("gpu,cuda".into())) {
317 EngineError::Validation {
318 kind: ValidationKind::CapabilityMismatch,
319 detail,
320 } => assert_eq!(detail, "gpu,cuda"),
321 other => panic!("{other:?}"),
322 }
323 }
324
325 #[test]
326 fn contention_bucket() {
327 assert!(matches!(
328 EngineError::from(ScriptError::LeaseConflict),
329 EngineError::Contention(ContentionKind::LeaseConflict)
330 ));
331 assert!(matches!(
332 EngineError::from(ScriptError::UseClaimResumedExecution),
333 EngineError::Contention(ContentionKind::UseClaimResumedExecution)
334 ));
335 }
336
337 #[test]
338 fn execution_not_active_detail_flows_through() {
339 let src = ScriptError::ExecutionNotActive {
340 terminal_outcome: "success".into(),
341 lease_epoch: "3".into(),
342 lifecycle_phase: "terminal".into(),
343 attempt_id: "att-1".into(),
344 };
345 match EngineError::from(src) {
346 EngineError::Contention(ContentionKind::ExecutionNotActive {
347 terminal_outcome,
348 lease_epoch,
349 lifecycle_phase,
350 attempt_id,
351 }) => {
352 assert_eq!(terminal_outcome, "success");
353 assert_eq!(lease_epoch, "3");
354 assert_eq!(lifecycle_phase, "terminal");
355 assert_eq!(attempt_id, "att-1");
356 }
357 other => panic!("{other:?}"),
358 }
359 }
360
361 #[test]
362 fn dependency_already_exists_falls_through_to_transport_without_enrich() {
363 let err = EngineError::from(ScriptError::DependencyAlreadyExists);
364 match &err {
365 EngineError::Transport { backend, source } => {
366 assert_eq!(*backend, "valkey");
367 assert!(matches!(
368 source.downcast_ref::<ScriptError>(),
369 Some(ScriptError::DependencyAlreadyExists)
370 ));
371 }
372 other => panic!("{other:?}"),
373 }
374 }
375
376 #[test]
377 fn conflict_variants() {
378 assert!(matches!(
379 EngineError::from(ScriptError::CycleDetected),
380 EngineError::Conflict(ConflictKind::CycleDetected)
381 ));
382 assert!(matches!(
383 EngineError::from(ScriptError::ExecutionAlreadyInFlow),
384 EngineError::Conflict(ConflictKind::ExecutionAlreadyInFlow)
385 ));
386 match EngineError::from(ScriptError::RotationConflict("kid-1".into())) {
387 EngineError::Conflict(ConflictKind::RotationConflict(k)) => assert_eq!(k, "kid-1"),
388 other => panic!("{other:?}"),
389 }
390 }
391
392 #[test]
393 fn state_variants() {
394 assert!(matches!(
395 EngineError::from(ScriptError::StaleLease),
396 EngineError::State(StateKind::StaleLease)
397 ));
398 assert!(matches!(
399 EngineError::from(ScriptError::BudgetExceeded),
400 EngineError::State(StateKind::BudgetExceeded)
401 ));
402 }
403
404 #[test]
405 fn bug_variants() {
406 assert!(matches!(
407 EngineError::from(ScriptError::AttemptNotInCreatedState),
408 EngineError::Bug(BugKind::AttemptNotInCreatedState)
409 ));
410 }
411
412 #[test]
413 fn transport_preserves_parse() {
414 let err = EngineError::from(ScriptError::Parse {
415 fcall: "test_fn".into(),
416 execution_id: None,
417 message: "bad envelope".into(),
418 });
419 match &err {
420 EngineError::Transport { backend, source } => {
421 assert_eq!(*backend, "valkey");
422 assert!(matches!(
423 source.downcast_ref::<ScriptError>(),
424 Some(ScriptError::Parse { .. })
425 ));
426 }
427 other => panic!("{other:?}"),
428 }
429 }
430
431 #[test]
432 fn transport_script_helper_round_trips() {
433 let err = transport_script(ScriptError::AttemptNotFound);
434 assert!(matches!(
435 transport_script_ref(&err),
436 Some(ScriptError::AttemptNotFound)
437 ));
438 assert_eq!(class(&err), ScriptError::AttemptNotFound.class());
439 }
440
441 #[test]
442 fn transport_preserves_valkey_kind() {
443 let src = ScriptError::Valkey(ferriskey::Error::from((
444 ferriskey::ErrorKind::IoError,
445 "boom",
446 )));
447 let err = EngineError::from(src);
448 assert_eq!(valkey_kind(&err), Some(ferriskey::ErrorKind::IoError));
449 }
450
451 #[test]
452 fn class_transport_delegates() {
453 let err = EngineError::from(ScriptError::Valkey(ferriskey::Error::from((
454 ferriskey::ErrorKind::IoError,
455 "x",
456 ))));
457 assert_eq!(class(&err), ErrorClass::Retryable);
458 }
459
460 #[test]
461 fn class_transport_with_non_script_source_terminal() {
462 let raw = std::io::Error::other("simulated postgres net error");
463 let err = EngineError::Transport {
464 backend: "postgres",
465 source: Box::new(raw),
466 };
467 assert_eq!(class(&err), ErrorClass::Terminal);
468 assert!(valkey_kind(&err).is_none());
469 assert!(transport_script_ref(&err).is_none());
470 }
471
472 #[test]
473 fn unavailable_has_no_script_source() {
474 let err = EngineError::Unavailable { op: "claim" };
475 assert_eq!(class(&err), ErrorClass::Terminal);
476 assert!(valkey_kind(&err).is_none());
477 assert!(transport_script_ref(&err).is_none());
478 }
479}