RxJava error handling for hot observable - java

I'm pretty new to RxJava and have some questions on patterns etc.
I'm creating an observable using the code below:
public Observable<Volume> getVolumeObservable(Epic epic) {
return Observable.create(event -> {
try {
listeners.add(streamingAPI.subscribeForChartCandles(epic.getName(), MINUTE, new HandyTableListenerAdapter() {
#Override
public void onUpdate(int i, String s, UpdateInfo updateInfo) {
if (updateInfo.getNewValue(CONS_END).equals(ONE)) {
event.onNext(new Volume(Integer.parseInt(updateInfo.getNewValue(LAST_TRADED_VOLUME))));
}
}
}));
} catch (Exception e) {
LOG.error("Error from volume observable", e);
}
});
}
Everything is working as expected, but I have some questions on error handling.
If I understand correctly, this is to be viewed as a "hot observble", i.e. events will happen regardless of there being a subscription or not (onUpdate is a callback used by a remote server which I have no control over).
I've chosen not to call onError here since I don't want the observable to stop emitting events in case of a single exception. Is there a better pattern to be used? .retry() comes to mind, but I'm not sure that it makes sense for a hot observable?
Also, how is the observable represented when the subscriptions is created, but before the first onNext is called? Is it just an Observable.empty()

1) Your observable is not hot. The distinguishing factor is whether multiple subscribers share the same subscription. Observable.create() invokes subscribe function for every subscriber, i.e. it is cold.
It is easy to make it hot though. Just add share() operator. It will subscribe with first subscriber and unsubscribe with last one. Do not forget to implement unsubscribe functionality with something like this:
event.setCancellable(() -> listeners.remove(...));
2) Errors could be recoverable and not recoverable.
In case you consider an error to be self-recoverable (no action required from your side) you should not call onError as this will kill your observable (no further events would be emitted). You can possibly notify your subscribers by emitting special Volume message with error details attached.
In case an error is fatal, e.g. you have failed to add listener, so there could be no further messages, you should not silently ignore this. Emit onError as your observable is not functional anyway.
In case an error requires actions from you, typically retry, or retry with timeout, you can add one of retryXxx() operators. Do this after create() but before share().
3) Observable is an object with subscribe() method. How exactly it is represented depends on the method you created it with. See source code of create() for example.

Related

Interrupting the reactive pipeline via exception VS propagating error object

What is the best in terms of reactive programming when there is a need of interrupting a reactive pipeline?
The logic is very straightforward.
The web service, web application will accept requests.
Step 1, from the request, make one first HTTP request to a third party API. The first HTTP service will either answer with what we need, in our example, a string starting with good, or something we do not need.
Step 2, only if step 1 responded with what is needed, make a second HTTP request to a second HTTP service, also no control over, to get the ultimate and greatest response.
Note, this is sequential, we cannot call step 2 unless we have the correct value from step 1.
Obviously, making an entire HTTP call to step 2 at this point does not make sense at all.
Therefore, I am thinking of doing:
#PostMapping(path = "/question")
public Mono<ResponseEntity<String>> createDummyMono(String theImportantKey) {
return WebClient.create("http://first-service.com/get" + theImportantKey).get().exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class))
.flatMap(extractGoodValueFromStepOne -> {
if (extractGoodValueFromStepOne.startsWith("good")) {
System.out.println("Great! Step1 responded with something starting with good! Only with this we should invoke the second API");
return WebClient.create("http://second-service.com/get" + extractGoodValueFromStepOne.substring(4)).get().exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class));
} else {
System.out.println("This is bad, Step 1 did not return something starting with good, no need to make the second API call then. Let's just propagate an error message all the way to response with a dummy Mono");
return Mono.just("Step 1 did not answer with something good, the ultimate answer is an error");
}
})
.map(ResponseEntity::ok);
}
In this logic, the second step, represented by the flatMap will see if step 1 responded something we need. Only this case, a second HTTP request will be made to step 2. However, if it is not, I am building a dummy Mono to propagate and carry down the reactive pipeline.
A second solution, is to throw an exception, and catch it with #ExceptionHandler for instance
#PostMapping(path = "/question")
public Mono<ResponseEntity<String>> throwRuntimeException(String theImportantKey) {
return WebClient.create("http://first-service.com/get" + theImportantKey).get().exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class))
.flatMap(extractGoodValueFromStepOne -> {
if (extractGoodValueFromStepOne.startsWith("good")) {
System.out.println("Great! Step1 responded with something starting with good! Only with this we should invoke the second API");
return WebClient.create("http://second-service.com/get" + extractGoodValueFromStepOne.substring(4)).get().exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class));
} else {
System.out.println("This is bad, Step 1 did not return something starting with good, no need to make the second API call then. Let's just propagate an error message all the way to response with a dummy Mono");
throw new RuntimeException("Step 1 did not answer with something good, the ultimate answer is an error");
}
})
.map(ResponseEntity::ok);
}
#ExceptionHandler
public Mono<ResponseEntity<String>> exception(final RuntimeException runtimeException) {
return Mono.just(ResponseEntity.ok("Step 1 did not answer with something good, the ultimate answer is an error"));
}
Here, the logic is the same. Just if step 1 did not answer with what we need, I interrupt the pipeline by throwing a RuntimeException.
I kinda think, neither the first solution, passing down some dummy Mono or throwing an unchecked RuntimeException sounds the correct way to do in a reactive world.
May I ask which is the correct solution to answer to this problem and why please?
Your dummy Mono solution only works because there is nothing after in the chain that needs to do any additional processing, what if after your flatMap you need to do an additional flatMapon the successful value? then you will be in a pickle when a strange dummy Monocomes flying down the chain.
.flatMap(value -> {
if (value.startsWith("good")) {
System.out.println("good");
return WebClient.create("http://second-service.com/get" + value.substring(4))
.get()
.exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class));
} else {
System.out.println("Boo");
return Mono.just("some value");
}
}).flatMap(value2 -> {
// what now?
})
When an exception is thrown in an operator the exception will be propagatade through the stream as an onErrorevent. Like when we for instance return a Mono#error.
Some exceptions like for instance OutOfMemoryExceptionwill not be considered as such, but are instead fatal events and will terminate the flow immediately.
But otherwise most commonly the exception will then be transferred through the chain and "regular" operators will se that, that is an error event so they will just skip that event and pass it down the chain either out to the calling client, or until any of the specialized error event handlers see it, or as in your case be snatched up by an Exception handler that you have defined.
The correct way would be in your cases is to return a Mono#error (so you are explicit in your return) saying that if this happens we return an error and then either you recover, drop the value or whatever you want to do, or as you have done, handled the exception using an exception handler.
Your first solution behaves more like a return empty, and then you have switchIfEmpty operator so you change to another publisher (Mono) if last operator returned empty. Or you could use onErrorResume that will, if a specific error comes along, return a fallback Publisher.
There are very, very many ways of handling errors in reactor and i suggest you read up on them and try them all out.
4.6.2. Handling Exceptions in Operators or Functions

Invoking non-blocking operations sequentially while consuming from a Flux including retries

So my use-case is to consume messages from Kafka in a Spring Webflux application while programming in the reactive style using Project Reactor, and to perform a non-blocking operation for each message in the same order as the messages were received from Kafka. The system should also be able to recover on its own.
Here is the code snippet that is setup to consume from :
Flux<ReceiverRecord<Integer, DataDocument>> messages = Flux.defer(() -> {
KafkaReceiver<Integer, DataDocument> receiver = KafkaReceiver.create(options);
return receiver.receive();
});
messages.map(this::transformToOutputFormat)
.map(this::performAction)
.flatMapSequential(receiverRecordMono -> receiverRecordMono)
.doOnNext(record -> record.receiverOffset().acknowledge())
.doOnError(error -> logger.error("Error receiving record", error))
.retryBackoff(100, Duration.ofSeconds(5), Duration.ofMinutes(5))
.subscribe();
As you can see, what I do is: take the message from Kafka, transform it into an object intended for a new destination, then send it to the destination, and then acknowledge the offset to mark the message as consumed and processed. It is critical to acknowledge the offset in the same order as the messages being consumed from Kafka so that we don't move the offset beyond messages that were not fully processed (including sending some data to the destination). Hence I'm using a flatMapSequential to ensure this.
For simplicity let's assume the transformToOutputFormat() method is an identity transform.
public ReceiverRecord<Integer, DataDocument> transformToOutputFormat(ReceiverRecord<Integer, DataDocument> record) {
return record;
}
The performAction() method needs to do something over the network, say call an HTTP REST API. So the appropriate APIs return a Mono, which means the chain needs to be subscribed to. Also, I need the ReceiverRecord to be returned by this method so that the offset can be acknowledged in the flatMapSequential() operator above. Because I need the Mono subscribed to, I'm using flatMapSequential above. If not, I could have used a map instead.
public Mono<ReceiverRecord<Integer, DataDocument>> performAction(ReceiverRecord<Integer, DataDocument> record) {
return Mono.just(record)
.flatMap(receiverRecord ->
HttpClient.create()
.port(3000)
.get()
.uri("/makeCall?data=" + receiverRecord.value().getData())
.responseContent()
.aggregate()
.asString()
)
.retryBackoff(100, Duration.ofSeconds(5), Duration.ofMinutes(5))
.then(Mono.just(record));
I have two conflicting needs in this method:
1. Subscribe to the chain that makes the HTTP call
2. Return the ReceiverRecord
Using a flatMap() means my return type changes to a Mono. Using doOnNext() in the same place would retain the ReceiverRecord in the chain, but would not allow the HttpClient response to be subscribed to automatically.
I can't add .subscribe() after asString(), because I want to wait till the HTTP response is completely received before the offset is acknowledged.
I can't use .block() either since it runs on a parallel thread.
As a result, I need to cheat and return the record object from the method scope.
The other thing is that on a retry inside performAction it switches threads. Since flatMapSequential() eagerly subscribes to each Mono in the outer flux, this means that while acknowledgement of offsets can be guaranteed in order, we can't guarantee that the HTTP call in performAction will be performed in the same order.
So I have two questions.
Is it possible to return record in a natural way rather than returning the method scope object?
Is it possible to ensure that both the HTTP call as well as the offset acknowledgement are performed in the same order as the messages for which these operations are occurring?
Here is the solution I have come up with.
Flux<ReceiverRecord<Integer, DataDocument>> messages = Flux.defer(() -> {
KafkaReceiver<Integer, DataDocument> receiver = KafkaReceiver.create(options);
return receiver.receive();
});
messages.map(this::transformToOutputFormat)
.delayUntil(this::performAction)
.doOnNext(record -> record.receiverOffset().acknowledge())
.doOnError(error -> logger.error("Error receiving record", error))
.retryBackoff(100, Duration.ofSeconds(5), Duration.ofMinutes(5))
.subscribe();
Instead of using flatMapSequential to subscribe to the performAction Mono and preserve sequence, what I've done instead is delayed the request for more messages from the Kafka receiver until the action is performed. This enables the one-at-a-time processing that I need.
As a result, performAction doesn't need to return a Mono of ReceiverRecord. I also simplified it to the following:
public Mono<String> performAction(ReceiverRecord<Integer, DataDocument> record) {
HttpClient.create()
.port(3000)
.get()
.uri("/makeCall?data=" + receiverRecord.value().getData())
.responseContent()
.aggregate()
.asString()
.retryBackoff(100, Duration.ofSeconds(5), Duration.ofMinutes(5));
}

Project Reactor async send email with retry on error

I need to send some data after user registered. I want to do first attempt in main thread, but if there are any errors, I want to retry 5 times with 10 minutes interval.
#Override
public void sendRegisterInfo(MailData data) {
Mono.just(data)
.doOnNext(this::send)
.doOnError(ex -> logger.warn("Main queue {}", ex.getMessage()))
.doOnSuccess(d -> logger.info("Send mail to {}", d.getRecipient()))
.onErrorResume(ex -> retryQueue(data))
.subscribe();
}
private Mono<MailData> retryQueue(MailData data) {
return Mono.just(data)
.delayElement(Duration.of(10, ChronoUnit.MINUTES))
.doOnNext(this::send)
.doOnError(ex -> logger.warn("Retry queue {}", ex.getMessage()))
.doOnSuccess(d -> logger.info("Send mail to {}", d.getRecipient()))
.retry(5)
.subscribe();
}
It works.
But I've got some questions:
Did I correct to make operation in doOnNext function?
Is it correct to use delayElement to make a delay between executions?
Did the thread blocked when waiting for delay?
And what the best practice to make a retries on error and make a delay between it?
doOnXXX for logging is fine. But for the actual element processing, you must prefer using flatMap rather than doOnNext (assuming your processing is asynchronous / can be converted to returning a Flux/Mono).
This is correct. Another way is to turn the code around and start from a Flux.interval, but here delayElement is better IMO.
The delay runs on a separate thread/scheduler (by default, Schedulers.parallel()), so not blocking the main thread.
There's actually a Retry builder dedicated to that kind of use case in the reactor-extra addon: https://github.com/reactor/reactor-addons/blob/master/reactor-extra/src/main/java/reactor/retry/Retry.java

RxJava Subject Cache Result With Retry

I have Observable<FeaturedItemList> getFeatured() that is called everytime the page opened. This function is called from two different components on the same page. Since it retrieves from the network, I cached it and make it shareable with ReplaySubject.
public Observable<FeaturedItemList> getFeatured() {
if(mFeaturedReplaySubject == null) {
mFeaturedReplaySubject = ReplaySubject.create();
getFromNetwork().subscribe(mFeaturedReplaySubject);
}
return mFeaturedReplaySubject;
}
Then I realize that when the request failed for some reasons, if the user come back to that page it will not show any results unless the user killed the app. So I decided to have some retry logic. Here's what I do:
public Observable<FeaturedItemList> getFeatured() {
synchronized (this) {
if (mFeaturedReplaySubject == null) {
mFeaturedReplaySubject = ReplaySubject.create();
getFromNetwork().subscribe(mFeaturedReplaySubject);
return mFeaturedReplaySubject;
} else {
return mFeaturedReplaySubject.onErrorResumeNext(throwable -> {
mFeaturedReplaySubject = null;
return getFeatured();
});
}
}
}
While this works, I'm afraid I'm doing something not good here on there's a case that won't be covered with this approach.
Is there any better approach?
Also for sharing the observable using subject, I read somewhere that I can use connect(), publish(), and share() but I'm not sure how to use it.
The code
private Observable<FeaturedItemList> mFeatured =
// initialized on construction
getFromNetwork()
.retry(3) // number of times to retry
.cache();
public Observable<FeaturedItemList> getFeatured() {
return mFeatured;
}
Explanation
Network source
Your getFromNetwork() function shall return regular observable, which is supposed to access network every time it is subscribed. It shall not access network when it is invoked. For example:
Future<FeaturedItemList> makeAsyncNetworkRequest() {
... initiate network request here ...
}
Observable<FeaturedItemList> getFromNetwork() {
return Observable.fromCallable(this::makeAsyncNetworkRequest)
.flatMap(Observable::fromFuture);
}
Retry
There is a family of .retryXxx() operators, which get activated on errors only. They either re-subscribe to source or pass error down the line, subject to various conditions. In case of no error these operators do nothing. I used simple retry(count) in my example, it will retry specified number of times without delay. You may add a delay or whatever complicated logic using retryWhen() (see here and here for examples).
Cache
cache() operator records the sequence of events and replays it to all new subscribers. The bad thing is that it is not refreshable. It stores the outcome of upstream forever, whether it is success or error, and never retries again.
Alternative to cache()
replay().refCount() replays events to all existing subscribers, but forgets everything as soon as all of them unsubscribe (or complete). It will re-subscribe to getFromNetwork() when new subscriber(s) arrive (with retry on error of course).
Operators mentioned but not needed
share() is a shorthand for publish().refCount(). It allows multiple concurrent subscribers to share single subscription, i.e. makes single call to subscribe() instead of doing it for every subscriber. Both cache() and replay().refCount() incorporate same functionality.

RxJava: Reading multiple subscriptions and performing an action based on their results?

I have a network call that depends on the inputs of multiple UI elements. It's basically an interface for a transaction, where the user can pick things like the amount, currency, and destination. Before the request is fired off, I need to verify everything (for example, whether or not the user's balance has the amount, whether or not the destination is valid, etc.). I have Observables for all of these network calls, but I'm not sure what the best way to go about starting all of these calls concurrently and using their results to determine what action to take.
Basically, the ideal flow is for each condition to have a failure case (which I can determine in code for each), and if any of those failure cases are met, display an error to the user saying which inputs were invalid. If all of the checks pass, fire off the transaction.
How should I go about this?
If I understood correctly, the signatures of your Observables look similar to this:
// verifier Observables which perform network calls
Observable<Verification1> test1 = ...
Observable<Verification2> test2 = ...
...
// Observable to fire the transaction
Observable<TransactionResult> fireTransaction = ...
// represents the clicks on the "go" button of the UI
Observable<Void> goButtonClicks = ...
Then you could combine all these Observables as follows:
goButtonClicks.flatMap(theVoid -> {
return Observable.zip(
test1.subscribeOn(Schedulers.io()),
test2.subscribeOn(Schedulers.io()),
(v1, v2) -> {
if (v1 and v2 pass all your requirements) {
return fireTransaction;
} else {
return Observable.just(new TransactionFailure("error"));
}
}
);
}).subscribe(transactionResult -> {
UI.showMessage(transactionResult.getMessage());
})
The .subscribeOn(Schedulers.io()) calls give you parallelism in the verification network calls, and zip allows you to "wait" on all results.
However, I guess that on the server side, you will have to do all these tests again for security reasons. So if you can change the architecture, you may want to always "fire" the transaction in the UI, let the server make the checks, and return a success/failure notification to the UI.

Categories