0

I've inherited a Spring Integration project that incorporates Spring Retry. I'm not sure it has ever been tested out, and there is no separate tests for it. So I'm trying to exercise it with a simple scenario.

By mocking the RestTemplate exchange method, I'd like to be able to test the retry logic. I can get the exception I want thrown to occur, but it only happens once - no retry is taking place.

The XML for the retry advice is here(retry-advice-context.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:int="http://www.springframework.org/schema/integration"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
       http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <bean id="retryAdvice" class="org.springframework.integration.handler.advice.RequestHandlerRetryAdvice" >
        <property name="retryTemplate">
            <bean class="org.springframework.retry.support.RetryTemplate">
                <property name="backOffPolicy">
                    <bean class="org.springframework.retry.backoff.ExponentialBackOffPolicy">
                        <property name="initialInterval" value="${retry.initialInterval}"/>
                        <property name="maxInterval" value="${retry.maxInterval}"/>
                        <property name="multiplier" value="${retry.multiplier}"/>
                    </bean>
                </property>

                <property name="retryPolicy">
                    <bean class="com.reachlocal.mediapublishing.shim.integration.retry.CustomRetryPolicy">
                        <constructor-arg name="maxAttempts" value="${retry.maxAttempts}" />
                        <constructor-arg name="retryableExceptions" ref="retryableExceptions" />
                    </bean>
                </property>
            </bean>
        </property>

        <property name="recoveryCallback">
            <bean class="org.springframework.integration.handler.advice.ErrorMessageSendingRecoverer">
                <constructor-arg ref="errorChannel" />
            </bean>
        </property>
    </bean>

    <util:map id="retryableExceptions"  map-class="java.util.HashMap" >
        <entry key="java.net.SocketException" value="true" />
        <entry key="com.examplel.ConnectionException" value="true" />
        <entry key="com.example.CustomException" value="true" />
    </util:map>

</beans>

Here is a chunk of an SI processing file:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:int="http://www.springframework.org/schema/integration"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                  http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">

    <import resource="retry-advice-context.xml"/>

    <int:channel id="channel1">
    </int:channel>

    <int:header-value-router id="commandTypeRouter" input-channel="commandChannel"  <---DEFINED IN MASTER FILE
                             header-name="commandType" resolution-required="true">
        <int:mapping value="COMMAND_1" channel="channel1"/>
    </int:header-value-router>

    <int:chain id="command1Chain" input-channel="channel1" output-channel="commandProcessed">
        <int:header-enricher>
            <int:error-channel ref="errorChannel" />
        </int:header-enricher>
        <int:service-activator ref="eventDataWriter" method = "addEventStart"/>

        <int:service-activator ref="accountProcessor" method="processAccount">
            <int:request-handler-advice-chain><ref bean="retryAdvice" /></int:request-handler-advice-chain>
        </int:service-activator>
    </int:chain>
</beans>

So the retry bean, retryAdvice, is a part of the different chains. There is a lot more to the chains, so I only want to be able to check the retry logic from the service layer. There are no Retry annotations anywhere in the code (don't know if they are needed).

A couple of questions:

  1. Can I test the retry feature from the service layer or do I need to execute the entire chain?
  2. Is there anything missing (annotations, other XML) that the retry mechanism requires?

BTW, this is using SI 4.1.3.

Thanks.

UPDATE 1:

Managed to get a Gary's project running in my environment. After that I added in the retry-advice-context.xml file into the main SI xml. I changed the Map to only have RuntimeException in it. The log statements showed ExponentialBackoffPolicy statements. I was also getting the RetryTemplate debug log statements.

With some more understanding I translated what was there to the real code I'm working with and had more success. I'm getting the log statement that my exception occurred and will be retried up to 3 times.

Unfortunately what I get is:

17:29:26.154 DEBUG [task-scheduler-2][org.springframework.retry.support.RetryTemplate] Checking for rethrow: count=1
17:29:26.155 DEBUG [task-scheduler-2][org.springframework.retry.support.RetryTemplate] Retry failed last attempt: count=1

So it initially knows that it's supposed to retry up to 3 times. But then it states that it's made its last attempt after 1 retry.

In Gary's working code the debug statements will show Retry: count=2 etc... for the next successive try.

There is a sleep statement in the Spock test code during the time the retries should be taking place. I both lengthened and shortened the time without any change.

Going to continue to try and debug through the retry code to see why it stops on the 1st retry.

Les
  • 487
  • 1
  • 10
  • 22
  • Can you clarify in question 1, what you mean by "the service layer"? – code4kix Sep 21 '16 at 01:42
  • What I mean is in the service class that makes the REST call to the external system. The request that may have to be retried. It calls the code with the Spring RestTemplate use. – Les Sep 21 '16 at 05:00

1 Answers1

1

You don't need any additional annotations or XML.

If you give the chain and service id attributes you can test the handler independently...

<int:chain id="myChain" input-channel="foo">
    <int:transformer expression="payload" />
    <int:service-activator id="myService" ref="bar">
        <int:request-handler-advice-chain>
            <int:ref bean="retry" />
        </int:request-handler-advice-chain>
    </int:service-activator>
</int:chain>

<bean id="retry" class="org.springframework.integration.handler.advice.RequestHandlerRetryAdvice" />

<bean id="bar" class="com.example.Bar" />

With Bar being...

public class Bar {

    private int count;

    public void bar(String in) {
        System.out.println(in);
        if (count++ < 2) {
            throw new RuntimeException("foo");
        }
    }

}

Test:

public class So39604931ApplicationTests {

    @Autowired
    @Qualifier("myChain$child.myService.handler")
    public MessageHandler handler;

    @Test
    public void test() {
        handler.handleMessage(new GenericMessage<>("foo"));
    }

}

Result:

foo
foo
foo

You should also turn on debug logging for org.springframework.retry to see the retry behavior.

08:51:16.897 [main] DEBUG o.s.retry.support.RetryTemplate - Retry: count=0
foo
08:51:16.902 [main] DEBUG o.s.retry.support.RetryTemplate - Checking for rethrow: count=1
08:51:16.902 [main] DEBUG o.s.retry.support.RetryTemplate - Retry: count=1
foo
08:51:16.903 [main] DEBUG o.s.retry.support.RetryTemplate - Checking for rethrow: count=2
08:51:16.903 [main] DEBUG o.s.retry.support.RetryTemplate - Retry: count=2
foo
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • A couple of questions: so you still have to test via the chain since you are dealing with the message handler, correct? That's the reason for GenericMessage. My XML for a service-activator only uses `ref`, so what happens with @Qualifier? I don't really want to add `id` attributes to all my service-activators. Basically I don't know how you came up with `child.myService`. Thanks. – Les Sep 21 '16 at 19:29
  • A chain is a chain of message handlers; the qualifier gets you to the hander instance that wraps your `accountProcessor` bean; so you are just exercising that individual handler. The general naming strategy is `$child..handler`. Without an `id` on the chain member, we don't register the handler (which wraps your bean) in the application context, so there's no way to wire it into a test. If you don't want to use an id, an alternative is to inject the`MessageHandlerChain` bean and use `getHandlers()`. But that's a bit brittle because it's by position. It was added in 4.3. – Gary Russell Sep 21 '16 at 20:05
  • Can the `id` and `ref` be the same or do they need to be different? – Les Sep 21 '16 at 20:16
  • It can be the same because, in the chain member, it's not the full bean name, it's just part of the `myChain$child..handler` bean name. – Gary Russell Sep 21 '16 at 20:35
  • Thanks. BTW, does this mean that in SI `ref` has a different meaning than the reference to an instance of the bean? – Les Sep 21 '16 at 20:56
  • No; the meaning is the same; `ref` is a reference to a bean with that name; just like anywhere else, but there's a lot of undercover stuff. For a bean that implements `MessageHandler`, the ref is used directly (and is then the `handler`) for other types, the bean is wrapped in a framework `MessageHandler` which does any necessary message conversion. The handler is then used within a chain or, when outside a chain, wrapped in a consumer (with the consumer type depending on the input channel). – Gary Russell Sep 21 '16 at 21:27
  • So I'm using Groovy/Spock for this testing and am setting up Mock objects. In the XML file that defines the retry advice, there is a Map with the retryable exceptions (this is using ExponentialBackoffPolicy), but nothing is retried. It looks like the exception is thrown once, but nothing in the console, and no retry debug log statements. Is the Map not being used? – Les Sep 24 '16 at 00:45
  • I can't speculate what your issue is without you providing a simple example like that in my answer, which works just fine, as expected. It's not clear what you mean by "setting up Mock objects" in your test - the advice is applied during context initialization, if you want some different configuration for tests, you need to override the `retryAdvice` bean in your test case so it is applied instead of the standard bean. Although I am not very familiar with groovy or spock but I am happy to help if you can provide a sample project. – Gary Russell Sep 24 '16 at 02:07
  • We have a custom retry policy (extends SimpleRetryPolicy) defined that has an overridden `registerThrowable`, yet this never gets called. What would cause this to not happen? Spock has the built in functionality to create a 'mock' object of one used by the class under test. So I mocked the RestTemplate and the call to `exchange` to throw my custom exception - one that is supposed to be retried. In my code `exchange` is called 2 services (beans) below the method specified in the SI chain - as opposed to your example which is immediate. And no debug log statements either. – Les Sep 26 '16 at 00:20
  • Again: `I can't speculate what your issue is without you providing a simple example like that in my answer, which works just fine, as expected.` I can't even anticipate what your issue is with just a simple textual description. You need to provide an example app that clearly shows your issue. Otherwise, I shall stop responding to these comments. – Gary Russell Sep 26 '16 at 01:15