given the core Resilience Features supersede spring-retry, is there a replacement for the spring-retry @Recover functionality?
ref. https://github.com/spring-projects/spring-retry?tab=readme-ov-file#declarative-example
@Retryable(retryFor = RemoteAccessException.class)
public void service() {
// ... do something
}
@Recover
public void recover(RemoteAccessException e) {
// ... panic
}
from what i understand, the retry support in core is intentionally minimal but as spring-retry is in the attic/maintenance-only now, having to fall back to Programmatic Retry Support (i.e. RetryTemplate) would be a step backwards in my opinion.
if i am not missing something and there is no replacement (yet), please consider this issue a feature-/enhancement-request to add it.
thanks & regards.
Comment From: jhoeller
As a general note: In a programmatic arrangement, this is simply the code in the try block handling RetryException - which is why we have no dedicated recovery callback mechanism there.
Point taken that there is no equivalent in the annotation-based variant. However, at the core framework level, we try to avoid separate annotated methods that semantically work in combination since this is not self-descriptive and not easily discoverable. We'd rather have a recovery callback specified in the @Retryable declaration itself, or possibly a method name to point to. Also, we intend to avoid resolution among multiple recovery methods, and we'd rather avoid substituting the method return value in a recovery method since that seems better expressed in a programmatic RetryTemplate arrangement.
What is your recovery method doing concretely? Is it actually trying to recover, suppressing the exception and pretending that the original method call worked - or rather logging or the like (since you labelled it "panic") which is effectively listener functionality, not changing the outcome? Note that we do not expose listener functionality in @Retryable yet either (since it has to be adapted to RetryTemplate as well as to Reactor for reactive methods) but have been considering that at least.
Comment From: zyro23
the "panic" example is just copy/paste from the spring-retry readme ;)
in my use-cases it is currently always some sort of error-"logging", which may be just an actual log.xyz statement but as well creating an issue via an api call or creating an entry in a error queue/topic or similar.
thanks for your feedback!
Comment From: jhoeller
@zyro23 is this logging and/or error case forwarding specific to a particular service method/class or rather common among multiple services?
In any case, this indeed sounds like a retry listener to me, specifically a retry exhaustion listener. We have been considering a listener mechanism for @Retryable already but were wondering how to design it.
Alternatively, such exception listening could be handled by a custom higher-level aspect that detects RemoteAccessException for certain services - without specifically trying to detect retry exhaustion, rather reacting to RemoteAccessException in general.
Comment From: zyro23
@jhoeller our retry exhaustion handling is pretty much specific to each retryable method.
for the specific case, what comes to mind are additional @Retryable annotation attributes, e.g. a String retryListener() attribute that accepts a bean name of a RetryListener bean.
i can see the generalized/cross-cutting case as well. perhaps something along the lines of a @RetryListenerAdvice annotation which allows a RetryListener bean to be scoped to specific annotations/types/packages, similar to @ControllerAdvice?
but i am really just thinking out loud here.
thanks & regards!
Comment From: quaff
My case is to mark method as idempotent.
@Retryable(retryFor = { OptimisticLockingFailureException.class, DataIntegrityViolationException.class },
notRecoverable = OptimisticLockingFailureException.class, noRetryFor = DataIntegrityViolationException.class)
@Transactional
public @interface Idempotent {
@AliasFor(annotation = Retryable.class)
String recover();
}
public class TestEntityService extends TestEntityService {
final TestEntityRepository repository;
TestEntityService(TestEntityRepository repository) {
this.repository = repository;
}
@Override
@Idempotent(recover = "tryFind")
TestEntity save(Request request) {
TestEntity entity = new TestEntity();
entity.setSeqNo(request.getSeqNo());
return this.repository.save(entity);
}
@Recover
TestEntity tryFind(DataIntegrityViolationException ex, Request request) {
return this.repository.findBySeqNo(request.getSeqNo());
}
}
Comment From: jhoeller
@quaff as for the idempotent use case above, that is not a scenario that we intend to cover at all. Your declaration is essentially combining a retry definition with a non-retryable failure that has a fallback step. Frankly, I find that separation hard to read and hard to follow for anyone reading that code. From a Spring 7 perspective, you should rather separate those steps into a retry definition for OptimisticLockingFailureException (which can still use @Retryable or a custom composed annotation) and a try-catch block for the DataIntegrityViolationException fallback within the save method implementation
@zyro23 for listener scenarios that do not try to change the outcome of the method call, let's track that in #35382.