22

I want implement a conditional repeat on a Mono in WebFlux with WebClient. The situation is the following:

We have a rest service service that returns a generated document. The generation of this document is triggered from another service that gets called before this one. The document generation service needs between 10-30 seconds.

We want to check after 10 seconds if document (Mono<Document>) 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();
Honza Zidek
  • 9,204
  • 4
  • 72
  • 118
Bernado
  • 548
  • 1
  • 6
  • 18

1 Answers1

44

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.

Phil Clay
  • 4,028
  • 17
  • 22
  • Well, i tried it and until one thing it works fine. It seems that delaySubscription is executed again in the repeat. So i am waiting 20secs until the backoff comes into game. Could that be? Will be pasting my code a little later.... – Bernado May 01 '19 at 11:47
  • Edited to move delaySubscription to after the retry/repeat so that the delay is not executed on each retry/repeat – Phil Clay May 01 '19 at 13:06
  • Ok will try it again tomorrow and will give feedback. Thank you. – Bernado May 01 '19 at 13:11
  • It works fine, thank you. One thing seems to be strange. If I would have configured the repeat handling the way that the time is shorter than there is a result from the Service (e.g. repeat.4times.a.4.secs.thenend and the service takes 20secs) the whole result will be a null value although theres a block() at the end....can someone explain that? – Bernado May 03 '19 at 10:12
  • 1
    `.block()` will return null if the Mono completes empty, which is what happens if you're doing something similar to my second example. The `Mono` will complete empty because the `.filter` filters out the value, and the `repeatWhenEmpty` condition no longer applies (i.e. it has timed out). – Phil Clay May 04 '19 at 16:06
  • Is there a way to log that the retry is occurring? – wild_nothing Jul 02 '19 at 12:08
  • 1
    @wild_nothing use `onErrorMap` – Mister_Jesus Aug 27 '19 at 12:34
  • how to execute something when max retry has been reached? – user1955934 Oct 07 '19 at 08:56
  • Is it possible to throw an exception or log when `retryMax` or `timeout` met? How can we do that. thanks – jeevjyot singh chhabda Apr 16 '20 at 04:27
  • Can I use retryWhen when invoking some endpoint returns nothing/empty list? I want to use retryWhen in case invoking my endpoint doesn't return anything (Mono.empty). My question is if in such a case retryWhen will be invoked too – Matley Jun 29 '20 at 13:03
  • @jeevjyotsinghchhabda Yes. See RetrySpec.onRetryExhaustedThrow – Phil Clay Jun 29 '20 at 16:12
  • 1
    @Matley retry* only applies when an exception occurs. To resubscribe when a Mono completes empty, use Mono.repeatWhenEmpty – Phil Clay Jun 29 '20 at 16:14
  • @Phil Clay thank you for your response:) Can I have another question? I have a long chain of flatMap() etc.. Can I tell Webflux to repeat only some particular flatMap with calling my endpoint? My topic: https://stackoverflow.com/questions/62641880/webflux-repeatwhenempty-or-retrywhen – Matley Jun 29 '20 at 16:22
  • Is there any way to `log` when `retryWhen` is called & for how many times? – I'm_Pratik Dec 13 '21 at 12:00
  • @Phil Clay Is there a way to long poll using block as well? I am trying to avoid making a for loop with web calls until I get the desired response – Georgian Benetatos Apr 28 '22 at 13:23
  • @GeorgianBenetatos Yes, use the retry or repeat operators as described in the answer and then block at the end. – Phil Clay Apr 28 '22 at 16:24