/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.retry.support;
import static org.easymock.EasyMock.createStrictMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.springframework.classify.BinaryExceptionClassifier;
import org.springframework.retry.ExhaustedRetryException;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.TerminatedRetryException;
import org.springframework.retry.backoff.BackOffContext;
import org.springframework.retry.backoff.BackOffInterruptedException;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.StatelessBackOffPolicy;
import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
/**
* @author Rob Harrop
* @author Dave Syer
*/
public class RetryTemplateTests {
RetryContext context;
int count = 0;
@Test
public void testSuccessfulRetry() throws Throwable {
for (int x = 1; x <= 10; x++) {
MockRetryCallback callback = new MockRetryCallback();
callback.setAttemptsBeforeSuccess(x);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class,
true)));
retryTemplate.execute(callback);
assertEquals(x, callback.attempts);
}
}
@Test
public void testSpecificExceptionRetry() throws Throwable {
for (int x = 1; x <= 10; x++) {
final int attemptsBeforeSuccess = x;
final AtomicInteger attempts = new AtomicInteger(0);
RetryCallback<String, IllegalStateException> callback = new RetryCallback<String, IllegalStateException>() {
@Override
public String doWithRetry(RetryContext context)
throws IllegalStateException {
if (attempts.incrementAndGet() < attemptsBeforeSuccess) {
// The parametrized exception type in the callback is really just
// syntactic sugar since rules of erasure mean that the handler
// can't really tell the difference between runtime exceptions.
throw new IllegalArgumentException("Planned");
}
return "foo";
}
};
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class,
true)));
retryTemplate.execute(callback);
assertEquals(x, attempts.get());
}
}
@Test
public void testSuccessfulRecovery() throws Throwable {
MockRetryCallback callback = new MockRetryCallback();
callback.setAttemptsBeforeSuccess(3);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(2,
Collections.<Class<? extends Throwable>, Boolean> singletonMap(
Exception.class, true)));
final Object value = new Object();
Object result = retryTemplate.execute(callback, new RecoveryCallback<Object>() {
public Object recover(RetryContext context) throws Exception {
return value;
}
});
assertEquals(2, callback.attempts);
assertEquals(value, result);
}
@Test
public void testAlwaysTryAtLeastOnce() throws Throwable {
MockRetryCallback callback = new MockRetryCallback();
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new NeverRetryPolicy());
retryTemplate.execute(callback);
assertEquals(1, callback.attempts);
}
@Test
public void testNoSuccessRetry() throws Throwable {
MockRetryCallback callback = new MockRetryCallback();
// Something that won't be thrown by JUnit...
callback.setExceptionToThrow(new IllegalArgumentException());
callback.setAttemptsBeforeSuccess(Integer.MAX_VALUE);
RetryTemplate retryTemplate = new RetryTemplate();
int retryAttempts = 2;
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(retryAttempts,
Collections.<Class<? extends Throwable>, Boolean> singletonMap(
Exception.class, true)));
try {
retryTemplate.execute(callback);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertNotNull(e);
assertEquals(retryAttempts, callback.attempts);
return;
}
fail("Expected IllegalArgumentException");
}
@Test
public void testDefaultConfigWithExceptionSubclass() throws Throwable {
MockRetryCallback callback = new MockRetryCallback();
int attempts = 3;
callback.setAttemptsBeforeSuccess(attempts);
callback.setExceptionToThrow(new IllegalArgumentException());
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(attempts,
Collections.<Class<? extends Throwable>, Boolean> singletonMap(
Exception.class, true)));
retryTemplate.execute(callback);
assertEquals(attempts, callback.attempts);
}
@Test
public void testRollbackClassifierOverridesRetryPolicy() throws Throwable {
MockRetryCallback callback = new MockRetryCallback();
int attempts = 3;
callback.setAttemptsBeforeSuccess(attempts);
callback.setExceptionToThrow(new IllegalArgumentException());
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(attempts,
Collections.<Class<? extends Throwable>, Boolean> singletonMap(
Exception.class, true)));
BinaryExceptionClassifier classifier = new BinaryExceptionClassifier(
Collections
.<Class<? extends Throwable>> singleton(IllegalArgumentException.class),
false);
retryTemplate.execute(callback, new DefaultRetryState("foo", classifier));
assertEquals(attempts, callback.attempts);
}
@Test
public void testSetExceptions() throws Throwable {
RetryTemplate template = new RetryTemplate();
SimpleRetryPolicy policy = new SimpleRetryPolicy(3,
Collections.<Class<? extends Throwable>, Boolean> singletonMap(
RuntimeException.class, true));
template.setRetryPolicy(policy);
int attempts = 3;
MockRetryCallback callback = new MockRetryCallback();
callback.setAttemptsBeforeSuccess(attempts);
try {
template.execute(callback);
} catch (Exception e) {
assertNotNull(e);
assertEquals(1, callback.attempts);
}
callback.setExceptionToThrow(new RuntimeException());
template.execute(callback);
assertEquals(attempts, callback.attempts);
}
@Test
public void testBackOffInvoked() throws Throwable {
for (int x = 1; x <= 10; x++) {
MockRetryCallback callback = new MockRetryCallback();
MockBackOffStrategy backOff = new MockBackOffStrategy();
callback.setAttemptsBeforeSuccess(x);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(backOff);
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class,
true)));
retryTemplate.execute(callback);
assertEquals(x, callback.attempts);
assertEquals(1, backOff.startCalls);
assertEquals(x - 1, backOff.backOffCalls);
}
}
@Test
public void testEarlyTermination() throws Throwable {
try {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.execute(new RetryCallback<Object, Exception>() {
public Object doWithRetry(RetryContext status) throws Exception {
status.setExhaustedOnly();
throw new IllegalStateException("Retry this operation");
}
});
fail("Expected ExhaustedRetryException");
} catch (ExhaustedRetryException ex) {
// Expected for internal retry policy (external would recover
// gracefully)
assertEquals("Retry this operation", ex.getCause().getMessage());
}
}
@Test
public void testEarlyTerminationWithOriginalException() throws Throwable {
try {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setThrowLastExceptionOnExhausted(true);
retryTemplate.execute(new RetryCallback<Object, Throwable>() {
public Object doWithRetry(RetryContext status) throws Exception {
status.setExhaustedOnly();
throw new IllegalStateException("Retry this operation");
}
});
fail("Expected ExhaustedRetryException");
} catch (IllegalStateException ex) {
// Expected for internal retry policy (external would recover
// gracefully)
assertEquals("Retry this operation", ex.getMessage());
}
}
@Test
public void testNestedContexts() throws Throwable {
RetryTemplate outer = new RetryTemplate();
final RetryTemplate inner = new RetryTemplate();
outer.execute(new RetryCallback<Object, Throwable>() {
public Object doWithRetry(RetryContext status) throws Throwable {
context = status;
count++;
Object result = inner.execute(new RetryCallback<Object, Throwable>() {
public Object doWithRetry(RetryContext status) throws Throwable {
count++;
assertNotNull(context);
assertNotSame(status, context);
assertSame(context, status.getParent());
assertSame("The context should be the child", status,
RetrySynchronizationManager.getContext());
return null;
}
});
assertSame("The context should be restored", status,
RetrySynchronizationManager.getContext());
return result;
}
});
assertEquals(2, count);
}
@Test
public void testRethrowError() throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new NeverRetryPolicy());
try {
retryTemplate.execute(new RetryCallback<Object, Exception>() {
public Object doWithRetry(RetryContext context) throws Exception {
throw new Error("Realllly bad!");
}
});
fail("Expected Error");
} catch (Error e) {
assertEquals("Realllly bad!", e.getMessage());
}
}
@Test
public void testFailedPolicy() throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new NeverRetryPolicy() {
@Override
public void registerThrowable(RetryContext context, Throwable throwable) {
throw new RuntimeException("Planned");
}
});
try {
retryTemplate.execute(new RetryCallback<Object, Exception>() {
public Object doWithRetry(RetryContext context) throws Exception {
throw new RuntimeException("Realllly bad!");
}
});
fail("Expected Error");
} catch (TerminatedRetryException e) {
assertEquals("Planned", e.getCause().getMessage());
}
}
@Test
public void testBackOffInterrupted() throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(new StatelessBackOffPolicy() {
protected void doBackOff() throws BackOffInterruptedException {
throw new BackOffInterruptedException("foo");
}
});
try {
retryTemplate.execute(new RetryCallback<Object, Exception>() {
public Object doWithRetry(RetryContext context) throws Exception {
throw new RuntimeException("Bad!");
}
});
fail("Expected RuntimeException");
} catch (BackOffInterruptedException e) {
assertEquals("foo", e.getMessage());
}
}
/**
* {@link BackOffPolicy} should apply also for exceptions that are re-thrown.
*/
@Test
public void testNoBackOffForRethrownException() throws Throwable {
RetryTemplate tested = new RetryTemplate();
tested.setRetryPolicy(new SimpleRetryPolicy(1,
Collections.<Class<? extends Throwable>, Boolean> singletonMap(
Exception.class, true)));
BackOffPolicy bop = createStrictMock(BackOffPolicy.class);
BackOffContext backOffContext = new BackOffContext() {
};
tested.setBackOffPolicy(bop);
expect(bop.start(isA(RetryContext.class))).andReturn(backOffContext);
replay(bop);
try {
tested.execute(new RetryCallback<Object, Exception>() {
public Object doWithRetry(RetryContext context) throws Exception {
throw new Exception("maybe next time!");
}
}, null, new DefaultRetryState(tested) {
@Override
public boolean rollbackFor(Throwable exception) {
return true;
}
});
fail();
} catch (Exception expected) {
assertEquals("maybe next time!", expected.getMessage());
}
verify(bop);
}
private static class MockRetryCallback implements RetryCallback<Object, Exception> {
private int attempts;
private int attemptsBeforeSuccess;
private Exception exceptionToThrow = new Exception();
public Object doWithRetry(RetryContext status) throws Exception {
this.attempts++;
if (attempts < attemptsBeforeSuccess) {
throw this.exceptionToThrow;
}
return null;
}
public void setAttemptsBeforeSuccess(int attemptsBeforeSuccess) {
this.attemptsBeforeSuccess = attemptsBeforeSuccess;
}
public void setExceptionToThrow(Exception exceptionToThrow) {
this.exceptionToThrow = exceptionToThrow;
}
}
private static class MockBackOffStrategy implements BackOffPolicy {
public int backOffCalls;
public int startCalls;
public BackOffContext start(RetryContext status) {
startCalls++;
return null;
}
public void backOff(BackOffContext backOffContext)
throws BackOffInterruptedException {
backOffCalls++;
}
}
}