TSM - Spring Retry: Cum să gestionezi eșecurile temporare ca un profesionist!

Bianca Moga - Software Developer

De fiecare dată când se execută un request, există șansa ca acesta să eșueze - fie din cauza unei conexiuni instabile, fie din cauza unui server temporar indisponibil. În astfel de situații, o strategie comună este să reîncercăm operațiunea, oferind aplicației noastre șansa de a depăși erorile temporare. Dar cum gestionăm acest proces fără să scriem cod redundant sau să pierdem din claritatea aplicației? Aici intervine Spring Retry, o bibliotecă puternică și flexibilă, integrată perfect în ecosistemul Spring Boot, care automatizează și simplifică reîncercarea operațiunilor eșuate.

Cum folosim Spring Retry

Mai întâi avem nevoie de următoarele dependențe în fișierul nostru pom.xml:

<dependency>
    <groupId>
     org.springframework.retry
   </groupId>
   <artifactId>
     spring-retry
   </artifactId>
   <version>2.0.3</version>
</dependency>
<dependency>
   <groupId>
     org.springframework
   </groupId>
   <artifactId>
     spring-aspects
   </artifactId>
    <version>6.1.5</version>
</dependency>

De asemenea, vom adăuga @EnableRetry în clasa de configurări ce conține deja @Configuration, pentru a crea un proxy din clasele care au metode de tip retryable.

@EnableRetry
public class YourApplication {
  public static void main(
   String[] args) {

  SpringApplication
 .run(YourApplication.class, args);
   }
}

De obicei, folosim \@Retryable atunci când facem un apel la un serviciu care poate fi indisponibil la un anumit moment și dorim să încercăm din nou să ne conectăm la acesta după o perioadă.

Exemplu:

@Retryable(
  retryFor = {
   ConnectException.class,   
   ResourceAccessException.class
  },
  maxAttempts = 5, 
  backoff = @Backoff(value = 1000, 
    multiplier = 2))

  public void callAPI(String arg) 
   throws ConnectException, 
   ResourceAccessException {
   //cod pentru retry...
 }

Putem configura următorii parametri:

Odată ce s-a executat numărul maxim de încercări, putem să ne revenim din eroarea întâlnită prin crearea unei metode de recuperare, marcată cu @Recover care va fi apelată la final. Rețineți că, dacă o încercare este reușită, nu se va mai apela următoarea încercare și nu se va apela metoda de recuperare.

Exemplu:

@Recover
public void recover(){
//cod pentru recuperare
}

Când folosim Spring Retry

  1. Eșecuri legate de rețea

  2. Folosirea brokerul de mesaje

  3. Blocări ale resurselor

  4. Acces la resurse externe

  5. Indisponibilitatea serviciilor apelate

  6. Procesare asincronă

  7. Eșecuri ale dependințelor externe

  8. Eșecuri în descoperirea serviciilor

  9. Indisponibilitatea temporară a cache-ului

RetryTemplate

RetryTemplate oferă o abordare programatică pentru implementarea operațiunii retry. Este mai flexibil și poate fi utilizat oriunde în cod, inclusiv în clase care nu sunt gestionate de Spring. Necesită să creezi manual instanța de RetryTemplate și să configurezi comportamentul său (maxAttempt, backoff, excepții etc.).

Exemplu:

RetryTemplate retryTemplate = RetryTemplate.builder()
 .maxAttempts(5)    // Maxim 5 încercări
 .fixedBackoff(1000)  // Pauză de 1 secundă între   
                    // încercări
 .retryOn(RuntimeException.class)  
 .build();

String result = retryTemplate.execute(context -> {
  System.out.println("Execut operația... Încercare: " 
   + context.getRetryCount());

  if (context.getRetryCount() < 4) {
    throw new RuntimeException("Eroare temporară");
  }
  return "Operație finalizată cu succes!";
});
System.out.println(result);

În exemplul de mai sus avem un număr maxim de încercări(maxAttempts(5)), ceea ce înseamnă că operația va fi încercată de maximum 5 ori: 1 încercare inițială + 4 operațiuni retry. Dacă operația continuă să eșueze după 5 încercări, se aruncă o excepție. Prin configurarea (fixedBackoff(1000)), se introduce o pauză de 1 secundă între încercări. Operațiunea retry pe RuntimeException (retryOn(RuntimeException.class)) va face următoarele: operațiunea retry se va declanșa doar dacă este aruncată o excepție de tip RuntimeException, iar orice altă excepție (sau finalizarea cu succes) va încheia procesul.

Dacă și a cincea încercare aruncă un RuntimeException, RetryTemplate nu mai face alte operațiuni retry și va arunca excepția către apelant. Rezultatul în acest caz ar fi o eroare și stack trace-ul excepției.

Retry vs RetryTemplate

Caracteristică @Retry RetryTemplate
Tip de implementare Declarativă Programatică
Activare Necesită \@EnableRetry și un bean Poate fi utilizat oriunde în cod
Spring
Ușurință în utilizare Simplu de folosit pentru metode de Mai configurabil, dar necesită mai mult
retry cod
Flexibilitate Limitat la metodele gestionate de Spring Poate fi folosit în clase non-Spring
Fallback ( @Recover) Suport direct cu o metodă specifică. Necesită gestionare manuală a
fallback-ului
Scenarii tipice Apeluri service/metode gestionate de Operații complexe sau apeluri
Spring non-Spring

Pe scurt, \@Retry este mai simplu, dar mai puțin flexibil, iar RetryTemplate este mai versatil, dar necesită cod suplimentar.

Concluzie

Retry este în general util când operațiunea este probabil să reușească dacă este încercată din nou după o scurtă întârziere. Totuși, retry-ul excesiv poate duce la epuizarea resurselor și la probleme de performanță. Configurarea corectă a limitelor de retry, a întârzierilor și a gestionării excepțiilor este crucială pentru a asigura că mecanismul de retry nu duce la impact negativ asupra sistemului.

Referințe:

  1. https://docs.spring.io/spring-batch/docs/1.0.x/spring-batch-docs/reference/html/ch06.html

  2. https://medium.com/\@virendra-oswal/retrying-and-recovery-via-spring-boot-using-spring-retry-91b590ef60f9

  3. https://www.baeldung.com/spring-retry

  4. https://medium.com/\@ankithahjpgowda/spring-retry-overview-6674b06a3235