2

I'm calling a service class method using the transformDeferred method:

public Mono<SPathResponse> getPath(SPathRequest request) {
        return pathService.getPath(request)
                .transformDeferred(RetryOperator.of(retry));
}

My retry configuration:

Retry retry = Retry.of("es", RetryConfig.custom()
            .maxAttempts(3)
            .waitDuration(Duration.of(30, ChronoUnit.MILLIS))
            .writableStackTraceEnabled(true)
            .failAfterMaxAttempts(true)
            .retryOnException(throwable -> throwable instanceof RuntimeException)
            .build());

and the test method:

@Test
    void shouldRetry() {
        BDDMockito.given(pathService.getPath(any(SPathRequest.class)))
                .willReturn(Mono.error(RuntimeException::new))
                .willReturn(Mono.just(SPathResponse.builder().build()));
        cachedPathService.getPath(SPathRequest.builder()
                        .sourceNodeId("2")
                        .sourceCategoryId("1234")
                        .destinationNodeId("123")
                        .build())
                .as(StepVerifier::create)
                .expectError(RuntimeException.class)
                .verify();
        var captor = ArgumentCaptor.forClass(SPathRequest.class);
        BDDMockito.then(pathService).should(times(2)).getPath(captor.capture());

Running it, I do get the expected exception, but 'getPath' is invoked only once.

I probably miss something cause the retry mechanism should have retried and return the stubbed result on the 2nd invocation which should fail the test since no exception occurred and the actual result should have been expected.

What is wrong with my configuration?

Edit: I want the equivalent of this snippet (from resilience4j-reactor examples) for directly invoking on my Mono rather then wrapping a function with Mono.fromCallable):

    @Test
    public void returnOnErrorUsingMono() {
        RetryConfig config = retryConfig();
        Retry retry = Retry.of("testName", config);
        RetryOperator<String> retryOperator = RetryOperator.of(retry);
        given(helloWorldService.returnHelloWorld())
            .willThrow(new HelloWorldException());

        StepVerifier.create(Mono.fromCallable(helloWorldService::returnHelloWorld)
            .transformDeferred(retryOperator))
            .expectSubscription()
            .expectError(HelloWorldException.class)
            .verify(Duration.ofSeconds(1));

        StepVerifier.create(Mono.fromCallable(helloWorldService::returnHelloWorld)
            .transformDeferred(retryOperator))
            .expectSubscription()
            .expectError(HelloWorldException.class)
            .verify(Duration.ofSeconds(1));

        then(helloWorldService).should(times(6)).returnHelloWorld();
        Retry.Metrics metrics = retry.getMetrics();
        assertThat(metrics.getNumberOfFailedCallsWithRetryAttempt()).isEqualTo(2);
        assertThat(metrics.getNumberOfFailedCallsWithoutRetryAttempt()).isEqualTo(0);
    }

Where retryConfig is defined like this:

    private RetryConfig retryConfig() {
        return RetryConfig.custom()
            .waitDuration(Duration.ofMillis(10))
            .build();
    }

Thanks.

omer keynan
  • 135
  • 1
  • 11

1 Answers1

0

It's a long time ago that you asked this question but I could not find any other relevant answers surrounding this topic so figured I would post my findings here in case it benefits someone. My code is in Kotlin, but it should be able to convert it to Java pretty easily if you so desire.

I could not really find what is wrong with your code, everything seems in order as far as I could see, so the problem probably lies in something that is not being shown in your question. I'll just post a setup that worked well for me.

So to start, configure your Resilience4j setup:

import io.github.resilience4j.core.IntervalFunction
import io.github.resilience4j.retry.RetryConfig
import io.github.resilience4j.retry.RetryRegistry
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClientRequestException
import java.io.IOException

@Configuration
class Resilience4jConfiguration {
    companion object {
        const val YOUR_SERVICE = "yourService"
    }

    @Bean
    fun retryRegistry(): RetryRegistry {
        val config: RetryConfig = RetryConfig.custom<YourResponse>()
            .maxAttempts(5)
            .retryExceptions(IOException::class.java, WebClientRequestException::class.java)
            .intervalFunction(IntervalFunction.ofExponentialBackoff(100, 2.0))
            .build()

        return RetryRegistry.of(config)
    }

    @Bean
    fun retryLogger(retryRegistry: RetryRegistry) = RetryLogger(retryRegistry)
}

class RetryLogger(
    retryRegistry: RetryRegistry
) {
    companion object {
        private val logger = logger()
    }

    init {
        retryRegistry.retry(YOUR_SERVICE)
            .eventPublisher
            .onRetry {
                logger.info("Retrying: $it")
            }
    }
}

Now you can use this in your client code, for example like this:

import io.github.resilience4j.reactor.retry.RetryOperator
import io.github.resilience4j.retry.RetryRegistry
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono


@Component
class YourClient(
    retryRegistry: RetryRegistry,
    @Value("\${your.url}") private val baseUrl: String
) {
    private val webClient = WebClient.builder().baseUrl(baseUrl).build()

    private val retry = retryRegistry.retry(YOUR_SERVICE)

    fun performRequest(id: String): Mono<YourResponse> =
        webClient
            .get()
            .uri("/$id")
            .retrieve()
            .bodyToMono(YourResponse::class.java)
            .transformDeferred(RetryOperator.of(retry))
            .onErrorResume { fallback(it) }

    fun fallback(e: Throwable): Mono<YourResponse> = Mono.error(RuntimeException("Tried too many times"))
}

With this setup I do see retries being performed. Hope this helps.

EDIT: the code above works using Spring Boot 3.1.1 and Resilience4j 2.1.0, where you need these 2 dependencies:

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-retry</artifactId>
    <version>${resilience4j.version}</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-reactor</artifactId>
    <version>${resilience4j.version}</version>
</dependency>
Jonck van der Kogel
  • 2,983
  • 3
  • 23
  • 30