Search code examples
javareactive-programmingproject-reactor

Project Reactor - the usage of defer() to make method retry-able


Recently I tried to test the retry behaviour in unit tests by setting up the mocked dependency to return multiple Mono.error() before returning a successful mono.just() at the end:

@Mock
Dependency dependency;

@InjectMocks
ClassUnderTest classUnderTest;

@Test
void someTest() {
    final Object object = new Object();
    when(dependency.method(anyString()))
        .thenReturn(Mono.error(new Exception()))
        .thenReturn(Mono.error(new Exception()))
        .thenReturn(Mono.error(new Exception()))
        .thenReturn(Mono.just(object));

    StepVerifier.create(classUnderTest.method("abc"))
        .expectNext(object)
        .verifyComplete();

    verify(dependency, times(4)).method("abc");
}

The above setup won't work as I figured out later, the retry in Reactor is not done via recalling the method for specific amount of time, but by calling the method once, gets the publisher, and re-subscribing to it again and again.

class ClassUnderTest {
    private Dependency dependency;

    public Mono<Object> method(final String str) {
        return this.dependency.method(str).retryWhen(Retry.max(3));
    }
}

And re-subscribing won't work, if the Dependency#method is implemented as:

class Dependency {
    private OtherDependency otherDependency;

    public Mono<Object> method(final String str) {
        return this.otherDependency.get(str).map(/* some mapping logic */);
    }
}

Dependency#method cannot make too much assumption on whether or not OtherDependency#get is deferred. Hence, Dependency needs to:

class Dependency {
    private OtherDependency otherDependency;

    public Mono<Object> method(final String str) {
        return Mono.defer(() -> this.otherDependency.get(str)).map(/* some mapping logic */);
    }
}

Since we want to say that each method should be "retry-able", does that mean, we need to always use defer(...)?

Or am I mis-understanding anything?


Solution

  • I should have thought about it.

    The easier way instead of making all methods natively "retry-able", is to wrap the publisher with defer before attaching the retryWhen operator.

    Before:

    class ClassUnderTest {
        private Dependency dependency;
    
        public Mono<Object> method(final String str) {
            return dependency.method(str).retryWhen(Retry.max(3));
        }
    }
    

    After:

    class ClassUnderTest {
        private Dependency dependency;
    
        public Mono<Object> method(final String str) {
            return Mono.defer(() -> dependency.method(str)).retryWhen(Retry.max(3));
        }
    }
    

    Now, we do not have to say all methods should be natively "retry-able", but to always wrap the to-be-retried publisher with defer.