Skip to content

Conversation

chihyu0917
Copy link
Contributor

This PR supersedes discussion in #496
When multiple @recover candidates exist, choose the most specific and deterministic handler instead of relying on reflection order. Previously the selected @recover method depended on HashMap iteration order. Switching to LinkedHashMap + deterministic sorting removes this problem. Verified by NonDex.

Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signed-off-by: chihyuh3 <[email protected]>

Please, use your legal name according to DCO requirements.
You can configure your Git client to provide a proper name.

Please, also add your name to the @author list of the affected class.

}

}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, ensure a new line in the end of file.

@Override
public T recover(Object[] args, Throwable cause) {
Class<? extends Throwable> causeType = (cause == null) ? null : cause.getClass();
Method method = findClosestMatch(args, cause.getClass());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have just extracted the type, but don't use it here.

@chihyu0917 chihyu0917 force-pushed the fix-bugs branch 2 times, most recently from 5ea121e to 42632b9 Compare October 1, 2025 17:53
@chihyu0917
Copy link
Contributor Author

Thanks for your review! I corrected those mistakes that you mentioned.

  • Commit(s) contain Signed-off-by: trailer (DCO) with legal name
  • Add my name to the @author list
  • End-of-File
  • Now use causeType variable

Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, run tests locally (./mvnw clean verify) before pushing:

Error:  Failures: 
Error:    EnableRetryNoThreadLocalTests>EnableRetryTests.recoveryWithoutParam:151 
expected: "test"
 but was: null
Error:    EnableRetryTests.recoveryWithoutParam:151 
expected: "test"
 but was: null
Error:    RecoverAnnotationRecoveryHandlerTests.noMatch:105 
Expecting actual throwable to be an instance of:
  org.springframework.retry.ExhaustedRetryException
but was:
  java.lang.IllegalArgumentException: argument type mismatch

Also, pay attention to Spring Java Format plugin: https://github.com/spring-io/spring-javaformat.

When multiple @recover candidates exist, choose the most specific and
deterministic handler instead of relying on reflection order.

Signed-off-by: Chih-Yu Huang <[email protected]>
@chihyu0917
Copy link
Contributor Author

Thanks for the review and feedback! My apologies for the regressions in the previous attempt.

I have now run the full ./mvnw clean verify to ensure all tests pass and have applied the spring-javaformat plugin for code style.

The root cause of the non-determinism was twofold: the unpredictable order of methods returned by reflection, and the use of unordered HashMaps which destroyed any stable ordering.

This PR now makes the entire selection process deterministic by:

Sorting Discovered Methods: The init method now sorts all found @recover methods alphabetically, ensuring a stable initial processing order.
Preserving Order: All internal Maps used for storing and filtering these methods have been changed to LinkedHashMap to preserve this stable order throughout the handler's lifecycle.
Deterministic Tie-Breaking: The selection logic now first finds all candidates with the best exception-distance, and then deterministically picks the most specific one (by parameter count) from that stable, sorted list.

This approach resolves the original issue and passes all existing tests. Ready for another look. Thanks!

return result;
}

private Method findMethodWithNoThrowable(Object[] args, List<Method> methods) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this can be static and therefore goes to the end of class.

return null;
}

private int calculateDistance(Class<?> cause, Class<?> type) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also static

int startingIndex = 0;
if (parameterTypes.length > 0 && Throwable.class.isAssignableFrom(parameterTypes[0])) {
startingIndex = 1;
private boolean compareParameters(Object[] args, Class<?>[] parameterTypes, boolean hasThrowable) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static?

@chihyu0917
Copy link
Contributor Author

Following your suggestion, I’ve converted them to static. Thanks for providing your suggestions!

@artembilan artembilan merged commit 12246fe into spring-projects:main Oct 2, 2025
2 checks passed
@artembilan
Copy link
Member

Thank you; and looking forward for more!

@chihyu0917
Copy link
Contributor Author

Great, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants