Política de reintentos con Spring Retry

0
11029

Política de reintentos con Spring-retry

Índice de contenidos

1. Introducción
2. Entorno
3. Solución Retry template
4. Solución AOP
5. Conclusiones
6. Referencias

1. Introducción

Definir una política de reintentos es una necesidad bastante frecuente en aplicaciones empresariales. La integración con un sistema externo, atacar a servicios web, el envío a colas JMS u operaciones con base de datos
son algunos ejemplos de causas que pueden necesitar la implementación de una política de reintentos.

Implementar una política de reintentos personalizada es la solución más solicitada en la mayoría de aplicaciones. Un ejemplo de ello lo podemos encontrar aquí.
Pero al final estas soluciones no dejan de ser implementaciones específicas de las aplicaciones, y no suponen una solución estandarizada a los reintentos de operaciones en aplicaciones.

Spring-retry es un módulo de Spring que hasta la versión 2.2.0 estaba integrado dentro de spring-batch, y que ahora se define como componente independiente dentro de la jerarquía de Spring. Dicho módulo nos ofrece una solución bastante sencilla de implementar e integrar políticas de reintentos en nuestras aplicaciones JEE. Ofrece una gran versatilidad de configuraciones, pudiendo decidir el número de reintentos por operación, timeouts, estados entre reintentos, etc.
Además, como veremos más adelante, se integra de forma muy sencilla con nuestras aplicaciones Spring, y se puede externalizar su configuración si usamos la solución con AOP, al estar integrada en los propios archivos de configuración de contexto de Spring.

Elementos esenciales en Spring-retry

Cuando necesitamos realizar una operación que exija definir una política de reintentos cuando la operación falle, como puede ser por ejemplo reintentar la consulta a un servicio REST, o reintentar el envío de un correo electrónico,
tendremos que automatizarla con la gestión de una serie de elementos básicos de Spring-retry. Dichos elementos son:

  • Interfaz RetryOperations. Tiene la siguiente estructura:
    
    public interface RetryOperations {
    
    <T> T execute(RetryCallback<T> retryCallback) throws Exception;
    
    <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback)
        throws Exception;
    
    <T> T execute(RetryCallback<T> retryCallback, RetryState retryState)
        throws Exception, ExhaustedRetryException;
    
    <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback,
        RetryState retryState) throws Exception;
    
    }
    
                

    Como podemos observar, se definen cuatro métodos execute, que se ejecutarán cuando el método al que ‘escuchan’ lanza una excepción. Los métodos de RetryOperations reciben los siguientes parámetros:

  • Callback: Ejecución de reintento. Puede ser de dos tipos:
    • RetryCallBack: Callback de reintento (cuando se ha ejecutado un reintento). Se define para la ejecución de un post procesamiento cuando se realiza un reintento (después de que el método observado lance un java.lang.Exception)
    • RecoveryCallBack: Callback de recuperación (cuando se han terminado los reintentos). Se define para la ejecución de un post procesamiento cuando se agotan los intentos (dependiendo de la política definida).
  • RetryState: Algunas políticas de reintentos requieren que se guarde un estado entre intentos (por ejemplo, cuando tenemos variables que cambian entre la ejecución del método, y necesitamos almacenarlas por cualquier motivo).
    Spring-batch nos ofrece una clase RetryState para poder almacenar dicha información de estado entre ejecuciones. Para más información sobre cómo se almacena dicha información, ver el apartado de referencias

2. Entorno

  • Macbook pro core i7 con 16gb RAM
  • SO: Yosemite
  • IDE: Spring Tool Suite 3.4.0 RELEASE
  • Apache Maven 3.1.1

3. Solución Retry template

En este apartado vamos a dar una solución al siguiente problema:

– Queremos enviar un correo electrónico a un destinatario cualquiera, pero necesitamos que si el envío falla (por cualquier razón: El servidor de correo está caído, se produce un timeout en el intento de envío, etc.) se reintente un máximo de 5 veces.
Para no sobresaturar al servidor de correo, necesitamos un período de 5 segundos entre reintentos de envío.

Vamos a dar una solución con spring-retry basado en RetryTemplate. Lo primero es añadir las siguientes dependencias a nuestro proyecto Maven:


    <properties>
    <spring.version>3.2.8.RELEASE</spring.version>
        <spring.retry.version>1.1.0.RELEASE</spring.retry.version>
    </properties>

    <dependencies>

        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
            <version>${spring.retry.version}</version>
        </dependency>

        <!-- AOP -->
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-aop</artifactId>
          <version>${spring.version}</version>
          <scope>compile</scope>
        </dependency>
    
         <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-aspects</artifactId>
          <version>${spring.version}</version>
          <scope>compile</scope>
        </dependency>
    
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjrt</artifactId>
          <version>1.7.4</version>
          <scope>compile</scope>
        </dependency>


        <!-- JSR -->
        <dependency>
          <groupId>javax.annotation</groupId>
          <artifactId>jsr250-api</artifactId>
          <version>1.0</version>
          <scope>compile</scope>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

        <dependency>
          <groupId>org.mockito</groupId>
          <artifactId>mockito-all</artifactId>
          <version>1.9.5</version>
          <scope>test</scope>
        </dependency>
        
        <!-- Log -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.6.6</version>
            <scope>compile</scope>
        </dependency>
        
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.6.6</version>
            <scope>compile</scope>
        </dependency>
        
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.6.6</version>
            <scope>compile</scope>
        </dependency>
        
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
            <scope>compile</scope>
        </dependency>
        
    </dependencies>

A continuación, vamos a crear nuestro servicio de Spring que va a tener un método sendMail. Dicho método lanzará una excepción las 4 primeras veces que se ejecute, y a la quinta simulará el envío de un correo electrónico:


    public class ExampleServiceImpl implements ExampleService {

    private static final Logger LOG = LoggerFactory.getLogger(ExampleServiceImpl.class);
    
    private int times = 0;
    
    public String sendMail() throws Exception {
        
        LOG.trace("Sending mail");
        
        if(times < 4) {
            times++;
            throw new Exception("Retrying mail sending..."); 
        }
        
        LOG.trace("Mail sent!");
        
        return "OK";
    }
    
}

En el test de este servicio, vamos a definir un nuevo RetryTemplate con las siguientes características:

  • Tendrá una política de reintentos (RetryPolicy), que recibe:
    • Un entero indicando el número de reintentos, en nuestro caso 5
    • Un mapa donde se registrarán aquellas excepciones que soporta la política, es decir, comprobará la excepción que ha lanzado el método sobre el que realiza los intentos, y la comparará con las del mapa. Si coincide alguna, tratará el reintento
  • Tendrá una política de BackOff. En Spring-retry tenemos algunas políticas de BackOff, según las necesidades de negocio. Entre ellas, cabe destacar:
    • ExponentialBackOffPolicy: Define una política de reintentos exponencial (por ejemplo, intervalos de 2 segundos, con un multiplicador de 1.5 hasta alcanzar 20 segundos)
    • FixedBackOffPolicy: Política de reintentos clásica con timeout

    Se usará para este ejemplo una política FixedBackOffPolicy, que establecerá un timeout entre reintentos de 5 segundos.

  • Definirá un método RetryCallBack que se ejecutará cuando se lance una excepción en el método sobre el que realiza los intentos

El test por tanto es el siguiente:


    public class RetryTest {

    private static final Logger LOG = LoggerFactory.getLogger(RetryTest.class);

    private ExampleService service;

    private RetryTemplate retryTemplate;

    @Before
    public void init() {
        retryTemplate = new RetryTemplate();

        Map<Class<? extends Throwable>, Boolean> supportedExceptionsMap = 
            new HashMap<Class<? extends Throwable>, Boolean>();
        supportedExceptionsMap.put(Exception.class, true);
        
        //Política de reintentos
        RetryPolicy retryPolicy = new SimpleRetryPolicy(5, supportedExceptionsMap);

        //Política BackOff
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();

        //5000ms -> 5s
        backOffPolicy.setBackOffPeriod(5000);
        
        retryTemplate.setRetryPolicy(retryPolicy);
        retryTemplate.setBackOffPolicy(backOffPolicy);
        
        service = spy(new ExampleServiceImpl());
    }

    @Test
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void givenAServiceMethodWhenTryingToInvokeItThenRetry5Times() {

        try {

            LOG.trace("[TEST] Trying to invoke send mail method...");

            retryTemplate.execute(new RetryCallback() {

                public String doWithRetry(RetryContext arg0) throws Exception {
                    System.out.println(String.format("\tRetry count ->  %s ",
                            arg0.getRetryCount()));
                    return service.sendMail();
                }

            });

            verify(service, times(5)).sendMail();
            verifyNoMoreInteractions(service);
            

        } catch (Throwable e) {
            e.printStackTrace();
        }

    }

}

En el test verificamos que el método sendMail ha sido invocado 5 veces. La ejecución del mismo provoca la siguiente salida en el LOG:

2015-01-28 15:44:03,767 TRACE com.autentia.tutoriales.spring.retry.
    RetryTest.givenAServiceMethodWhenTryingToInvokeItThenRetry5Times(RetryTest.java:58)
     - [TEST] Trying to invoke send mail method...

    Retry count ->  0 

2015-01-28 15:44:03,801 TRACE com.autentia.tutoriales.spring.retry.
service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

    Retry count ->  1 

2015-01-28 15:44:08,808 TRACE com.autentia.tutoriales.spring.retry.
service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

    Retry count ->  2 

2015-01-28 15:44:13,814 TRACE com.autentia.tutoriales.spring.retry.
service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

    Retry count ->  3 

2015-01-28 15:44:18,818 TRACE com.autentia.tutoriales.spring.retry.
service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

    Retry count ->  4 

2015-01-28 15:44:23,822 TRACE com.autentia.tutoriales.spring.retry.
service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

2015-01-28 15:44:23,822 TRACE com.autentia.tutoriales.spring.retry.
service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:26) - Mail sent!


4. Solución AOP

La solución que se propone en este apartado es una variante de la anterior, teniendo en cuenta que, en lugar de usar RetryTemplate, se da una solución basada en AOP.

Para ello, definimos el siguiente archivo de configuración de spring:


<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:aop="http://www.springframework.org/schema/aop" 
    xmlns:context="http://www.springframework.org/schema/context" 
    xmlns:jee="http://www.springframework.org/schema/jee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd   
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd  
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd     
    http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">
    
    
    <context:annotation-config/>
    
    <context:component-scan base-package="com.autentia.tutoriales.spring.retry"/>
    
    
    <bean id="batchRetryPolicy" class="org.springframework.retry.policy.SimpleRetryPolicy">
        <property name="maxAttempts" value="5"/>
    </bean>

    <bean id="backOffPolicy" class="org.springframework.retry.backoff.FixedBackOffPolicy">
        <property name="backOffPeriod" value="5"/>
    </bean>

    <bean id="batchRetryTemplate" class="org.springframework.retry.support.RetryTemplate">
        <property name="retryPolicy" ref="batchRetryPolicy"/>
        <property name="backOffPolicy" ref="backOffPolicy"/>
    </bean>

    <bean id="retryAdvice"
        class="org.springframework.retry.interceptor.RetryOperationsInterceptor">
        <property name="retryOperations" ref="batchRetryTemplate"/>
    </bean>

    <aop:config>
        <aop:pointcut id="transactional"
            expression="execution(* com..*ExampleServiceImpl.sendMail(..))" />
        <aop:advisor pointcut-ref="transactional" advice-ref="retryAdvice"
            order="-1" />
    </aop:config>
    
</beans>

Como podemos observar, estamos definiendo tanto las políticas como el template con clases de soporte de Spring-retry. Además, definimos un Advice, que enlazará con la política de reintentos cuando se ejecute el pointcut definido

Definimos el test como sigue:


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring-configuration/retry-config.xml"})
public class RetryAOPTest_IT {

    private static final Logger LOG = LoggerFactory.getLogger(RetryAOPTest_IT.class);

    @Resource
    private ExampleService service;

    @Test
    public void givenAServiceMethodWhenTryingToInvokeItThenRetry5Times() {

        try {
            
            service.sendMail();
            
            assertEquals(service.getTimes(), 4);
            
        } catch(Exception e) {
            LOG.error("Error trying to send email!");
        }

    }

}

El test anterior levanta el contexto de Spring, realiza una llamada a sendMail y comprueba que ha sido ejecutado 4 veces, a través de la vairable times del servicio

El resultado de la ejecución del test anterior produce las siguientes trazas en el Log:


2015-01-28 16:54:46,293 TRACE org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:214) - RetryContext retrieved: [RetryContext: count=0, lastException=null, exhausted=false]

2015-01-28 16:54:46,293 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:258) - Retry: count=0

2015-01-28 16:54:47,649 TRACE com.autentia.tutoriales.spring.retry.service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

2015-01-28 16:54:47,656 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:293) - Checking for rethrow: count=1

2015-01-28 16:54:47,657 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:258) - Retry: count=1

2015-01-28 16:54:48,958 TRACE com.autentia.tutoriales.spring.retry.service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

2015-01-28 16:54:48,964 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:293) - Checking for rethrow: count=2

2015-01-28 16:54:48,964 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:258) - Retry: count=2

2015-01-28 16:54:49,998 TRACE com.autentia.tutoriales.spring.retry.service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

2015-01-28 16:54:50,005 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:293) - Checking for rethrow: count=3

2015-01-28 16:54:50,006 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:258) - Retry: count=3

2015-01-28 16:54:50,865 TRACE com.autentia.tutoriales.spring.retry.service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

2015-01-28 16:54:50,870 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:293) - Checking for rethrow: count=4

2015-01-28 16:54:50,871 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:258) - Retry: count=4

2015-01-28 16:54:51,782 TRACE com.autentia.tutoriales.spring.retry.service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

2015-01-28 16:54:51,782 TRACE com.autentia.tutoriales.spring.retry.service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:26) - Mail sent!

Como vemos, ha intentado ejecutar 5 veces el método, hasta que el método ha devuelto en la 5ª vez un resultado satisfactorio, por lo que ha dejado de reintentar. Si hacemos la prueba de bajar el número de reintentos a 3:


2015-01-28 17:17:53,762 TRACE org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:214) - RetryContext retrieved: [RetryContext: count=0, lastException=null, exhausted=false]

2015-01-28 17:17:53,762 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:258) - Retry: count=0

2015-01-28 17:17:57,211 TRACE com.autentia.tutoriales.spring.retry.service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

2015-01-28 17:17:57,217 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:293) - Checking for rethrow: count=1

2015-01-28 17:17:57,217 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:258) - Retry: count=1

2015-01-28 17:17:57,218 TRACE com.autentia.tutoriales.spring.retry.service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

2015-01-28 17:17:57,224 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:293) - Checking for rethrow: count=2

2015-01-28 17:17:57,224 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:258) - Retry: count=2

2015-01-28 17:17:57,224 TRACE com.autentia.tutoriales.spring.retry.service.impl.ExampleServiceImpl.sendMail(ExampleServiceImpl.java:19) - Sending mail

2015-01-28 17:17:57,225 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:293) - Checking for rethrow: count=3

2015-01-28 17:17:57,225 DEBUG org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:313) - Retry failed last attempt: count=3

2015-01-28 17:17:57,227 ERROR com.autentia.tutoriales.spring.retry.RetryAOPTest_IT.givenAServiceMethodWhenTryingToInvokeItThenRetry5Times(RetryAOPTest_IT.java:35) - Error trying to send email!

Vemos que se lanza una excepción, capturada en el test.

5. Conclusiones

Spring-retry nos ofrece una solución estándar, sencilla y rápida para la gestión de reintentos en nuestras aplicaciones JEE, proporcionando políticas muy diversas, desde un único reintento hasta la repetición exponencial.
Evita además que tengamos que implementar una política de reintentos para cada aplicación que así lo requiera.

Se puede consultar el proyecto en github aquí

6. Referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad