Conditional repeat or retry on Mono with webclient from Spring WebFlux

Bernado picture Bernado · Apr 30, 2019 · Viewed 12.4k times · Source

What i want to do is a conditional repeat on a Mono in Webflux with webclient.The Situation is the following:

We have some business rest service service that returns a generated document. the generation of this document is triggered from another service that gets called before this one. But, back to business: the document generation service needs from 10-30 seconds. What we want to do is: check after 10 seconds if document (Mono) is generated. If so, all is fine. If not, repeat (or retry) after another 5 seconds and check if document is generated. And so on until (worst case) a timeout after 30 seconds. Is this possible? Some (pseudo) code:

return this.webClient.post().uri(SERVICE_URL)).        
body(BodyInserters.fromObject(docRequest)).retrieve().
bodyToMono(Document.class).
delaySubscription(Duration.ofSeconds(10)).
repeat5TimesWithDynamicTimeDelayUntil(!document.isEmpty()).
subscribe();

Greetz Bernardo

Answer

Phil Clay picture Phil Clay · May 1, 2019

Yes, it is possible.

Mono has two concepts for re-subscribing (and thus, re-triggering the request)

  • retry = re-subscribe if the upstream completed with an exception
  • repeat = re-subscribe if the upstream completed successfully

Each concept has multiple overloaded methods on Mono for different use cases. Look for the retry* and repeat* methods. For example, to retry a maximum number of times with no delay, use retry(int numRetries).

More complex use cases are supported via the retryWhen and repeatWhen methods, as shown in the following examples.

retryWhen

To retry if the mono completed with an exception a maximum of 5 times with 5 seconds between each attempt:

// From reactor-core >= v3.3.4.RELEASE
import reactor.util.retry.Retry;

this.webClient
        .post()
        .uri(SERVICE_URL)
        .body(BodyInserters.fromValue(docRequest))
        .retrieve()
        .bodyToMono(Document.class)
        .retryWhen(Retry.fixedDelay(5, Duration.ofSeconds(5)))
        .delaySubscription(Duration.ofSeconds(10))

The retry builder supports other backoff strategies (e.g. exponential) and other options to fully customize retries.

Note the retryWhen(Retry) method used above was added in reactor-core v3.3.4.RELEASE, and the retryWhen(Function) method was deprecated. Prior to reactor-core v3.3.4.RELEASE, you could use the retry function builder from reactor-extras project to create a Function to pass to retryWhen(Function).

repeatWhen

If you need to repeat on success, then use .repeatWhen or .repeatWhenEmpty instead of .retryWhen above.

Use the repeat function builder from reactor-extras project to create the repeat Function as follows:

// From reactor-extras
import reactor.retry.Repeat;

this.webClient
        .post()
        .uri(SERVICE_URL)
        .body(BodyInserters.fromValue(docRequest))
        .retrieve()
        .bodyToMono(Document.class)
        .filter(document -> !document.isEmpty())
        .repeatWhenEmpty(Repeat.onlyIf(repeatContext -> true)
                .exponentialBackoff(Duration.ofSeconds(5), Duration.ofSeconds(10))
                .timeout(Duration.ofSeconds(30)))
        .delaySubscription(Duration.ofSeconds(10))

You can also chain a .retry* with a .repeat* if you want to re-subscribe on both success or failure.