It may sound funny or maybe novice, but I don't understand what it means that something "failed" in Reactive Programming. Do you mean a null? An empty object? someone could please give me examples.
Some of the scenarios I wish to realize are:
Assuming I send a query parameter and get either a listing with values or an empty listing; and lastly, I do not send the query parameter.
If an empty listing is issued I want to return an exception and a 404 status code.
If they do not send the query parameter I want to return an exception and some status code.
And of course, if a list with values is found, return it. Will it be possible to make these cases in a single method? how do I do it?
First, a reactor operator can ends in different ways:
Completes successfully after emitting one (Mono) or more (Flux) value(s)
Completes empty: The pipeline sends completion signal, but no value has been emitted
Completes in error: somewhere in the pipeline, an error happened. By default, as in imperative code, it stops the chain of operations, and is propagated.
Cancelled: the pipeline might be interrupted by a manual action or a system shutdown. It then ends in error (a special kind of error, but an error nonetheless)
Secondly, reactive-stream, whatever the implementation (RxJava or Reactor) does not accept null values. It means that trying to produce a null value in/from a reactive stream will either cause an error or an undefined behavior. This is stated in reactive-stream specification, rule 2.13:
Calling [...] onNext [...] MUST return normally except when any provided parameter is null in which case it MUST throw a java.lang.NullPointerException to the caller
Let's try to produce some simple examples first.
This program shows the possible ways a pipeline can complete:
// Reactive streams does not accept null values:
try {
Mono.just(null);
} catch (NullPointerException e) {
System.out.println("NULL VALUE NOT ACCEPTED !");
}
// Mono/Flux operations stop if an error occurs internally, and send it downstream
try {
Mono.just("Something")
.map(it -> { throw new IllegalStateException("Bouh !"); })
.block();
} catch (IllegalStateException e) {
System.out.println("Error propagated: "+e.getMessage());
}
// A mono or a flux can end "empty". It means that no value or error happened.
// The operation just finished without any result
var result = Mono.just("Hello !")
.filter(it -> !it.endsWith("!"))
// Materialize allow to receive the type of signal produced by the pipeline (next value, error, completion, etc.)
.materialize()
.block();
System.out.println("Input value has been filtered out. No 'next' value " +
"received, just 'completion' signal:" + result.getType());
Its output:
NULL VALUE NOT ACCEPTED !
Error propagated: Bouh !
Input value has been filtered out. No 'next' value received, just 'completion' signal:onNext
Then, let's look at a program that intercept empty pipelines and errors, and handle them gracefully:
// Errors can be intercepted and replaced by a value:
var result = Mono.error(new IllegalStateException("No !"))
.onErrorResume(err -> Mono.just("Override error: Hello again !"))
.block();
System.out.println(result);
// Empty pipelines can also be replaced by another one that produce a value:
result = Mono.just("Hello !")
.filter(it -> !it.endsWith("!"))
.switchIfEmpty(Mono.just("Override empty: Hello again !"))
.block();
System.out.println(result);
It produces:
Override error: Hello again !
Override empty: Hello again !
With all this tools, we can solve the problem you describe with your query.
Let's mimic it:
public static Flux<String> processRequest(String queryParam) {
if (queryParam == null || queryParam.isEmpty()) return Flux.error(new IllegalArgumentException("Bad request"));
return Mono.just(queryParam)
.flatMapMany(param -> Flux.fromArray(param.split("_")))
.switchIfEmpty(Mono.error(new IllegalStateException("No data")));
}
public static void main(String[] args) {
String[] inputs = { null, "hello_world", "___" };
for (var input : inputs) {
try {
String result = processRequest(input)
.collect(Collectors.joining(", ", "[", "]"))
.block();
System.out.println("COMPLETED: " + result);
} catch (Exception e) {
System.out.println("ERROR: " + e.getMessage());
}
}
}
It prints:
ERROR: Bad request
COMPLETED: [hello, world]
ERROR: No data
Related
Wrote a class as below:
class KeyFetcher {
String key = "";
Mono getKeys() {
try {
key = keyService.getKeys();
if(key == null) {
throw new RuntimeException("key value is null");
} else {
this.key = key;
}
} catch(Exception e) {
return Mono.error(e);
}
return Mono.just(key)
}
#Scheduled // spring scheduler
void fetchDataFromExternalService() {
client.fetchData(key) // returns Mono
.retry(3)
.map(this::processResponse)
.doOnError(this::handleError)
.subscribe();
}
}
I want to modify the fetchDataFromExternalService method to include the getKeys() also before calling external service, and retry three times on both methods. Retry will be like:
get keys - try 3 times on failure - then go to handleError and do not call external service.
if key are fetched within 3 retry boundary, then now fetch data from external service with max 3 retries.
if error encountered in client.fetchData(), then go to handleError but not retry getKeys9) again.
I tried below:
Mono.defer(()-> getKeys())
.flatMap(obj -> client.fetchData(key))
.retry(3)
.map(this::processResponse)
.doOnError(this::handleError)
.subscribe();
But the problem is that when getKey() returns response in say 2nd retry, it enters fetchData(), but if this methods throws exception, it enters handleError() before again re-entering getKey() which is not desirable. All I want is once getKey() retry is exhausted, control should enter handleError() and close the Mono. Same for fetchData(). I think I am doing something wrong above, but need suggestion as I am new to this. Thanks.
You are well describing your issue, and if you apply strictly your description to your code, you will get what you need.
In your code sample, you create a pipeline that fetch a key, then fetch data. Therefore, the retry is applied to the entire chain, because it receive a publisher that already chain both actions.
You want:
to fetch a key with 3 max retries:
Mono<Key> getKeyWithRetry = Mono.defer(() -> getKeys()).retry(3);
Fetch data from an input key, with retry:
Function<Key, Mono<Data>> fetchDataWithRetry = key -> fetchData(key).retry(3)
And finally, assemble all that with error handling:
getKeyWithRetry()
.flatMap(fetchDataWithRetry)
.map(this::processResponse)
.doOnError(this::handleError)
.subscribe();
As per documentation I am expecting onErrorContinue will ignore the error element and continue the sequence. Below test case is failing with exception
java.lang.AssertionError: expectation "expectNext(12)" failed (expected: onNext(12); actual: onError(java.lang.RuntimeException:
#Test
public void testOnErrorContinue() throws InterruptedException {
Flux<Integer> fluxFromJust = Flux.just(1, 2,3,4,5)
.concatWith(Flux.error(new RuntimeException("Test")))
.concatWith(Flux.just(6))
.map(i->i*2)
.onErrorContinue((e,i)->{
System.out.println("Error For Item +" + i );
})
;
StepVerifier
.create(fluxFromJust)
.expectNext(2, 4,6,8,10)
.expectNext(12)
.verifyComplete();
}
onErrorContinue() may not be doing what you think it does - it lets upstream operators recover from errors that may occur within them, if they happen to support doing so. It's a rather specialist operator.
In this case map() does actually support onErrorContinue, but map isn't actually producing an error - the error has been inserted into the stream already (by way of concat() and the explicit Flux.error() call.) In other words, there's no operator producing the error at all, so there's therefore nothing for it to recover from, as an element supplied is erroneous.
If you changed your stream so that map() actually caused the error, then it would work as expected:
Flux.just(1, 2,3,4,5)
.map(x -> {
if(x==5) {
throw new RuntimeException();
}
return x*2;
})
.onErrorContinue((e,i)->{
System.out.println("Error For Item +" + i );
})
.subscribe(System.out::println);
Produces:
2
4
6
8
Error For Item +5
An alternative depending on the real-world use case may be to use onErrorResume() after the element (or element source) that may be erroneous:
Flux.just(1, 2, 3, 4, 5)
.concatWith(Flux.error(new RuntimeException()))
.onErrorResume(e -> {
System.out.println("Error " + e + ", ignoring");
return Mono.empty();
})
.concatWith(Flux.just(6))
.map(i -> i * 2)
.subscribe(System.out::println);
In general, using another "onError" operator (such as onErrorResume()) is generally the more usual, and more recommended approach, since onErrorContinue() is dependent on operator support and affects upstream, not downstream operators (which is unusual.)
So I have a method which returns an Vavr Try:
public Try<Result> request() {...}
request comes from a source which I cannot modify. Currently, I flatmap over the result from request and depending if the Result has an error return a Try with an exception or a success with the data from the Result:
public Try<Data> fetchData() {
return request().flatMap(result -> {
if (result.hasError()) {
return Try.failure(new FailedRequestException());
} else {
return Try.success(result.data());
}
});
}
What I want is in some places where fetchData is used first do something with the data if the Try is a success and if it is a failure, log an error if the error is a FailedRequestException, else, do something else with the exception, something like the following:
fetchData().andThen(data -> ...).onFailure(ex -> {
if (ex instanceOf FailedRequestException) {
log.error("Could not fetch data: " + ex.getMessage());
} else {
// Do something with the exception
...
}
});
My problem with this approach is that fetchData returns a Try so the caller cannot know that a FailedRequestException is part of the possible failures. I can let fetchData return a Try<Either<FailedRequestException, Data>> but this doesn't feel right either. Is there any way to do the above in a more elegant way? I also tried using the Match and Case but the Case expects a Function as handler and not a Consumer.
To sum up: you actually have 3 scenarios (success, failure with FailedRequestException, any other failure). This sounds like a job for pattern matching! Let's make the code as visible and expressive as the business requirement :)
Match(fetchData()).of(
Case($Success($()), data -> doStuff(data)),
Case($Failure($(instanceOf(FailedRequestException.class))), fre -> logFreAndReturnValue(fre)),
Case($Failure($()), e -> doSomethingWithOtherException(e))
);
FWIW, you can rewrite your fetchData implementation as such:
Try(request())
.mapFailure(Case($(), ignored -> new FailedRequestException()))
.map(Result::data);
As a rule of thumb, try to stick to using flatMap when the context (Success or Failure) may change. In your current fetchData implementation a success remains a success, a failure remains a failure, so it is a mapping between the input and the output, hence use map family of functions.
Cheers!
Even though I am using Supplier for my streams and using Supplier.Get() each time I want to retrieve my strem and perform a terminal operation on it, I am still getting the "stream has already been operated upon or closed" exception. Could anyone please look at my code and suggest what I am doing wrong?
Method where exception is being thrown:
private static void printMyDetails(Supplier<Stream<Result>> mySupplier, String myStatus) {
checkNotNull(mySupplier);
checkArgument(isNotEmpty(myStatus), "Invalid My status");
if (mySupplier.get().noneMatch(result -> true)) { //<-This is where the exception is thrown
if (log.isInfoEnabled()) {
log.info("Number of My with status '{}': {}", My, 0);
}
} else {
log.info("Number of My with status '{}': {}", myStatus, mySupplier.get().count());
log.info("Details of My(s) with status: '{}'", myStatus);
mySupplier.get().sorted().forEach(Utils::printMyNameAndDetails);
}
}
Place which is calling the above method:
rb.keySet().stream().filter(key -> containsIgnoreCase(key, "status")).map(rb::getString)
.filter(StringUtils::isNotEmpty).forEach(status -> {
var resultsWithExpectedStatusSupplier = requireNonNull(getResultsWithExpectedStatusSupplier(results, status));
resultsWithExpectedStatusSupplier.ifPresentOrElse(streamSupplier -> printMyDetails(streamSupplier, status), () -> {
if (log.isInfoEnabled())
log.info("0 My with status: {}", status);
});
});
The stream supplier:
private static Optional<Supplier<Stream<Result>>> getResultsWithExpectedStatusSupplier(
#NotNull List<Result> results, #NotNull String expectedStatus) {
checkArgument(!results.isEmpty(), "Results list is empty");
checkArgument(isNotEmpty(expectedStatus), "Invalid expected status");
var resultStreamWithExpectedStatus = requireNonNull(results.stream().filter(result -> ofNullable(result).map(Result::getStatus)
.allMatch(s -> isNotEmpty(s) && s.equalsIgnoreCase(expectedStatus))));
return resultStreamWithExpectedStatus.count() == 0 ? Optional.empty() : Optional.of(() -> resultStreamWithExpectedStatus);
}
The general problem is as Christian Ullenboom said: The stream has already been consumed. The exact location in your code is the call to resultStreamWithExpectedStatus.count() in the method getResultsWithExpectedStatusSupplier, as Stream.count is a reduction/terminal operation which consumes the stream.
As stated in e.g. this answer, you cannot get the streams size without consuming it. Fix it by storing the filtered items in a collection (e.g. Collectors.toList), querying the size there and returning the collection itself rather than the stream?
On a side note, I think you misuse Optional a bit too much. The code could be simpler passing empty streams (or even better: pass an empty, filtered collection).
You can consume a Stream just once. It looks like the Supplier is always giving the same Stream again and again. After the first terminal operation the Stream is drained; the Stream from the Supplier has to be a new Stream all the time.
I want to create a pattern in my application where all Observable<T> objects that are returned have some default error handling, meaning that the subscribers may use the .subscribe(onNext) overload without fear of the application crashing. (Normally you'd have to use .subscribe(onNext, onError)). Is there any way to acheive this?
I've tried attaching to the Observable by using onErrorReturn, doOnError and onErrorResumeNext - without any of them helping my case. Maybe I'm doing it wrong, but I still get rx.exceptions.OnErrorNotImplementedException if an error occurs within the Observable.
Edit 1: This is example of an Observable that emits an error, which I want to handle in some middle layer:
Observable.create(subscriber -> {
subscriber.onError(new RuntimeException("Somebody set up us the bomb"));
});
Edit 2: I've tried this code to handle the error on behalf of the consumer, but I still get OnErrorNotImplementedException:
// obs is set by the method illustrated in edit 1
obs = obs.onErrorResumeNext(throwable -> {
System.out.println("This error is handled by onErrorResumeNext");
return null;
});
obs = obs.doOnError(throwable -> System.out.println("A second attempt at handling it"));
// Consumer code:
obs.subscribe(
s -> System.out.println("got: " + s)
);
This will work - the key was to return Observable.empty();
private <T> Observable<T> attachErrorHandler(Observable<T> obs) {
return obs.onErrorResumeNext(throwable -> {
System.out.println("Handling error by printint to console: " + throwable);
return Observable.empty();
});
}
// Use like this:
Observable<String> unsafeObs = getErrorProducingObservable();
Observable<String> safeObservable = attachErrorHandler(unsafeObs);
// This call will now never cause OnErrorNotImplementedException
safeObservable.subscribe(s -> System.out.println("Result: " + s));