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.