Springboot version: 3.3.2 Java: 21
import io.cloudevents.CloudEvent;
import java.io.IOException;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.core5.http.NoHttpResponseException;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
@Component
@Slf4j
public class MyClient {
private final RestTemplate restTemplate;
public MyClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
restTemplate.setInterceptors(createInterceptors());
}
@Retryable(
retryFor = {NoHttpResponseException.class, RestClientException.class},
noRetryFor = {MyRunTimeException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 1),
listeners = {"MyRetryListener"})
public CloudEvent getDecision(CloudEvent cloudEvent, String endpoint) {
HttpEntity<CloudEvent> requestEntity = new HttpEntity<>(cloudEvent);
ResponseEntity<CloudEvent> responseEntity =
restTemplate.exchange(endpoint, HttpMethod.POST, requestEntity, CloudEvent.class);
return responseEntity.getBody();
}
private List<ClientHttpRequestInterceptor> createInterceptors() {
return List.of(
(request, body, execution) -> {
try {
ClientHttpResponse response = execution.execute(request, body);
HttpStatusCode statusCode = response.getStatusCode();
if (!statusCode.is2xxSuccessful()) {
throw new MyRunTimeException(
"Failed to create decision: response status %s".formatted(statusCode));
} else {
return response;
}
} catch (IOException ex) {
throw new MyRunTimeException("Unexpected error: %s".formatted(ex.getMessage()), ex);
}
});
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class MyRetryListener implements RetryListener {
public <T, E extends Throwable> void onError(
RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.error(
"My retry attempt {} failed due to: {}", context.getRetryCount(), throwable.getMessage());
}
}
I'm confused about the Springboot @Retryable behaviour, I have especially add exception MyRunTimeException in noRetryFor
, but the @Retryable is still executed once for MyRunTimeException
The RestTemplate interceptor throws MyRunTimeException if the response status code is not 2xx. So I created @SpringbootTest and simulate the response status is 500.
I was able to find the error log MyRetryListener : My retry attempt 1 failed due to: Failed to create decision: response status 500 INTERNAL_SERVER_ERROR if the response status is not 2xx. It only retried once. May I ask how can I fix this issue?
The logic there in the RetryTemplate
like this:
catch (Throwable e) {
lastException = e;
try {
registerThrowable(retryPolicy, state, context, e);
}
catch (Exception ex) {
throw new TerminatedRetryException("Could not register throwable", ex);
}
finally {
doOnErrorInterceptors(retryCallback, context, e);
}
if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
That registerThrowable()
calls eventually a RetryContextSupport
:
public void registerThrowable(Throwable throwable) {
this.lastException = throwable;
if (throwable != null)
count++;
}
So, that is really expected to see that 1
in logs of your MyRetryListener.onError()
.
That next if (canRetry(retryPolicy, context)
after interceptor is not going to be fulfilled because of your noRetryFor = {MyRunTimeException.class}
. Therefore so far all good.