I am working on a project reactor workshop and am stuck with the following task:
/**
* TODO 5
* <p>
* For each item call received in colors flux call the {#link #simulateRemoteCall} operation.
* Timeout in case the {#link #simulateRemoteCall} does not return within 400 ms, but retry twice
* If still no response then provide "default" as a return value
*/
The problem I can't wrap my head around is that Flux never actually throws the TimeOutException! I am able to observe this in the console log:
16:05:09.759 [main] INFO Part04HandlingErrors - Received red delaying for 300
16:05:09.781 [main] INFO Part04HandlingErrors - Received black delaying for 500
16:05:09.782 [main] INFO Part04HandlingErrors - Received tan delaying for 300
I tried to play around with the order of the statements, though it didn't seem to change the behaviour. Note: In addition, I tried the overloaded variant of timeout() which accepts a default value that should be returned, if no element is emitted.
public Flux<String> timeOutWithRetry(Flux<String> colors) {
return colors
.timeout(Duration.ofMillis(400))
//.timeout(Duration.ofMillis(400), Mono.just("default"))
.retry(2)
.flatMap(this::simulateRemoteCall)
.onErrorReturn(TimeoutException.class, "default");
}
Can someone clear up why the timeout doesn't occur? I suspect that the mechanism is somehow not "bound" to the method invoked by flatMap.
For completeness: The helper method:
public Mono<String> simulateRemoteCall(String input) {
int delay = input.length() * 100;
return Mono.just(input)
.doOnNext(s -> log.info("Received {} delaying for {} ", s, delay))
.map(i -> "processed " + i)
.delayElement(Duration.of(delay, ChronoUnit.MILLIS));
}
More completeness, this is the test I am given to verify the functionality:
#Test
public void timeOutWithRetry() {
Flux<String> colors = Flux.just("red", "black", "tan");
Flux<String> results = workshop.timeOutWithRetry(colors);
StepVerifier.create(results).expectNext("processed red", "default", "processed tan").verifyComplete();
}
The answer of Martin Tarjányi is correct, but you also asked why in your code
return colors
.timeout(Duration.ofMillis(400))
//.timeout(Duration.ofMillis(400), Mono.just("default"))
.retry(2)
.flatMap(this::simulateRemoteCall)
.onErrorReturn(TimeoutException.class, "default");
no timeout occurs.
The reason is that if the elements of the colors flux are available, then invoking .timeout(Duration.ofMillis(400)) has no effect as timeout only propagates a TimeoutException if no item is emitted within the given duration of 400ms, but this is not the case in this example.
As a consequence the element is emitted and retry(2) has no effect either. Next you invoke simulateRemoteCall on the emitted element which takes some time, but which does not return an error. The result of your code is (beyond timing differences) the same as if you simply apply a map on the given flux:
public Flux<String> timeOutWithRetry(Flux<String> colors) {
return colors.map(s -> "processed " + s);
}
If you want to see a timeout on invocation of simulateRemoteCall then you must add the timeout method after this invocation.
Instead of using flatMap you could also use concatMap. The difference is whether the order should be preserved or not, i.e. whether the default values may occur out of order or not.
Using concatMap the answer looks as follows:
public Flux<String> timeOutWithRetry(Flux<String> colors) {
return colors.concatMap(
color -> simulateRemoteCall(color)
.timeout(Duration.ofMillis(400))
.retry(2)
.onErrorReturn("default"));
}
You're right in that it's the order and place of the statements that is incorrect.
Since you want to retry/timeout/error-handle the remote call, you should put these operators on the Mono of the remote call instead of the Flux.
Timeout on Flux observes the time elapsed between subsequent elements. However, when you use flatMap you get concurrency out of the box and delay between elements practically will be zero (assuming the colors Flux is sourced by an in-memory list). So this operator should not be put directly on the Flux to achieve your goal.
Retry on Flux means it resubscribes to the source in case of error, which depending on the source could result in re-processing already processed elements. Instead, you want to retry failed elements only, so it also should be put on Mono.
public Flux<String> timeOutWithRetry(Flux<String> colors) {
return colors.flatMap(color -> simulateRemoteCall(color).timeout(Duration.ofMillis(400))
.retry(2)
.onErrorReturn("default"));
}
Related
We are given a Mono, that's handling some action(say a database update), and returns a value.
We want to add that Mono(transformed) to a special list that contains actions to be completed for example during shutdown.
That mono may be eagerly subscribed after adding to the list, to start processing now, or .subscribe() might not be called meaning it will be only subscribed during shutdown.
During shutdown we can iterate on the list in the following way:
for (Mono mono : specialList) {
Object value = mono.block(); // (do something with value)
}
How to transform the original Mono such that when shutdown code executes, and Mono was previously subscribed(), the action will not be triggered again but instead it will either wait for it to complete or replay it's stored return value?
OK, looks like it is as simple as calling mono.cache(), so this is how I used it in practice
public Mono<Void> addShutdownMono(Mono<Void> mono) {
mono = mono.cache();
Mono<Void> newMono = mono.doFinally(signal -> shutdownMonos.remove(mono));
shutdownMonos.add(mono);
return newMono;
}
public Function<Mono<Void>,Mono<Void>> asShutdownAwaitable() {
return mono -> addShutdownMono(mono);
}
database.doSomeAction()
.as(asShutdownAwaitable)
.subscribe() // Or don't subscribe at all, deferring until shutdown
Here is the actual shutdown code.
It was also important to me that they execute in order of being added, if user chose not to eagerly subscribe them, that's reason for Flux.concat instead of Flux.merge.
public void shutdown() {
Flux.concat(Lists.transform(new ArrayList<>(shutdownMonos), mono -> mono.onErrorResume(err -> {
logger.error("Async exception during shutdown, ignoring", err);
return Mono.empty();
}))
).blockLast();
}
I have a bit more complex use case in Spring Gateway which is based on WebFlux and I ran into small issue with Mono usage. Long story short, my switchIfEmpty is called even if not required. I prepared small example which allows me to reproduce this problem:
public class ReactiveTest {
#Test
void test1() {
isOdd(2)
.flatMap(this::onNotEmpty)
.switchIfEmpty(Mono.defer(this::onEmpty))
.block();
}
Mono<String> isOdd(Integer number) {
return number % 2 != 0 ? Mono.just("Yes") : Mono.empty();
}
Mono<Void> onNotEmpty(String value) {
System.out.println("Value uppercased " + value.toUpperCase());
return Mono.empty();
}
Mono<Void> onEmpty() {
System.out.println("Value not present, this shouldn't been called if value was odd");
return Mono.empty();
}
}
I hope this is pretty self-explanatory, but just to be safe:
isOdd(Integer number) may produce Mono with data or empty Mono
I expect onNotEmpty to be called only if previous Mono had data
I expect onEmpty to be called only if isOdd produced empty Mono
Unfortunatelly, both onNotEmpty and onEmpty are called all the time, regardless if I pass odd or even number to isOdd.
How can I make sure that onEmpty is called only when isOdd produced Mono.empty()?
The onNotEmpty(String value) method is always returning Mono.empty(), meaning that .switchIfEmpty(Mono.defer(this::onEmpty)) will always be call either because isOdd(2) is already an empty Mono or because onNotEmpty(String value) method was called and returned an empty Mono.
In order to avoid this, you need to change your onNotEmpty(String value) method to return something else than an empty Mono.
Additionally, please avoid using block() since this defeats the whole purpose of using Spring WebFlux by blocking the thread waiting for something to be emitted by the reactive chain.
I have a (possibly infinite) Flux source that is supposed to first store each message (e.g. into a database) and then asynchronously forward the messages (e.g. using Spring WebClient).
The forward(s) in case of failure are supposed to log an error, without completing the source Flux.
I however realized that forward(s) wihtin the flow (flatMap(...)) block execution of the source Flux after exactly 256 messages that cause exceptions (e.g. reactor.retry.RetryExhaustedException).
Representative example that fails in the assert since only 256 messages are processed:
#Test
#SneakyThrows
public void sourceBlockAfter256Exceptions() {
int numberOfRequests = 500;
Set<Integer> sink = new HashSet<>();
Flux
.fromStream(IntStream.range(0, numberOfRequests).boxed())
.map(sink::add)
.flatMap(i -> Mono
// normally the forwards are contained here e.g. by means of Mono.when(...).thenReturn(...).retryWhen(...):
.error(new Exception("any"))
)
.onErrorContinue((throwable, o) -> log.error("Error", throwable))
.subscribe();
Thread.sleep(3000);
Assertions.assertEquals(numberOfRequests, sink.size());
}
Doing the forward within the subscribe(...) doesn't block the source Flux but that's certainly no solution, since I don't possibly want to lose messages.
Questions:
What has happened here? (probably related to some state stored in just one bit)
How can I do this correctly?
EDIT:
According to the discussion below I've constructed an example that uses FluxMessageChannel (which up to my understanding is made for infinite streams and definitly not expected to block after 256 Errors) and has exactly the same behaviour:
#Test
#SneakyThrows
public void maxConnectionWithChannelTest() {
int numberOfRequests = 500;
Set<Integer> sink = new HashSet<>();
FluxMessageChannel fluxMessageChannel = MessageChannels.flux().get();
fluxMessageChannel.subscribeTo(
Flux
.fromStream(IntStream
.range(0, numberOfRequests).boxed()
.map(i -> MessageBuilder.withPayload(i).build())
)
.map(Message::getPayload)
.map(sink::add)
.flatMap(i -> Mono.error(new Exception("whatever")))
);
Flux
.from(fluxMessageChannel)
.subscribe();
Thread.sleep(3000);
Assert.assertEquals(numberOfRequests, sink.size());
}
EDIT:
I just raised an issue in the reactor core project: https://github.com/reactor/reactor-core/issues/2011
I have an infinite hot flux of data. I am about to engage in carrying out an operation on each element in the stream, each of which returns a Mono which will complete (one way or another) after some finite time.
There is the possibility of an error being thrown from these operations. If so, I want to resubscribe to the hot flux without missing anything, retrying elements that were in the middle of being processed when the error was thrown (i.e. anything that did not complete successfully).
What do I do here? I can tolerate repeated operations on the same elements, but not losing elements entirely from the stream.
I've attempted to use a ReplayProcessor to handle this, but I can't see a way of making it work without repeating a lot of operations that might well have succeeded (using a very conservative timeout), or losing elements due to new elements overriding old ones in the buffer (as below).
Test case:
#Test
public void fluxTest() {
List<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
strings.add("three");
strings.add("four");
ConnectableFlux<String> flux = Flux.fromIterable(strings).publish();
//Goes boom after three uses of its method, otherwise
//returns a mono. completing after a little time
DangerousClass dangerousClass = new DangerousClass(3);
ReplayProcessor<String> replay = ReplayProcessor.create(3);
flux.subscribe(replay);
replay.flatMap(dangerousClass::doThis)
.retry(1)
.doOnNext(s -> LOG.info("Completed {}", s))
.subscribe();
flux.connect();
flux.blockLast();
}
public class DangerousClass {
Logger LOG = LoggerFactory.getLogger(DangerousClass.class);
private int boomCount;
private AtomicInteger count;
public DangerousClass(int boomCount) {
this.boomCount = boomCount;
this.count = new AtomicInteger(0);
}
public Mono<String> doThis(String s) {
return Mono.fromSupplier(() -> {
LOG.info("doing dangerous {}", s);
if (count.getAndIncrement() == boomCount) {
LOG.error("Throwing exception from {}", s);
throw new RuntimeException("Boom!");
}
return s;
}).delayElement(Duration.ofMillis(600));
}
}
This prints:
doing dangerous one
doing dangerous two
doing dangerous three
doing dangerous four
Throwing exception from four
doing dangerous two
doing dangerous three
doing dangerous four
Completed four
Completed two
Completed three
One is never completed.
The error (at least in the above example) can only occur in the flatMap(dangerousClass::doThis) call - so resubscribing to the root Flux and replaying elements when this one flatMap() call has failed seems a bit odd, and (probably) isn't what you want to do.
Instead, I'd recommend ditching the ReplayProcessor and just calling retry on the inner flatMap() call instead, so you end up with something like:
ConnectableFlux<String> flux = Flux.range(1, 10).map(n -> "Entry " + n).publish();
DangerousClass dangerousClass = new DangerousClass(3);
flux.flatMap(x -> dangerousClass.doThis(x).retry(1))
.doOnNext(s -> System.out.println("Completed " + s))
.subscribe();
flux.connect();
This will give you something like the following, with all entries completed and no retries:
doing dangerous Entry 1
doing dangerous Entry 2
doing dangerous Entry 3
doing dangerous Entry 4
Throwing exception from Entry 4
doing dangerous Entry 4
Completed Entry 2
Completed Entry 1
Completed Entry 3
Completed Entry 4
I have a list a want to refresh every minute.
For example the user list here : https://github.com/android10/Android-CleanArchitecture/blob/master/domain/src/main/java/com/fernandocejas/android10/sample/domain/interactor/GetUserList.java
I add a periodical refresh using repeatWhen :
public Observable<List<User>> buildUseCaseObservable(Void unused) {
return this.userRepository
.users()
.repeatWhen(new Function<Observable<Object>, ObservableSource<?>>() {
#Override
public ObservableSource<?> apply(Observable<Object> objectObservable) throws Exception {
return objectObservable.delay(1, TimeUnit.MINUTES);
}
});
}
It works fine this way, calling onNext every minute.
But if I want to refresh immediately this list (because of user's action or because of a notification), I don't know how to perform that.
Should I cancel/dispose the observable and restart a new one ?
Thanks
From your code I understand that the users list is generated and emitted upon subscription.
Here are some solutions I can think of, instead of unsubscribing and resubscribing upon the event to which you want to react immediately:
Instead of using the repeatWhen operator, use the interval creation operator combined with the flatMap to invoke the subscription to a new Observable every minute and use the merge operator to add reaction to the other event in which you are interested. Something like this:
#Test
public void intervalObservableAndImmediateReaction() throws InterruptedException {
Observable<String> obs = Observable.interval(1, TimeUnit.SECONDS)
.cast(Object.class)
.mergeWith(
Observable.just("mockedUserClick")
.delay(500, TimeUnit.MILLISECONDS))
.flatMap(
timeOrClick -> Observable.just("Generated upon subscription")
);
obs.subscribe(System.out::println);
Thread.currentThread().sleep(3000); //to see the prints before ending the test
}
or adjusted to your needs (but the principal is the same):
Observable.interval(1, TimeUnit.MINUTES)
.mergeWith(RxView.clicks(buttonView))
.flatMap(timeOrClick -> this.userRepository.users());
You can use the flatMap operator as before, even while keeping you working current implementation and without merging to an interval - just keep your working code and in another area of the programme chain it to the RxBinding of your choosing:
RxView.touches(yourViewVariable)
.flatMatp(motionEvent -> this.userRepository.users())
.subscribe(theObserver);
Note that in this solution the subscription is done independently to the two observables. You'll probably be better off if you use different observers, or manage a subject or something on that line. A small test I ran showed one subscriber handled subscribing to 2 different observables with no problem (in Rxjava1 - didn't check in Rxjava2 yet), but it feels iffy to me.
If you aren't concerned with adjusting the refresh time after one of the other observables emits data you can do something like the following:
// Specific example of a user manually requesting
val request = Observable.create<String> { emitter ->
refresh.setOnClickListener {
emitter.onNext("Click Request")
}
}
.observeOn(Schedulers.io())
.flatMap {
userRepository.users()
}
// Refresh based off of your original work, could use something like interval as well
val interval = userRepository.users()
.subscribeOn(Schedulers.io())
.repeatWhen { objectObservable ->
objectObservable.delay(1, TimeUnit.MINUTES)
}
// Combine them so that both emissions are received you can even add on another source
Observable.merge(request,interval)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
contents.text = it.toString()
}, {
contents.text = it.toString()
},{
println(contents.text)
})
Then you don't have to dispose and resubscribe every time