/*
* 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.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.springframework.classify.BinaryExceptionClassifier;
import org.springframework.dao.DataAccessException;
import org.springframework.retry.ExhaustedRetryException;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryException;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.RetryState;
import org.springframework.retry.policy.MapRetryContextCache;
import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
public class StatefulRecoveryRetryTests {
private RetryTemplate retryTemplate = new RetryTemplate();
private int count = 0;
private List<String> list = new ArrayList<String>();
@Test
public void testOpenSunnyDay() throws Exception {
RetryContext context = retryTemplate.open(new NeverRetryPolicy(), new DefaultRetryState("foo"));
assertNotNull(context);
// we haven't called the processor yet...
assertEquals(0, count);
}
@Test
public void testRegisterThrowable() {
NeverRetryPolicy retryPolicy = new NeverRetryPolicy();
RetryState state = new DefaultRetryState("foo");
RetryContext context = retryTemplate.open(retryPolicy, state);
assertNotNull(context);
retryTemplate.registerThrowable(retryPolicy, state, context, new Exception());
assertFalse(retryPolicy.canRetry(context));
}
@Test
public void testClose() throws Exception {
NeverRetryPolicy retryPolicy = new NeverRetryPolicy();
RetryState state = new DefaultRetryState("foo");
RetryContext context = retryTemplate.open(retryPolicy, state);
assertNotNull(context);
retryTemplate.registerThrowable(retryPolicy, state, context, new Exception());
assertFalse(retryPolicy.canRetry(context));
retryTemplate.close(retryPolicy, context, state, true);
// still can't retry, even if policy is closed
// (not that this would happen in practice)...
assertFalse(retryPolicy.canRetry(context));
}
@Test
public void testRecover() throws Throwable {
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true)));
final String input = "foo";
RetryState state = new DefaultRetryState(input);
RetryCallback<String, Exception> callback = new RetryCallback<String, Exception>() {
public String doWithRetry(RetryContext context) throws Exception {
throw new RuntimeException("Barf!");
}
};
RecoveryCallback<String> recoveryCallback = new RecoveryCallback<String>() {
public String recover(RetryContext context) {
count++;
list.add(input);
return input;
}
};
Object result = null;
try {
result = retryTemplate.execute(callback, recoveryCallback, state);
fail("Expected exception on first try");
}
catch (Exception e) {
// expected...
}
// On the second retry, the recovery path is taken...
result = retryTemplate.execute(callback, recoveryCallback, state);
assertEquals(input, result); // default result is the item
assertEquals(1, count);
assertEquals(input, list.get(0));
}
@Test
public void testSwitchToStatelessForNoRollback() throws Throwable {
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true)));
// Roll back for these:
BinaryExceptionClassifier classifier = new BinaryExceptionClassifier(Collections
.<Class<? extends Throwable>> singleton(DataAccessException.class));
// ...but not these:
assertFalse(classifier.classify(new RuntimeException()));
final String input = "foo";
RetryState state = new DefaultRetryState(input, classifier);
RetryCallback<String, Exception> callback = new RetryCallback<String, Exception>() {
public String doWithRetry(RetryContext context) throws Exception {
throw new RuntimeException("Barf!");
}
};
RecoveryCallback<String> recoveryCallback = new RecoveryCallback<String>() {
public String recover(RetryContext context) {
count++;
list.add(input);
return input;
}
};
Object result = null;
// On the second retry, the recovery path is taken...
result = retryTemplate.execute(callback, recoveryCallback, state);
assertEquals(input, result); // default result is the item
assertEquals(1, count);
assertEquals(input, list.get(0));
}
@Test
public void testExhaustedClearsHistoryAfterLastAttempt() throws Throwable {
RetryPolicy retryPolicy = new SimpleRetryPolicy(1, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true));
retryTemplate.setRetryPolicy(retryPolicy);
final String input = "foo";
RetryState state = new DefaultRetryState(input);
RetryCallback<String, Exception> callback = new RetryCallback<String, Exception>() {
public String doWithRetry(RetryContext context) throws Exception {
throw new RuntimeException("Barf!");
}
};
try {
retryTemplate.execute(callback, state);
fail("Expected ExhaustedRetryException");
}
catch (RuntimeException e) {
assertEquals("Barf!", e.getMessage());
}
try {
retryTemplate.execute(callback, state);
fail("Expected ExhaustedRetryException");
}
catch (ExhaustedRetryException e) {
// expected
}
RetryContext context = retryTemplate.open(retryPolicy, state);
// True after exhausted - the history is reset...
assertTrue(retryPolicy.canRetry(context));
}
@Test
public void testKeyGeneratorNotConsistentAfterFailure() throws Throwable {
RetryPolicy retryPolicy = new SimpleRetryPolicy(3, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true));
retryTemplate.setRetryPolicy(retryPolicy);
final StringHolder item = new StringHolder("bar");
RetryState state = new DefaultRetryState(item);
RetryCallback<StringHolder, Exception> callback = new RetryCallback<StringHolder, Exception>() {
public StringHolder doWithRetry(RetryContext context) throws Exception {
// This simulates what happens if someone uses a primary key
// for hashCode and equals and then relies on default key
// generator
((StringHolder) item).string = ((StringHolder) item).string + (count++);
throw new RuntimeException("Barf!");
}
};
try {
retryTemplate.execute(callback, state);
fail("Expected RuntimeException");
}
catch (RuntimeException ex) {
String message = ex.getMessage();
assertEquals("Barf!", message);
}
// Only fails second attempt because the algorithm to detect
// inconsistent has codes relies on the cache having been used for this
// item already...
try {
retryTemplate.execute(callback, state);
fail("Expected RetryException");
}
catch (RetryException ex) {
String message = ex.getMessage();
assertTrue("Message doesn't contain 'inconsistent': " + message, message.contains("inconsistent"));
}
RetryContext context = retryTemplate.open(retryPolicy, state);
// True after exhausted - the history is reset...
assertEquals(0, context.getRetryCount());
}
@Test
public void testCacheCapacity() throws Throwable {
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true)));
retryTemplate.setRetryContextCache(new MapRetryContextCache(1));
RetryCallback<Object, Exception> callback = new RetryCallback<Object, Exception>() {
public Object doWithRetry(RetryContext context) throws Exception {
count++;
throw new RuntimeException("Barf!");
}
};
try {
retryTemplate.execute(callback, new DefaultRetryState("foo"));
fail("Expected RuntimeException");
}
catch (RuntimeException e) {
assertEquals("Barf!", e.getMessage());
}
try {
retryTemplate.execute(callback, new DefaultRetryState("bar"));
fail("Expected RetryException");
}
catch (RetryException e) {
String message = e.getMessage();
assertTrue("Message does not contain 'capacity': " + message, message.indexOf("capacity") >= 0);
}
}
@Test
public void testCacheCapacityNotReachedIfRecovered() throws Throwable {
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(1, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true));
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setRetryContextCache(new MapRetryContextCache(2));
final StringHolder item = new StringHolder("foo");
RetryState state = new DefaultRetryState(item);
RetryCallback<Object, Exception> callback = new RetryCallback<Object, Exception>() {
public Object doWithRetry(RetryContext context) throws Exception {
count++;
throw new RuntimeException("Barf!");
}
};
RecoveryCallback<Object> recoveryCallback = new RecoveryCallback<Object>() {
public Object recover(RetryContext context) throws Exception {
return null;
}
};
try {
retryTemplate.execute(callback, recoveryCallback, state);
fail("Expected RuntimeException");
}
catch (RuntimeException e) {
assertEquals("Barf!", e.getMessage());
}
retryTemplate.execute(callback, recoveryCallback, state);
RetryContext context = retryTemplate.open(retryPolicy, state);
// True after exhausted - the history is reset...
assertEquals(0, context.getRetryCount());
}
private static class StringHolder {
private String string;
public StringHolder(String string) {
this.string = string;
}
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof StringHolder)) {
return false;
}
return string.equals(((StringHolder) obj).string);
}
public int hashCode() {
return string.hashCode();
}
public String toString() {
return "String: " + string + " (hash = " + hashCode() + ")";
}
}
}