From fbfc0b5c6bc0366410e99d8cfc25054309e27cca Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 25 Mar 2011 16:21:49 +0000 Subject: [PATCH] Add optional recovery callback for stateless retry interceptor --- .../RetryOperationsInterceptor.java | 101 ++++++++++++------ .../RetryOperationsInterceptorTests.java | 36 ++++++- 2 files changed, 99 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java index 97ac0854..fcda5cb5 100644 --- a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java @@ -1,24 +1,25 @@ /* * 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; @@ -26,17 +27,14 @@ 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}.
+ * 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}.
* - * 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 @@ -44,31 +42,32 @@ 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() { + RetryCallback retryCallback = new RetryCallback() { 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 { + + private final Object[] args; + + private final MethodInvocationRecoverer recoverer; + + /** + * @param args the item that failed. + */ + private ItemRecovererCallback(Object[] args, MethodInvocationRecoverer 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."); + } + + } + } diff --git a/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java index d629e0a4..15041086 100644 --- a/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java +++ b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java @@ -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,8 +54,8 @@ public class RetryOperationsInterceptorTests extends TestCase { 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)); @@ -58,12 +63,30 @@ protected void setUp() throws Exception { 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 + ., Boolean> singletonMap(Exception.class, true))); + interceptor.setRetryOperations(template); + interceptor.setRecoverer(new MethodInvocationRecoverer() { + 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 list = new ArrayList(); @@ -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() {