refactor: share transient resolution classification

This commit is contained in:
Lahfir 2026-07-13 23:23:07 -07:00
parent c137343e23
commit ff278ada56
3 changed files with 53 additions and 24 deletions

View file

@ -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());
}
}
}

View file

@ -223,24 +223,17 @@ fn point_observed_bounds_hash(err: &AppError) -> Option<u64> {
}
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;

View file

@ -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",