Permalink
Browse files

Add optional recovery callback for stateless retry interceptor

  • Loading branch information...
1 parent a97db67 commit fbfc0b5c6bc0366410e99d8cfc25054309e27cca @dsyer dsyer committed Mar 25, 2011
@@ -1,74 +1,73 @@
/*
* 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.
+ *
+ * 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.interceptor;
+import java.util.Arrays;
+
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.ProxyMethodInvocation;
+import org.springframework.retry.ExhaustedRetryException;
+import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryOperations;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
/**
- * A {@link MethodInterceptor} that can be used to automatically retry calls to
- * a method on a service if it fails. The injected {@link RetryOperations} is
- * used to control the number of retries. By default it will retry a fixed
- * number of times, according to the defaults in {@link RetryTemplate}.<br/>
+ * A {@link MethodInterceptor} that can be used to automatically retry calls to a method on a service if it fails. The
+ * injected {@link RetryOperations} is used to control the number of retries. By default it will retry a fixed number of
+ * times, according to the defaults in {@link RetryTemplate}.<br/>
*
- * Hint about transaction boundaries. If you want to retry a failed transaction
- * you need to make sure that the transaction boundary is inside the retry,
- * otherwise the successful attempt will roll back with the whole transaction.
- * If the method being intercepted is also transactional, then use the ordering
- * hints in the advice declarations to ensure that this one is before the
- * transaction interceptor in the advice chain.
+ * Hint about transaction boundaries. If you want to retry a failed transaction you need to make sure that the
+ * transaction boundary is inside the retry, otherwise the successful attempt will roll back with the whole transaction.
+ * If the method being intercepted is also transactional, then use the ordering hints in the advice declarations to
+ * ensure that this one is before the transaction interceptor in the advice chain.
*
* @author Rob Harrop
* @author Dave Syer
*/
public class RetryOperationsInterceptor implements MethodInterceptor {
private RetryOperations retryOperations = new RetryTemplate();
+ private MethodInvocationRecoverer<?> recoverer;
public void setRetryOperations(RetryOperations retryTemplate) {
Assert.notNull(retryTemplate, "'retryOperations' cannot be null.");
this.retryOperations = retryTemplate;
}
+ public void setRecoverer(MethodInvocationRecoverer<?> recoverer) {
+ this.recoverer = recoverer;
+ }
+
public Object invoke(final MethodInvocation invocation) throws Throwable {
- return this.retryOperations.execute(new RetryCallback<Object>() {
+ RetryCallback<Object> retryCallback = new RetryCallback<Object>() {
public Object doWithRetry(RetryContext context) throws Exception {
/*
- * If we don't copy the invocation carefully it won't keep a
- * reference to the other interceptors in the chain. We don't
- * have a choice here but to specialise to
- * ReflectiveMethodInvocation (but how often would another
- * implementation come along?).
+ * If we don't copy the invocation carefully it won't keep a reference to the other interceptors in the
+ * chain. We don't have a choice here but to specialise to ReflectiveMethodInvocation (but how often
+ * would another implementation come along?).
*/
if (invocation instanceof ProxyMethodInvocation) {
try {
- return ((ProxyMethodInvocation) invocation)
- .invocableClone().proceed();
- }
- catch (Exception e) {
+ return ((ProxyMethodInvocation) invocation).invocableClone().proceed();
+ } catch (Exception e) {
throw e;
} catch (Error e) {
throw e;
@@ -81,6 +80,42 @@ public Object doWithRetry(RetryContext context) throws Exception {
}
}
- });
+ };
+
+ if (recoverer != null) {
+ ItemRecovererCallback recoveryCallback = new ItemRecovererCallback(invocation.getArguments(), recoverer);
+ return this.retryOperations.execute(retryCallback, recoveryCallback);
+ }
+
+ return this.retryOperations.execute(retryCallback);
+
}
+
+ /**
+ * @author Dave Syer
+ *
+ */
+ private static final class ItemRecovererCallback implements RecoveryCallback<Object> {
+
+ private final Object[] args;
+
+ private final MethodInvocationRecoverer<? extends Object> recoverer;
+
+ /**
+ * @param args the item that failed.
+ */
+ private ItemRecovererCallback(Object[] args, MethodInvocationRecoverer<? extends Object> recoverer) {
+ this.args = Arrays.asList(args).toArray();
+ this.recoverer = recoverer;
+ }
+
+ public Object recover(RetryContext context) {
+ if (recoverer != null) {
+ return recoverer.recover(args, context.getLastThrowable());
+ }
+ throw new ExhaustedRetryException("Retry was exhausted but there was no recovery path.");
+ }
+
+ }
+
}
@@ -16,16 +16,21 @@
package org.springframework.retry.interceptor;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import junit.framework.TestCase;
-
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
+import org.junit.Before;
+import org.junit.Test;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.SingletonTargetSource;
@@ -37,7 +42,7 @@
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ClassUtils;
-public class RetryOperationsInterceptorTests extends TestCase {
+public class RetryOperationsInterceptorTests {
private RetryOperationsInterceptor interceptor;
@@ -49,21 +54,39 @@
private static int transactionCount;
- protected void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() throws Exception {
interceptor = new RetryOperationsInterceptor();
target = new ServiceImpl();
service = (Service) ProxyFactory.getProxy(Service.class, new SingletonTargetSource(target));
count = 0;
transactionCount = 0;
}
+ @Test
public void testDefaultInterceptorSunnyDay() throws Exception {
((Advised) service).addAdvice(interceptor);
service.service();
assertEquals(2, count);
}
+ @Test
+ public void testDefaultInterceptorWithRecovery() throws Exception {
+ RetryTemplate template = new RetryTemplate();
+ template.setRetryPolicy(new SimpleRetryPolicy(1, Collections
+ .<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true)));
+ interceptor.setRetryOperations(template);
+ interceptor.setRecoverer(new MethodInvocationRecoverer<Void>() {
+ public Void recover(Object[] args, Throwable cause) {
+ return null;
+ }
+ });
+ ((Advised) service).addAdvice(interceptor);
+ service.service();
+ assertEquals(1, count);
+ }
+
+ @Test
public void testInterceptorChainWithRetry() throws Exception {
((Advised) service).addAdvice(interceptor);
final List<String> list = new ArrayList<String>();
@@ -82,6 +105,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
assertEquals(2, list.size());
}
+ @Test
public void testRetryExceptionAfterTooManyAttempts() throws Exception {
((Advised) service).addAdvice(interceptor);
RetryTemplate template = new RetryTemplate();
@@ -97,6 +121,7 @@ public void testRetryExceptionAfterTooManyAttempts() throws Exception {
assertEquals(1, count);
}
+ @Test
public void testOutsideTransaction() throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(ClassUtils
.addResourcePathToPackagePath(getClass(), "retry-transaction-test.xml"));
@@ -110,6 +135,7 @@ public void testOutsideTransaction() throws Exception {
assertEquals(2, transactionCount);
}
+ @Test
public void testIllegalMethodInvocationType() throws Throwable {
try {
interceptor.invoke(new MethodInvocation() {

0 comments on commit fbfc0b5

Please sign in to comment.