diff --git a/crates/core/src/adapter_error.rs b/crates/core/src/adapter_error.rs index 8cebbe5..b21ce90 100644 --- a/crates/core/src/adapter_error.rs +++ b/crates/core/src/adapter_error.rs @@ -56,6 +56,16 @@ impl AdapterError { self.retryability == crate::retryability::Retryability::Retryable } + pub(crate) fn is_retryable_resolution_failure(&self) -> bool { + matches!( + self.code, + ErrorCode::StaleRef + | ErrorCode::AmbiguousTarget + | ErrorCode::Timeout + | ErrorCode::AppUnresponsive + ) && self.is_explicitly_retryable() + } + pub fn permits_retry_by_default(&self) -> bool { self.retryability != crate::retryability::Retryability::NonRetryable } @@ -221,4 +231,40 @@ mod tests { assert!(!error.is_explicitly_retryable()); assert!(!error.permits_retry_by_default()); } + + #[test] + fn retryable_resolution_failure_characterizes_every_error_code() { + let all = [ + ErrorCode::PermDenied, + ErrorCode::ElementNotFound, + ErrorCode::AppNotFound, + ErrorCode::ActionFailed, + ErrorCode::ActionNotSupported, + ErrorCode::StaleRef, + ErrorCode::AmbiguousTarget, + ErrorCode::WindowNotFound, + ErrorCode::PlatformNotSupported, + ErrorCode::Timeout, + ErrorCode::InvalidArgs, + ErrorCode::NotificationNotFound, + ErrorCode::SnapshotNotFound, + ErrorCode::PolicyDenied, + ErrorCode::AppUnresponsive, + ErrorCode::Internal, + ]; + let retryable = [ + ErrorCode::StaleRef, + ErrorCode::AmbiguousTarget, + ErrorCode::Timeout, + ErrorCode::AppUnresponsive, + ]; + + for code in all { + let expected = retryable.contains(&code); + let error = AdapterError::new(code.clone(), "failure") + .with_details(serde_json::json!({ "retryable": true })); + assert_eq!(error.is_retryable_resolution_failure(), expected); + assert!(!AdapterError::new(code, "failure").is_retryable_resolution_failure()); + } + } } diff --git a/crates/core/src/commands/pointer_action.rs b/crates/core/src/commands/pointer_action.rs index ee18f71..4785e30 100644 --- a/crates/core/src/commands/pointer_action.rs +++ b/crates/core/src/commands/pointer_action.rs @@ -223,24 +223,17 @@ fn point_observed_bounds_hash(err: &AppError) -> Option { } fn is_retryable_point_error(err: &AppError) -> bool { - match err.code() { - "STALE_REF" | "AMBIGUOUS_TARGET" | "TIMEOUT" | "APP_UNRESPONSIVE" => { - error_is_explicitly_retryable(err) + match err { + AppError::Adapter(error) if error.is_retryable_resolution_failure() => true, + AppError::Adapter(error) if error.code == crate::ErrorCode::ActionFailed => { + ["visible", "stable", "receives_events"] + .into_iter() + .any(|check| point_failed_check(err, check)) } - "ACTION_FAILED" => ["visible", "stable", "receives_events"] - .into_iter() - .any(|check| point_failed_check(err, check)), _ => false, } } -fn error_is_explicitly_retryable(err: &AppError) -> bool { - let AppError::Adapter(error) = err else { - return false; - }; - error.is_explicitly_retryable() -} - fn point_failed_check(err: &AppError, expected: &str) -> bool { let AppError::Adapter(error) = err else { return false; diff --git a/crates/core/src/ref_action_poll.rs b/crates/core/src/ref_action_poll.rs index ea0d7f6..3be977c 100644 --- a/crates/core/src/ref_action_poll.rs +++ b/crates/core/src/ref_action_poll.rs @@ -82,7 +82,7 @@ fn handle_resolve_failure( error: AdapterError, deadline: Deadline, ) -> Result<(), AdapterError> { - if is_permanent_error(&error.code) || !is_retryable_resolve_error(&error) { + if !error.is_retryable_resolution_failure() { trace_resolve_error(context.context, context.ref_id, &error); return Err(error); } @@ -125,16 +125,6 @@ fn is_permanent_actionability_error(code: &ErrorCode) -> bool { is_permanent_error(code) || matches!(code, ErrorCode::StaleRef | ErrorCode::AmbiguousTarget) } -fn is_retryable_resolve_error(error: &AdapterError) -> bool { - matches!( - error.code, - ErrorCode::StaleRef - | ErrorCode::AmbiguousTarget - | ErrorCode::Timeout - | ErrorCode::AppUnresponsive - ) && error.is_explicitly_retryable() -} - fn timeout(state: &RefActionPollState, deadline: Deadline) -> AdapterError { let mut details = json!({ "kind": "actionability_timeout",