0

I have a Spring Boot application that has a JMS Publisher. The publisher used retry logic that was in the publisher class that worked fine. But I want to change that to the Spring Retry Template and put it into the connection factory configuration.

I am having trouble figuring out how to call add the RetryTemplate into the Publisher class.

This is the Configuration class:

@Configuration
@EnableRetry
public class PurchasedTransServicePublisherConfig {

    @Value("${java.naming.factory.initial.publisher}")
    private String context;

    @Value("${java.naming.provider.url.publisher}")
    private String providerURL;

    @Value("${fedex.jms.LDAP.entryName.publisher}")
    private String ldapEntryName;

    private @Value("${jms.username.publisher:#{null}}") String jmsUserName;
    private @Value("${jms.password.publisher:#{null}}") String jmsPassword;

    @Value("${jms.destinationname.publisher}")
    private String destinationName;

    private static final Logger LOGGER = LoggerFactory.getLogger(PurchasedTransServicePublisherConfig.class);

    @Autowired(required = false)
    FxgCipherInitializer jmsParams;

    @Bean
    public JmsTemplate publisherJmsTemplate(final ConnectionFactory publisherConnectionFactory) {
        final JmsTemplate jmsTemplate = new JmsTemplate();
        jmsTemplate.setConnectionFactory(publisherConnectionFactory);
        jmsTemplate.setPubSubDomain(true);
        jmsTemplate.setDefaultDestinationName(destinationName);
        jmsTemplate.setSessionTransacted(true);
        return jmsTemplate;
    }

    @Bean
    public ConnectionFactory publisherConnectionFactory(final JndiTemplate publisherJndiTemplate) throws NamingException {
        final ConnectionFactory connectionFactory = (ConnectionFactory) publisherJndiTemplate.getContext().lookup(ldapEntryName);
        final UserCredentialsConnectionFactoryAdapter ucf = new UserCredentialsConnectionFactoryAdapter();
        ucf.setUsername(((null != jmsParams) ? jmsParams.getUsername() : jmsUserName));
        ucf.setPassword((null != jmsParams) ? jmsParams.getPassword() : jmsPassword);
        ucf.setTargetConnectionFactory(connectionFactory);
        return ucf;
    }

    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000l);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(2);
        retryTemplate.setRetryPolicy(retryPolicy);
        return retryTemplate;
    }


    @Bean
    public JndiTemplate publisherJndiTemplate() {
        final JndiTemplate jndiTemplate = new JndiTemplate();
        final Properties jndiProps = new Properties();
        jndiProps.setProperty(Context.INITIAL_CONTEXT_FACTORY, context);
        jndiProps.setProperty(Context.PROVIDER_URL, providerURL);
        jndiTemplate.setEnvironment(jndiProps);
        return jndiTemplate;
    }  
}

The only change between the working configuration and the RetryTemplate configuration is the addition of the annotation @EnableRetry and the method retryTemplate

The publisher class originally successfully retried the send messsage using this logic:

 private void send(final MessageCreator messageCreator) throws JMSException {
        int sendAttempts = 0;

        while (true) {
            try {
                jmsTemplate.send(messageCreator);
                LOGGER.info("Message Successfully Published");
                break;
            }  catch (RuntimeException e) {
                LOGGER.error("Caught Runtime Exception: {}", e.getMessage());
                sendAttempts++;
                handleJmsExceptionRetry(e, sendAttempts);
            }
        }
    }

I tried to implement the retry template like this:

 private void send(final MessageCreator messageCreator) throws JMSException {
        while (true) {
            try {
                publisherJmsTemplate.send(messageCreator);
                LOGGER.info("Message Successfully Published");
                break;
            }  catch (RuntimeException e) {
                LOGGER.error("Caught Runtime Exception: {}", e.getMessage()); 
                publisherRetryTemplate.execute(arg0 -> {
                    publisherJmsTemplate.send(messageCreator);
                    return null;
                });
            }
        }
    }

The test method that I created to unit test this is below:

 @Test
    public void testPublishTmsTrip_WhenPublishFailsMultipleTimes() {
        Mockito.doThrow(RuntimeException.class).when(mockJmsTemplate).send(mockMessageCreator);
        boolean testBoolean = tmsTripPublisher.publishTmsTripMessageEvent("TEST message");
        assertFalse(testBoolean);
    }

The problem is when it gets to the publisherRetryTemplate.execute..., it does not execute the RetryTemplate method. Any guidance into how to implement this retry logic would be greatly appreciated.

Gloria Santin
  • 2,066
  • 3
  • 51
  • 124
  • You don't need a try-catch block to retry a call when an exception is thrown. Just use publisherRetryTemplate.execute(arg0 -> { publisherJmsTemplate.send(messageCreator); return null; }); – Chamin Wickramarathna Jul 30 '19 at 07:09

3 Answers3

0

You don't need @EnableRetry unless you are using @Retryable on a method.

It's not clear why you have while (true) there, or why you initially call outside of the retry template's execute method.

Why not just

private void send(final MessageCreator messageCreator) throws JMSException {
    publisherRetryTemplate.execute(arg0 -> {
         publisherJmsTemplate.send(messageCreator);
         return null;
     });
}

?

It's not clear what you mean by "it does not execute the RetryTemplate method".

Gary Russell
  • 166,535
  • 14
  • 146
  • 179
0

You don't need to have a try-catch block to retry the request. Retry template itself retry the request as it configured(2 retries with 2000ms timeout).

Just use

private boolean send(final MessageCreator messageCreator) {       
   try {
       publisherRetryTemplate.execute(arg0 -> {
          publisherJmsTemplate.send(messageCreator);
          LOGGER.info("Message Successfully Published");
          return null;
       });
   } catch (Exception e) {
       LOGGER.error("Caught Exception: {}", e);
       // Error handling logic here
       return false;
   }
}
Chamin Wickramarathna
  • 1,672
  • 2
  • 20
  • 34
0

It looks like you mixed declarative approach with the programmatic one. You don't need the @EnableRetry in your configuration class as long as you don't use the methods annotated with the @Retryable and @Recover.

If you use programmatic retries with the RetryTemplate bean, you don't need to call the RetryTemplate.execute() method on exceptions in the catch block. While defining the RetryTemplate bean, you just need to specify the exceptions, which occurrence will start new operation specified in the execute() callback.

For example, if you want to retry the operation on the RuntimeException, but don't want to do it on some you CustomException, you have to specify it in the policy map:

new ExceptionClassifierRetryPolicy().
        setPolicyMap(ImmutableMap.<Class<? extends Throwable>, RetryPolicy> builder()
            .put(CustomException.class, new NeverRetryPolicy())
            .put(RuntimeException.class, new SimpleRetryPolicy(MAX_RETRY_ATTEMPTS))
            .build());
androberz
  • 744
  • 10
  • 25