Patrón Retry en los microservicios. (+Ejemplo Spring Retry)

Las llamadas de un servicio a otro en una arquitectura de microservicios son comunes, así como frecuentes pueden ser los errores que pueden tener lugar en la comunicación.

Ocasionalmente tienen lugar errores temporales por diversas causas como errores temporales de red, instancias saturadas, tiempos de respuesta excedidos, entre otros.

Cuando se produce este tipo de situaciones se afecta la calidad de servicio al no entregar la respuesta esperada por errores de muy poca duración, como solución a este tipo de situaciones se requiere poner en práctica una política de reintentos, que permita volver a realizar la solicitud en un número determinado de intentos para buscar la respuesta deseada. Esta práctica es el patrón Retry.

El patrón de reintentos tiene como objetivo lograr la estabilidad del sistema y trata de lograr el éxito de una llamada a otra, tratando que el sistema haga lo mejor que pueda para responder adecuadamente.

Cuando se realiza una petición a una instancia, y esta falla por estar ocupada temporalmente (503), como caso particular el reintento debe realizarse en un período espaciado de tiempo, pues los reintentos continuos pueden traer consigo que la instancia se mantenga ocupada más tiempo (respondiendo que está ocupada) y no pueda procesar las nuevas solicitudes.

Un esquema ilustrativo de la operación de reintentos puede verse en la figura siguiente.

El patrón de reintentos debe usarse cuando se interactua con sistemas externos que pueden llegar a un estado de fallos temporales y afectar la estabilidad de la plataforma.

Algunos ejemplos de escenarios de uso son:

  • Llamadas a servicios de envíos de correos electrónico (por ej: SMTP).
  • Llamadas Servicio/Servicio usando HTTP.
  • Envío usando protocolos de mensajería (MQTT, XMPP, etc).
  • Conexiones a bases de datos.
  • Acceso remoto a servidores de archivos (SMB, FTP, FTPS, etc)

Spring Retry

Existen variadas tecnologías para implementar políticas de reintentos, Resiliency4j-retry y Spring Retry son a nuestra consideración las más completas en el ecosistema Java.

Spring Retry en particular es una solución que hasta hace poco tiempo era parte de Spring Batch, ahora es un componente independiente disponible para todo el ecosistema Java y que auxiliado en aspectos, permite implementar completas políticas de reintentos.

Ejemplo práctico de una política de reintentos con Spring Retry

Ahora veremos un ejemplo de Spring Retry y explicaremos algunos elementos, este ejemplo es solo una introducción, no explica el concepto de templates que posibilita Spring Retry.

El ejemplo consiste en un proyecto básico de Spring, con un controlador y un servicio, en el servicio se implementa un método que falla, y con la política de reintentos creada se «intentará» la ejecución del método nuevamente, cuando se alcance el número de intentos y el método que simula errores continúe fallando entonces se tomará una acción secundaria para dar una respuesta por defecto y así garantizar la disponibilidad del sistema.

Vamos a crear un proyecto Spring Boot, le agregaremos el starter web (solo para el ejemplo porque vamos a crear un endpoint rest de pruebas, pero en la práctica no es requerido como dependencia), y agregaremos las siguientes dependencias que si son mandatorias del proyecto.

 <dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.5.RELEASE</version>
 </dependency>

 <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aspects</artifactId>
 </dependency>

Creemos ahora un interfaz para un servicio y su implementación de ejemplo:

@Service
public interface SampleService {
    String retryService(String s) throws RuntimeException;
    String retryExampleRecovery(RuntimeException t, String s);
}
@Service
public class SampleServiceImpl implements SampleService {
    private int intentos = 0;
    
    @Retryable(value = {RuntimeException.class},
            maxAttempts = 4,
            backoff = @Backoff(multiplier = 2))
    @Override
    public String retryService(String s) throws RuntimeException {
        System.out.println("Intento # " + (intentos++) + "" + Instant.now());
        if (s.equals("error")) {
            throw new RuntimeException("Error llamando a retryService ");
        }
        System.out.println("Acción realizada");
        return "OK";
    }

    @Recover
    @Override
    public String retryExampleRecovery(RuntimeException t, String s) {
        System.out.println(String.format("Recuperando a - %s", t.getMessage()));
        System.out.println("Recuperada la llamada al servicio que falla");
        return "OK (Recuperado)";
    }
}

Expliquemos la implementación del servicio, por partes.

Inicialmente implementa la interfaz y vemos que el método retryService está anotado con @Retryable, esta anotación tiene toda la magia.

@Retryable(value = {RuntimeException.class},
            maxAttempts = 4,
            backoff = @Backoff(multiplier = 2)
          )

La anotación Retryable posee los parámetros:

  • value: Indica las excepciones (tipos) para las cuales el método anotado va aplicar reintentos, este caso los reintentos tendrán lugar cuando tengan lugar excepciones del tipo RuntimeException.
  • maxAttempts: Número máximo de reintentos que se van a aplicar.
  • backoff: Define la forma en que el reintento puede aplicarse. La forma del reintento o política puede ser configurada a través de la anotación derivada @Backoff.

La anotación backoff por su parte en el ejemplo tiene el atributo multiplier en valor 2, lo que significa que ese va a ser el factor de multiplicación que se usará entre reintento y reintento, para este caso particular se intentará en los segundos al pasar 1s, luego 2s, luego 4s, etc, hasta cumplirse el número de reintentos establecidos en maxAttemps. Esta anotación incluye el atributo delay, que se usa para reintentar en cada ciertos períodos de tiempo fijos, sin necesidad del factor de multiplicación.

Otra parte interesante de este servicio es el método retryExampleRecovery, vean está anotado como @Recover, y que además recibe como parámetro un objeto del tipo de la excepción que él es capaz de recuperar. Este método será el que se invocará cuando la política de reintento se aplique y aún así no se logre recuperar el método anotado con @Retryable.

Descripción del funcionamiento del ejemplo

En el ejemplo anterior, cuando se haga una invocación a retryService y se le pase una cadena cualquiera diferente de «error», el sistema devolverá «OK». En cambio si se le pasa la cadena «error», entonces se lanzará un RuntimeException, esta excepción internamente será capturada (usando AOP) y al coincidir con el value de Retryable, se aplicará la política de reintento, en este caso se intentará 4 veces. Luego de fallar todas las veces se invocará a Recover, y se devolverá «OK (Recuperado)».

Se han agregado al ejemplo algunos «print» para ir imprimiendo por salida estándar e ir viendo como funciona Retry, en la práctica está claro que esto no es necesario.

Es importante recalcar que en el método retryExampleRecovery es donde se realizará la operación que puede fallar y sobre la que vale la pena reintentar, el caso más común sería una llamada servicio/servicio usando Http/Rest.

Complementa este ejemplo un controlador REST que inyecta el servicio y expone un API para probar.

@RestController
public class RetryController {

    @Autowired
    private SampleService sampleService;

    @GetMapping("/prueba/{msg}")
    public ResponseEntity<String> callService(@PathVariable("msg") String msg)  {
          return ResponseEntity.ok(sampleService.retryService(msg));
    }
}

Es importante tener en cuenta que debemos habilitar los reintentos en la aplicación, basta con anotar @EnableRetry en el punto de arranque del nuestra aplicación.

@SpringBootApplication
@EnableRetry
public class RetryApplication {

    public static void main(String[] args) {
        SpringApplication.run(RetryApplication.class, args);
    }

}

Dos consejos importantes

Antes de concluir quisiera dejarles dos consejos importantes:

  • Usar siempre customExceptions, para el manejo de una política de reintentos.
  • La política de reintentos que tenga en el @backoff un multiplicador. El empleo del multiplicador ayudará a mayor espacio de tiempo entre las invocaciones, dando más posibilidades de recuperación en el servicio que está fallando.

Espero te sea útil el artículo y comiences implementar políticas de reintento, esto aporta resiliencia y estabilidad a la plataforma.

4 comentarios en «Patrón Retry en los microservicios. (+Ejemplo Spring Retry)»

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

SACAViX Tech