I understand how @CircuitBreaker
annotation should work, but not sure about resetTimeout
parameter value.
I have an application that keep asking other external service in every 10 seconds. Each request might took 1-3 seconds. Default three attempts are fine, so I guess 40 seconds in openTimeout
should be fine. I decided to set 80 seconds as resetTimeout
. As far as I understand documentation if 3 failures will happen in 40 seconds then circuit will be open for period specified in resetTimeout param (80 sec).
During tests I noticed that if circuit is open and another request coming in then external service will not be called. That's ok but I expect that after resetTimeout
(80s) a real service will be called which never happened.
What is strange that if circuit is open and service is not called for period of resetTimeout(80s) then circuit will reset. IMHO spring should compare current time and failure that opened circuit, currently looks like it compares current time with last request.
Did I misunderstood idea of this library?
Service:
package test;
import org.springframework.retry.annotation.CircuitBreaker;
import org.springframework.retry.annotation.Recover;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClientException;
@Component
class ConnectionService {
@CircuitBreaker(include = { RestClientException.class }, openTimeout = 10_000, resetTimeout = 20_000)
String doUpload(String payload) {
if(payload.contentEquals("FAIL")) {
throw new RestClientException("");
}
System.out.println("real service called");
//callling external service....
return payload;
}
@Recover
public String recover(String payload) {
return "recovered";
}
}
Test:
package test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.annotation.EnableRetry;
@RunWith(MockitoJUnitRunner.class)
public class SpringRetryTest {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
SpringRetryTest.TestConfiguration.class);
ConnectionService connectionService = context.getBean(ConnectionService.class);
@Test
public void testCircuitBreaker() throws InterruptedException {
incorrectStep();
incorrectStep();
incorrectStep();
incorrectStep();
incorrectStep();
System.out.println();
System.out.println();
System.out.println();
final long l = System.currentTimeMillis();
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
correctStep(l);
//wait more than resetTimeout
System.out.println();
System.out.println();
System.out.println();
Thread.sleep(21_000L);
correctStep(l);
}
private void incorrectStep() throws InterruptedException {
doFailedUpload(connectionService);
Thread.sleep(1_000L);
System.out.println();
}
private void correctStep(final long l) throws InterruptedException {
doCorrectUpload(connectionService);
Thread.sleep(1_000L);
printTime(l);
}
private void printTime(final long l) {
System.out.println(String.format("%d ms after last failure", (System.currentTimeMillis() - l)));
}
private void doFailedUpload(ConnectionService externalService) throws InterruptedException {
System.out.println("before fail");
externalService.doUpload("FAIL");
System.out.println("after fail");
Thread.sleep(900);
}
private void doCorrectUpload(ConnectionService externalService) throws InterruptedException {
System.out.println("before ok");
externalService.doUpload("");
System.out.println("after ok");
Thread.sleep(900);
}
@Configuration
@EnableRetry
protected static class TestConfiguration {
@Bean
public ConnectionService externalService() {
return new ConnectionService();
}
@Bean
public RetryListener retryListener1() {
return new RetryListener() {
@Override
public <T, E extends Throwable> boolean open(final RetryContext retryContext, final RetryCallback<T, E> retryCallback) {
System.out.println("----/ ---- open, retry count:" + retryContext.getRetryCount());
return true;
}
@Override
public <T, E extends Throwable> void close(final RetryContext retryContext, final RetryCallback<T, E> retryCallback, final Throwable throwable) {
System.out.println("---------- close, retry count:" + retryContext.getRetryCount());
}
@Override
public <T, E extends Throwable> void onError(final RetryContext retryContext, final RetryCallback<T, E> retryCallback, final Throwable throwable) {
System.out.println("onError, retry count:" + retryContext.getRetryCount());
}
};
}
}
}
I'm using osx 10.12.6, java 1.8.0_144 spring:4.2.3.RELEASE and spring-retry:1.2.1.RELEASE ( my colleague tested 1.2.2 as well with the same result)
Issued had been solved: https://github.com/spring-projects/spring-retry/issues/99
It's not released yet, at this moment available as snapshot.