Open In App

Retry in Spring WebFlux

Last Updated : 28 May, 2024
Summarize
Comments
Improve
Suggest changes
Share
Like Article
Like
Report

The Spring WebFlux is part of the Spring Framework And It allows developers to develop nonblocking applications. In reactive programming lot of operators are available to handle to publishers and consumers. In this article, we will explain about retry operator in WebFlux with related examples. Retrying operator in Spring WebFlux is a common requirement especially when dealing with network calls or other potentially unreliable operations. Spring WebFlux provides a flexible way to implement retry logic using the retryWhen operator along with retry from the reactor.retry package.

In reactive programming, retrying an operator means resubscribing to the source publisher when an error occurs. The retryWhen operator is a powerful tool provided by Project Reactor to achieve this. It Allows you to specify when and how to retry based on the type of error and the retry strategy.

Using Retry with Different Strategies

  • Basic Retry
  • Exponential Backoff
  • Retry with Jitter
  • Advanced Retry Configuration
  • Handling Specific Exceptions
  • Logging Retry Attempts
  • Putting It All Together

1. Basic Retry:

A simple retry mechanism can retry a fixed number of times

import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

Mono<String> unreliableOperation = Mono.fromCallable(() -> {
// Simulate a failure
if (Math.random() < 0.5) {
throw new RuntimeException("Simulated error");
}
return "Success";
});

unreliableOperation
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1)))
.subscribe(System.out::println, System.err::println);


2. Exponential Backoff:

Exponential backoff gradually increases the delay between retries which is useful to prevent overwhelming a resource

import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

Mono<String> unreliableOperation = Mono.fromCallable(() -> {
// Simulate a failure
if (Math.random() < 0.5) {
throw new RuntimeException("Simulated error");
}
return "Success";
});

unreliableOperation
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
.subscribe(System.out::println, System.err::println);


3. Retry with Jitter:

Adding jitter introduces randomness to the backoff delay which can help avoid retry storms in distributed systems

import reactor.util.retry.Retry;
import java.time.Duration;

Retry jitterRetry = Retry.backoff(3, Duration.ofSeconds(1))
.jitter(0.5); // Jitter up to 50% of the delay

Mono<String> unreliableOperation = Mono.fromCallable(() -> {
if (Math.random() < 0.5) {
throw new RuntimeException("Simulated error");
}
return "Success";
});

unreliableOperation
.retryWhen(jitterRetry)
.subscribe(System.out::println, System.err::println);


4. Retry Indefinitely:

You can configure the retry to continue indefinitely

import reactor.util.retry.Retry;

Retry indefiniteRetry = Retry.indefinitely()
.filter(throwable -> throwable instanceof RuntimeException);

Mono<String> unreliableOperation = Mono.fromCallable(() -> {
if (Math.random() < 0.5) {
throw new RuntimeException("Simulated error");
}
return "Success";
});

unreliableOperation
.retryWhen(indefiniteRetry)
.subscribe(System.out::println, System.err::println);


5. Handling Specific Exceptions:

You can filter which exceptions should trigger a retry

import reactor.util.retry.Retry;

Retry specificRetry = Retry.fixedDelay(3, Duration.ofSeconds(1))
.filter(throwable -> throwable instanceof RuntimeException);

Mono<String> unreliableOperation = Mono.fromCallable(() -> {
if (Math.random() < 0.5) {
throw new RuntimeException("Simulated error");
}
return "Success";
});

unreliableOperation
.retryWhen(specificRetry)
.subscribe(System.out::println, System.err::println);


6. Logging Retry Attempts:

It's often useful to log retry attempts to monitor and debug issues

import reactor.util.retry.Retry;
import reactor.util.retry.RetrySignal;

Retry loggingRetry = Retry.backoff(3, Duration.ofSeconds(1))
.doBeforeRetry(retrySignal -> {
System.out.println("Retrying due to: " + retrySignal.failure().getMessage());
});

Mono<String> unreliableOperation = Mono.fromCallable(() -> {
if (Math.random() < 0.5) {
throw new RuntimeException("Simulated error");
}
return "Success";
});

unreliableOperation
.retryWhen(loggingRetry)
.subscribe(System.out::println, System.err::println);


7. Putting It All Together:

Here is a complete example of a Spring WebFlux service with advanced retry configuration, logging, and specific exception handling.

package com.example.demo.service;

import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

import java.time.Duration;

@Service
public class RetryService {

public Mono<String> unreliableOperation() {
return Mono.fromCallable(() -> {
if (Math.random() < 0.5) {
throw new RuntimeException("Simulated error");
}
return "Success";
})
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.filter(throwable -> throwable instanceof RuntimeException)
.doBeforeRetry(retrySignal -> {
System.out.println("Retrying due to: " + retrySignal.failure().getMessage());
})
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> retrySignal.failure()));
}
}

In this example,

  • The operation is retried up to 3 times with exponential backoff.
  • Retries are only performed for RuntimeException.
  • Each retry attempt is logged.
  • After exhausting all retries, the original exception is thrown.

Prerequisites:

You should have knowledge below listed topics to understand this article.

  • Spring Framework
  • Spring WebFlux
  • Publisher and Consumer
  • Operators in Spring reactor
  • Maven
  • Events Flow and Subscription

Tools & Technologies:

  • Spring Framework
  • Spring Reactive programming
  • Spring Tool Suite
  • Project Type: Maven

Steps to Implement Retry in Spring WebFlux

Here we created one Simple Spring Reactive Project. Below we provide in detailed Explanation with examples for a better understanding of the concept.

Project folder Structure:

Folder Structure


Step 1:

Create a Spring Stater project with required dependencies. Below, we provide those dependencies that we used in that spring project.

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>


Step 2:

After this create one Service class in the main package by using @Service annotation. In this class, we create a method called unreliableOperation by using Mono publisher with return type String. In the unreliableOperation method, we define the logic for Creating a service that simulates a potentially failing operation, such as an external API call.

Java
package com.app;

import java.time.Duration;

import org.springframework.stereotype.Service;

import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

@Service
public class RetryService {

    public Mono<String> unreliableOperation() {
        return Mono.fromCallable(() -> {
            // Simulate a failure
            if (Math.random() < 0.5) {
                throw new RuntimeException("Simulated error");
            }
            return "Success";
        })
        .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
            .filter(throwable -> throwable instanceof RuntimeException)
            .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> retrySignal.failure()));
    }
}


In this class, if the error is happened again the retry operator tries to subscribe to the Mono publisher every 3 seconds.

Step 3:

Now we create a controller class in the project main package to define the API endpoints with the Help of @RestController. Here we create an API method called retryEndpoint(). In this method, we just retrieve the unreliableOperation() by using the RetryService class object.

Java
package com.app;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api")
public class RetryController {

    private final RetryService retryService;

    public RetryController(RetryService retryService) {
        this.retryService = retryService;
    }

    @GetMapping("/retry")
    public Mono<String> retryEndpoint() {
        return retryService.unreliableOperation();
    }
}


Here we created a GET-type API request.

Step 4:

Now run this project as Spring Boot App. And this project runs on Netty Server with an 8080 port number by default.

Application Running


Step 5:

Now test the API. Here we use the Postman tool for testing the API

API testing
output


Advantages of Retry Operator:

  • Improved Resilience
  • Granular Control
  • Flexible Retry Strategies
  • Declarative and Composable
  • Error Handling and Recovery
  • Non-Blocking Nature
  • Monitoring and Logging
  • Customization for Specific Use Cases
  • Integration with Reactor Context

Next Article

Similar Reads