I'm trying to recover from errors using RxJava and GRPC. This is my observable:
Observable<Object> observable = Observable.fromCallable(() -> {
try {
Grpc.MyRequest request = Grpc.MyRequest.newBuilder()
.setToken(mToken)
.build();
Grpc.MyResponse reply = mStub.mytest(request);
return reply;
} catch (Exception e) {
///
}
}).cache();
And this is the subscription:
observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> {
})
.subscribe((result) -> {
MyResponse res = ((MyResponse) result);
if (res.getCode()!=0) {
//Check error code and try to refresh token and repeat this request after.
}
},throwable -> {
throwable.printStackTrace();
});
So, when I get the error from my GRPC service, depending on the error code, I want to try and recover from it by doing another request, and then repeating the original request. I'm not sure how to use RxJava retrywhen.
What is the most elegant way of doing something like this?
Error recovery in an observer chain does require a bit of tap dancing, and is by no means elegant. However, it can be contained in the observer chain.
boolean isRecoverable( Throwable t ) {
// this test can be as sophisticated as you want
if ( t instanceof StatusRuntimeException ) {
return true;
}
return false;
}
...
.retryWhen( throwableObservable ->
throwableObservable.flatMap( t -> isRecoverable( t )
? Observable.just("")
: Observable.error( t ) )
...
This approach allows you to decide what you want to do with the error. You could add a delay the just() so that you don't retry immediately. Instead of the just(), you could return an Observable that fetches a new API token.
Related
I`m struggling to retry my rxjava Single call after another network call is done in doOnError:
restApi.getStuff()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError {
getRefreshToken(it, AuthenticationManager.Callback{
retry(1)
})
}
.subscribeBy(
onSuccess = { response ->},
onError = { throwable ->}
)
But the retry method cannot be invoked inside the doOnError method.
Do you have any other ideas?
Eventually I used a different approach with creating an Interceptor for token authorization (#Skynet suggestion led me to it).
Here is more info about it:
Refreshing OAuth token using Retrofit without modifying all calls
if you want to check the response and then retry you should try this:
restApi.getStuff()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retryWhen(errors -> errors.flatMap(error -> {
// for some exceptions
if (error instanceof IOException) {
return Observable.just(null);
}
// otherwise
return Observable.error(error);
})
)
otherwise
restApi.getStuff()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry()
from the docs, retry() responds to onError. link
I have two async methods, that got to be called while one operation. Each method could be completed successfully or retrieve with error. On case of error, I got to retry call each method once again, with delayed of 2 sec. Mean, I should call both methods, despite of result of one of them. In error callback I want to know in which method error occured, or in both methods.
It seems I should use Completable for this, but I'm absolutely newbie in Rx.
private void method1(final CompletableEmitter e, String path){
Database.getInstance().getReference(path).addListener(new Listener() {
#Override
public void onDataChange(Data data) {
//todo something
e.onComplete();
}
#Override
public void onCancelled(DatabaseError databaseError) {
e.onError(new Throwable(databaseError.getMessage()));
}
});
}
Method2 is the same.
The following code doesn't work properly.
Completable completable1 = Completable.create(method1(e););
Completable completable2 = Completable.create(method2(e););
completable1
.doOnError(…)
.retry(1)
.andThen(completable2 //never called if completable1 gets onError each time
.retry(1)
.doOnError(…))
.subscribe(…).dispose();
You have a lot of ways to do this. I'm going just to limit to explain how to achieve this using two Completables
Let's say you have two completables:
Completable doSomething = ...
Completable doSomethingElse = ...
To execute these sequentially,
you can concatenate them using andThen operator. Then to delay a retry when an error occurs, you can use retryWhen:
doSomething.andThen(doSomethingElse)
.retryWhen { Flowable.timer(2, TimeUnit.SECONDS) }
.subscribe()
This snippet above will retry infinitely if an error is permanently occurring. To go beyond, you can limit the number of tries using:
.retryWhen { errors ->
val retryCounter = AtomicInteger()
errors.flatMap {
if (retryCounter.getAndIncrement() <= 3)
Flowable.timer(2, TimeUnit.SECONDS)
else Flowable.error(it)
}
}
If you want to retry only when a given type of error occurs, you can use:
.retryWhen { errors ->
val retryCounter = AtomicInteger()
errors.flatMap {
if (it is YourSpecficError && retryCounter.getAndIncrement() <= 3)
Flowable.timer(2, TimeUnit.SECONDS)
else Flowable.error(it)
}
}
In the case you want to retry each one independently, you can use:
doSomething.retryWhen { ... }
.andThen(doSomethingElse.retryWhen { ... })
.subscribe()
In addition, in order to avoid the retryWhen logic duplication, you could encapsulate this in an extension function:
fun Completable.retryDelayed(): Completable {
return this.retryWhen { errors ->
val retryCounter = AtomicInteger()
errors.flatMap {
if (it is YourSpecficError && retryCounter.getAndIncrement() <= 3)
Flowable.timer(2, TimeUnit.SECONDS)
else Flowable.error(it)
}
}
}
If you want to run your completables in parallel you ca use merge operator:
Completable doAll = Completable.merge(listOf(doSomething, doSomething))
I'm using a travel API which I first hit with a request to create a session and then I use the session URL returned from that URL to then call until its status parameter returns UpdatesComplete.
Here's what I have so far:
lateinit var pollUrl: String
travelInteractor.createSession("LHR", "AKL", "2018-04-20", "2018-04-22")
.doOnSubscribe {
loading.postValue(true)
}
.flatMap { url ->
pollUrl = url
travelInteractor.pollResults(pollUrl)
.retryWhen {
it.delay(1000, TimeUnit.MILLISECONDS)
}
}
.doOnNext {
if (it.status != "UpdatesComplete") travelInteractor.pollResults(pollUrl)
.retryWhen {
it.delay(1000, TimeUnit.MILLISECONDS)
}
}
.subscribe({
// Subscription stuff
)}
What's currently happening is that it will call doOnNext() and then it will make the network poll but I won't be capturing the subscription and also won't chain another poll. Is there a more efficient way I can be writing this?
Solution
Thanks to iagreen I managed to achieve this with:
lateinit var pollUrl: String
travelInteractor.createSession("LHR", "AKL", "2018-04-20", "2018-04-22")
.doOnSubscribe {
loading.postValue(true)
}
.flatMap { url ->
travelInteractor.pollResults(url)
.retryWhen {
it.delay(1000, TimeUnit.MILLISECONDS)
}
.repeatWhen {
it.delay(1000, TimeUnit.MILLISECONDS)
}
.filter {
it.itineraries.map { ... } // Use response here appropriately and then check status
it.status == "UpdatesComplete"
}
.take(1)
}
.subscribe({
// Subscription stuff
)}
I am assuming your pollResults(url) method returns a Single or an Observable that behaves like a single -- it returns one result and then onComplete. If that is truly the case, you can use repeatWhen to retry the request on success and retryWhen to retry on error. See the code below.
skyScannerInteractor.createSession("LHR", "AKL", "2018-04-20", "2018-04-22")
.doOnSubscribe {
loading.postValue(true)
}
.flatMap { url ->
skyScannerInteractor.pollResults(url)
.repeatWhen { complete -> complete.delay(1, TimeUnit.SECONDS) }
.retryWhen { errors -> errors.delay(1, TimeUnit.SECONDS) }
.filter({ it.status == "UpdatesComplete" })
.take(1) // Take the first valid value and complete
}
.subscribe({
// Subscription stuff
)}
A little explanation -
repeatWhen/retryWhen will try the request every second.
filter will cause elements with the wrong status to be ignored.
When you get the first status == "UpdatesComplete" element, take(1) will emit that value and complete -- this will have the effect of cancelling the retries.
Note: In the case of an error, retrying a network request forever is usually the wrong thing to do. I recommend you modify the retryWhen above to suit you use case to terminate in the event of network failure. For example, you could retry three times and then propagate the error. See this article for some examples on how you could do that. It is also a good reference on repeatWhen/retryWhen.
retry() and retryWhen() both respond to an onError event in the Observable, that is why it isn't actually retrying; you're not receiving those onError events in your pollResults() Observable. Right now, your retrying code doesn't actually depend on the JSON response.
There are two ways I would think about going about this:
Throw an exception in your pollResults() Observable if the JSON response is unsatisfactory. This should trigger the retryWhen(). You'll need to test for it in the Observable somewhere.
Reorganize your Observable like this:
`
lateinit var pollUrl: String
skyScannerInteractor.createSession("LHR", "AKL", "2018-04-20", "2018-04-22")
.doOnSubscribe {
loading.postValue(true)
}
.flatMap { url ->
pollUrl = url
skyScannerInteractor.pollResults(pollUrl)
}
.doOnNext {
if (it.status != "UpdatesComplete") {
throw IOException("Updates not complete.") //Trigger onError
}
}
.retryWhen { //Retry the Observable (createSession) when onError is called
it.delay(1000, TimeUnit.MILLISECONDS)
}
.subscribe({
// Will give result only when UpdatesComplete
}
I'm trying to do a simple search UI, where the text change triggers a search in the service and that gets mapped to a ViewState. It would seem easy, but the following code doesn't work:
queryText.filter { it.length > 3 }
.switchMap { service.search(it) }
.onErrorReturn { SearchResponse(null, it.message) }
.map { SearchViewState(items = it.items, error = it.error) }
.startWith { SearchViewState(loading = true) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { viewState.onNext(it) }
I've no idea what I did wrong, but through debugging I can see that the stream throws a NetworkOnMainThreadException and then terminates so new events are no longer processed.
What is the correct way to do this?
I assume queryText is the source of textchanges which happen on the main thread. Therefore subscribeOn has no effect on it. You should apply subscribeOn to the actual network call:
queryText.filter { it.length > 3 }
.switchMap {
service.search(it)
.subscribeOn(Schedulers.io())
.onErrorReturn { SearchResponse(null, it.message) }
.map { SearchViewState(items = it.items, error = it.error) }
.startWith ( SearchViewState(loading = true) )
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe { viewState.onNext(it) }
In addition, I think you have to do the error recovery and state changes associated with the particular network call, otherwise a failure will stop the entire sequence.
I am looking to poll the backend call for certain number of times for a predefined regular intervals. I would like to exit the loop if I have received an expected payload in between the loop and update the UI else terminate the polling.
Below is the code I normally do when I make standard http call.
//Response Model from backend API
public class ApplicationStatusResponse
{
public boolean isActive;
}
//Retrofit facade
#POST(v1/api/applicationStatus)
Single<ApplicationStatusResponse> checkApplicationStatus(#Body ApplicationStatusRequest applicationRequest);
-----
DisposableSingleObserver<ApplicationStatusResponse> disposableSingleObserver = new DisposableSingleObserver<ApplicationStatusResponse>() {
#Override
public void onSuccess(ApplicationStatusResponse response) {
// Update UI Here
}
#Override
public void onError(Throwable e) {
}
};
CompositeDisposable compositeDisposable = new CompositeDisposable();
// Following call works alaways works
DisposableSingleObserver<ApplicationStatusResponse> disposable = originationRepo.checkApplicationStatus(applicationStatusRequest)
.observeOn(schedulerProvider.mainThread())
.subscribeWith(disposableSingleObserver);
compositeDisposable.add(disposable);
But I am kind of lost here in the following code with the syntax error and I am not able to use the same disposableSingleObserver when calling from the Flowable.interval and need help with my use case where I need to update the UI the status regularly until the time is elapsed or status is active which ever happens first and also I am not after terminating the polling if I received HTTP Status Code of 500 instead repeat until the mentioned conditions are met.
//Help Needed here when I need polling in regular interval - I am kind of the syntax error complain from Android Studio
int INITIAL_DELAY = 0;
int POLLING_INTERVAL = 1000;
int POLL_COUNT = 8;
disposable = Flowable
.interval(INITIAL_DELAY, POLLING_INTERVAL, TimeUnit.MILLISECONDS)
.map(x -> originationRepo.checkApplicationStatus(applicationStatusRequest))
.take(POLL_COUNT) ??
// How can I receive the response payload and update the UI
compositeDisposable.add(disposable);
Appreciate your help in advance.
(in continuation with MyDogTom's answer you could also "short-circuit" the observable by throwing a custom Error/Exception)
Option 3:
disposable = Flowable
.interval(INITIAL_DELAY, POLLING_INTERVAL, TimeUnit.MILLISECONDS)
.map(x -> originationRepo.checkApplicationStatus(applicationStatusRequest)) // .flatMap (?)
.take(POLL_COUNT) //YES
.doOnNext() // update UI here
.map(response -> {
if(!response.checkCondition()) {
throw new ShortCircuitException();
}
return response.data();
})
.onErrorResumeNext(throwable -> (throwable instanceof ShortCircuitException)
? Observable.empty()
: Observable.error(throwable))
Option #1 Use filter + take(1)
disposable = Flowable
.interval(INITIAL_DELAY, POLLING_INTERVAL, TimeUnit.MILLISECONDS)
.map(x -> originationRepo.checkApplicationStatus(applicationStatusRequest))
.take(POLL_COUNT) //YES
.doOnNext() // update UI here
.map(response -> ) // should stop condition. true - stop, false - continue
.filter(!shouldContinue)
.take(1)
Option #2 Use Subject + takeUntil
Subject<Boolean> stopSubject = PublishSubject.create();
disposable = Flowable
.interval(INITIAL_DELAY, POLLING_INTERVAL, TimeUnit.MILLISECONDS)
.takeUntil(stopSubject.asObservable())
.map(x -> originationRepo.checkApplicationStatus(applicationStatusRequest))
.take(POLL_COUNT) //YES
.subscribe(
response -> {
//update UI
boolean shouldStop = ... // calculate
if (shouldStop) {
stopSubject.onNext(true);
}
}
...
)
PS. This is pseudo code. I hope you get idea.