Limit actions with rxJava and retryWhen operator - android

My application must do two things in general:
Accept only one network request at the same time
Retry if request failed
That's how I implement it:
public class RequestsLocker {
private volatile boolean isLocked;
public <T> Observable.Transformer<T, T> applyLocker() {
if(!isLocked()) {
return observable -> observable
.doOnSubscribe(() -> {
lockChannel();
})
.doOnUnsubscribe(() -> {
freeChannel();
});
} else {
return observable -> Observable.error(new ChannelBusyException("Channel is busy now."));
}
}
private void lockChannel() {
isLocked = true;
}
private void freeChannel() {
isLocked = false;
}
public boolean isLocked() {
return isLocked;
}
}
Looks nice.
Now my retryWhen implementation:
public static Observable<?> retryWhenAnyIoExceptionWithDelay(Observable<? extends Throwable> observable) {
return observable.flatMap(error -> {
// For IOExceptions, we retry
if (error instanceof IOException) {
return Observable.timer(2, TimeUnit.SECONDS);
}
// For anything else, don't retry
return Observable.error(error);
});
}
There is how I use it:
public Observable<List<QueueCarItem>> finishService(int id, PaymentType paymentType, String notes) {
return carsQueueApi.finishService(id, new FinishCarServiceRequest(paymentType.getName(), notes))
.compose(requestsLocker.applyLocker(RequestsLocker.RequestChannel.CHANGE));
}
...
public void finishCarService(QueueCarItem carItem, PaymentType paymentType,
String notes, Subscriber<List<QueueCarItem>> subscriber) {
queueApiMediator.finishService(carItem.getId(), paymentType, notes)
.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
.doOnError(this::handleError)
.retryWhen(RxOperatorsHelpers::retryWhenAnyIoExceptionWithDelay)
.subscribe(subscriber);
}
The main problem that doOnUnsubscribe() called on any error and then locker is open for any new request until the timer expires and resubscribing happens again. That's the problem. While the timer is ticking user can make another request.
How I can fix it?

The problem is that you're applying your transformer to the source observable i.e. before your retrywhen.
When there is an error you're always going to unsubscribe from and then resubscribe to the source observable
leading to your doOnUnsubscribe being called.
I suggest you try
public Observable<List<QueueCarItem>> finishService(int id, PaymentType paymentType, String notes) {
return carsQueueApi.finishService(id, new FinishCarServiceRequest(paymentType.getName(), notes));
}
public void finishCarService(QueueCarItem carItem, PaymentType paymentType,
String notes, Subscriber<List<QueueCarItem>> subscriber) {
queueApiMediator.finishService(carItem.getId(), paymentType, notes)
.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
.doOnError(this::handleError)
.retryWhen(RxOperatorsHelpers::retryWhenAnyIoExceptionWithDelay)
.compose(requestsLocker.applyLocker(RequestsLocker.RequestChannel.CHANGE));
.subscribe(subscriber);
}
PS: The apply locker transformer looks a bit different i.e. it doesn't take an argument in the code you linked.

Using retryWhen, to avoid unsubscribe onError you must use onErrorResumeNext which wont unsubscribe you.
Take a look of this example
/**
* Here we can see how onErrorResumeNext works and emit an item in case that an error occur in the pipeline and an exception is propagated
*/
#Test
public void observableOnErrorResumeNext() {
Subscription subscription = Observable.just(null)
.map(Object::toString)
.doOnError(failure -> System.out.println("Error:" + failure.getCause()))
.retryWhen(errors -> errors.doOnNext(o -> count++)
.flatMap(t -> count > 3 ? Observable.error(t) : Observable.just(null)),
Schedulers.newThread())
.onErrorResumeNext(t -> {
System.out.println("Error after all retries:" + t.getCause());
return Observable.just("I save the world for extinction!");
})
.subscribe(s -> System.out.println(s));
new TestSubscriber((Observer) subscription).awaitTerminalEvent(500, TimeUnit.MILLISECONDS);
}
Also about concurrency, if you do an operation in a flatMap operator, you can specify the Max concurrent.
public final <R> Observable<R> flatMap(Func1<? super T, ? extends Observable<? extends R>> func, int maxConcurrent) {
if (getClass() == ScalarSynchronousObservable.class) {
return ((ScalarSynchronousObservable<T>)this).scalarFlatMap(func);
}
return merge(map(func), maxConcurrent);
}
You can see more examples here https://github.com/politrons/reactive

My current solution is not to unlock RequestLocker on IoException as in this case request will be repeated after delay.
public <T> Observable.Transformer<T, T> applyLocker() {
if(!isLocked()) {
return observable -> observable.doOnSubscribe(() -> {
lockChannel();
}).doOnNext(obj -> {
freeChannel();
}).doOnError(throwable -> {
if(throwable instanceof IOException) {
return; // as any request will be repeated in case of IOException
}
freeChannel(channel);
});
} else {
return observable -> Observable.error(new ChannelBusyException("Channel is busy now"));
}
}

Related

RXJava Handle chain api calls: show different error message and continue the stream

I'm trying to convert a callback hell to RX but I'm stuck with getting the proper order, below is my functionality I want to achieve
a) User Login-> get the Auth Cookies, if login credentials invalid show error message
b) use the Auth Cookies to get Customer Type,
c) if the Customer Type is zero/ show profile Restricted Error Message and log out the user
d) if the customerType, not zero proceed to get the other customer Details
e) if any of the customer APIs returns an error response, log out the user and show login failure message
f) if all customer API success show the home screen
API
Login
#FormUrlEncoded
#POST("distauth/UI/Login")
Single<Response<Void>> doLogin1(#Field("username") String username, #Field("password") String password,
#Field("rememberme") String rememberMe, #Field("answer") String answer,
#QueryMap Map<String, String> options);
public Single<Boolean> doLogin(#NonNull String username, #Nullable String password) {
return authapi.doLogin1(username, password, "y", "", logiOptions)
.flatMap(new Function<Response<Void>, SingleSource<Boolean>>() {
#Override
public SingleSource<Boolean> apply(Response<Void> response) throws Exception {
if (response.code() == HttpStatus.MOVED_TEMPORARILY.value()
&& !StringUtils.isEmpty(Session.getCookie())
) {
return Single.just(true);
}
throw new Exception("Invalid Login Details");
}
});
}
//==========
Logout
#FormUrlEncoded
#POST("distauth/UI/Logout")
#Headers("Cache-Control: no-cache")
Completable doLogout(#Field("logout") boolean logout); //return 302 HTTP Status code with empty iPlanetCookie
//==========
NOTE: Loing/logout is not a REST API, this legacy app implement as Form Post ;) so when the success of login return 302 with cookies, and log out also return 302 as status code
Get Customer Details
Single<CustomerAccountVO> getCustomerAccountDetails(boolean forceRefresh);
//==========
Single<CustomerType> getCustomerUserProfile(boolean forceRefresh);
#Override
public Single<CustomerType> getCustomerUserProfile(boolean applyResponseCache) {
return this.mCustomerRemoteDataStore.getCustomerUserProfile(applyResponseCache)
.doOnSuccess(new Consumer<CustomerType>() {
#Override
public void accept(CustomerType customerType) throws Exception {
if (customerType != null && customerType.getBody() != null &&
!StringUtils.isEmpty(customerType.getBody())) {
if (customerType.getBody().equalsIgnoreCase(AppConfig.ERROR)) {
throw new CustomerProfileNotFound(500, "user account restrictions");
} else {
mCustomerLocalRepository.saveCustomerType(customerType);
}
}
}
}).doOnError(new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG, "error occurred while getting customer user profile", throwable);
}
});
}
//==========
Single<CustomerAccountId> getAccountId(boolean forceRefresh);
//==========
Single<Customer> getCustomer(boolean forceRefresh);
//==========
Get Customer Full Details
Single<CustomerDetails> getCustomerFullDetails(boolean applyResponseCache);
Implementation:
#Override
public Single<CustomerDetails> getCustomerFullDetails(boolean forceRefresh) {
Single<CustomerDetails> customerDetails = Single.zip(
getCustomerUserProfile(forceRefresh).subscribeOn(Schedulers.io()),
getAccountId(forceRefresh).subscribeOn(Schedulers.io()),
getCustomerAccountDetails(false).subscribeOn(Schedulers.io()),
getCustomer(forceRefresh).subscribeOn(Schedulers.io()), new Function4<CustomerType, CustomerAccountId,
CustomerAccountVO, Customer, CustomerDetails>() {
#Override
public CustomerDetails apply(#NonNull CustomerType customerType,
#NonNull CustomerAccountId customerAccountId,
#NonNull CustomerAccountVO customerAccountVO,
#NonNull Customer customer) throws Exception {
return CustomerDetails.builder().customerType(customerType).customerAccountVO
(customerAccountVO).customer(customer).customerAccountId(customerAccountId).
build();
}
});
return customerDetails;
}
//==========
Each customer request is independent so I thought to execute as sperate thread and zip the final result/
Single<BaseServerResponse> updateCustomerDetails(#Nonnull boolean secure, int secureRequestCode, #Nonnull JSONObject customerContact);
//Presenter Implementation: this implementation not working as i expect above, can some one help me to get this correct,
public void doLoginHandler(#NonNull String username, #NonNull String password) {
checkViewAttached();
getMvpView().showLoadingIndicator();
addSubscription(
apiService.doLogin2(username, password)
.subscribeOn(Schedulers.io())
.flatMap(new Function<Boolean, SingleSource<CustomerDetails>>() {
#Override
public SingleSource<CustomerDetails> apply(Boolean aBoolean) throws Exception {
if (aBoolean) {
//get customr Full Details
Log.d(TAG, "apply: "+aBoolean);
return customerRepository.getCustomerFullDetails(true);
}
return null;
}
}).observeOn(AndroidSchedulers.mainThread())
.onErrorResumeNext(new Function<Throwable, SingleSource<? extends CustomerDetails>>() {
#Override
public SingleSource<? extends CustomerDetails> apply(Throwable throwable) throws Exception {
if (throwable instanceof CustomerProfileNotFound) {
getMvpView().showUserProfileAccessRestrictMessage();
} else {
getMvpView().onLoginAuthFailure();
}
return Single.just(CustomerDetails.builder().errorOccurred(true).build());
}
})
.flatMapCompletable(new Function<CustomerDetails, CompletableSource>() {
#Override
public CompletableSource apply(CustomerDetails customerDetails) throws Exception {
if(customerDetails.isErrorOccurred()){
return apiService.doLogout();
}
return Completable.complete();
}
})
.subscribe(new Action() {
#Override
public void run() throws Exception {
getMvpView().onLoginAuthSuccess();
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
if (throwable instanceof CustomerProfileNotFound) {
getMvpView().showUserProfileAccessRestrictMessage();
} else {
getMvpView().onLoginAuthFailure();
}
}
}));
}
First I'll state the problem with your code.
.flatMapCompletable(new Function<CustomerDetails, CompletableSource>() {
#Override
public CompletableSource apply(CustomerDetails customerDetails) throws Exception {
if(customerDetails.isErrorOccurred()){
return apiService.doLogout();
}
return Completable.complete();
}
})
This chain observable (which is the one you subscribe to) is always going to give a Completed state unless a network error happens when calling the logout API, that's because you either return the logout Completable or an instant Completable.
Secondly, I think the solution is in logically sorting everything out, the key to error handling in such a case would be creating a different Exception for each error case with it's own error message,
it can go like this (I'm just using the logical names, hopefully that will give you the idea):
loginObservable.flatMap { authCredentials -> {
if (authCredentials.isValid())
return getCustomerTypeObservable(authCredentials)
else
return Single.error(InvalidCredentialsException("message goes here (optional)"))
}}.flatMap { type -> {
if (type == 0)
return Single.error(ProfileRestrictedException("different message maybe?"))
else
return getCustomerDetailsZippedObservable(type)
}}
/* ..etc */
Then at the subscription site you do something like:
myObservable.subscribe( {
/* Handle success*/
}, { exception ->
when(exception) {
is InvalidCredentialsException -> mvpView.showError(message)
is ProfileRestrictedException -> {
mvpView.showError(message)
logout()
}
else -> /* Handle an exception that is not listed above */
}
} )
This way IMO is more convenient than using onErrorResumeNext.
EDIT: You can also overcome the issue stated above by doing something like:
.flatMapCompletable { customerDetails -> {
if(customerDetails.isErrorOccurred()){
return apiService.doLogout()
.then(Completable.error(LoginFailedException("Message"))) /* This will guarantee the stream terminates with the required error type after logout is successful */
} else {
return Completable.complete()
}
}}

How can I create a stream in RxJava2 (Android) that has no input value, but produces a String?

In Android, I want to use the call AdvertisingIdClient.getAdvertisingIdInfo(getContext()).getId() on a separate thread (IO-thread) and handle the string on the main thread.
I wan't to do this with RxJava2.
This is what I have now: (which works)
SingleOnSubscribe<String> source = new SingleOnSubscribe<String>() {
#Override
public void subscribe(SingleEmitter<String> e) throws Exception {
e.onSuccess(AdvertisingIdClient.getAdvertisingIdInfo(getContext()).getId());
}
};
Single.create(source)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
Timber.e(throwable.getMessage());
}
})
.subscribe(new Consumer<String>() {
#Override
public void accept(String s) throws Exception {
advertisingId = s;
}
});
What I would prefer, which is purely taste, is if I could "just" create the stream and handle it all in the flow of methods. Such as: (warning, super pseudo code)
Completable
.empty()
.produce(() -> String {
return makeString();
})
.sub/obs-On()...
.subscribe(coolString -> {mVariable = coolString})
So, Make an Observable and turn it into an Observable by executing some function.
Just use defer or fromCallable like in this example:
Observable<String> stringObservable = Observable.fromCallable(() -> {
return getStuff();
});
Test
#Test
public void fromCallable() throws Exception {
Observable<String> stringObservable = Observable.fromCallable(() -> {
return getStuff();
});
ExecutorService executorService = Executors.newSingleThreadExecutor(runnable -> {
return new Thread(runnable, "myFancyThread");
});
Scheduler scheduler = Schedulers.from(executorService);
TestObserver<String> test = stringObservable.subscribeOn(scheduler)
.test();
test.await()
.assertResult("wurst");
assertThat(test.lastThread().getName()).contains("myFancyThread");
}
private String getStuff() {
return "wurst";
}

RxJava make retryWhen() fire to onError() method

I have a following class with RxJava implementation for download two resources from API. I do some rx to allow it retry or repeat when it don't meet api/connection requirements. However, I cannot make retryWhen() fire onError() after attempt more than 3 times.
QUESTION / HELP
please, review my code and help me solve this issue.
NOTE
I'm implement Rx by read these two articles.
article 1, article 2
SplashPresenter.class
public class SplashPresenter implements SplashContract.Presenter {
private static final String TAG = SplashPresenter.class.getName();
private static final int RETRY_TIMEOUT = 10;
private static final int STOP_RETRY_TIME = 3;
private static final int START_RETRY_TIME = 1;
private SplashContract.View mView;
#Override
public void init(SplashContract.View view) {
this.mView = view;
}
#Override
public void onResume() {
GetRemoteReceiverRelationshipSpec relationSpec = new GetRemoteReceiverRelationshipSpec();
GetRemoteIncompleteReasonSpec reasonSpec = new GetRemoteIncompleteReasonSpec();
Observable<RepoResult<ArrayList<IncompleteReasonViewModel>>> queryReason =
Repository.getInstance().query(reasonSpec);
Repository.getInstance().query(relationSpec)
.concatMap(result -> queryReason)
.repeatWhen(complete -> complete
.zipWith(Observable.range(START_RETRY_TIME, STOP_RETRY_TIME), (v, i) -> i)
.flatMap(repeatCount -> {
Log.i(TAG, "Repeat attempt: " + repeatCount);
mView.showLoadingDialog();
return Observable.timer(RETRY_TIMEOUT,
TimeUnit.SECONDS);
}))
.takeUntil(RepoResult::isSuccess)
.retryWhen(error -> error
.zipWith(Observable.range(START_RETRY_TIME, STOP_RETRY_TIME), (v, i) -> i)
.flatMap(retryCount -> {
Log.i(TAG, "Retry attempt: " + retryCount);
mView.showLoadingDialog();
if (mView.getCommunicator() != null) {
mView.getCommunicator().onConnectionFail(retryCount);
}
return Observable.timer(RETRY_TIMEOUT,
TimeUnit.SECONDS);
}))
.filter(RepoResult::isSuccess)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
result -> Log.i(TAG, "onNext()"),
err -> {
Log.i(TAG, "onError()");
if (mView.getCommunicator() != null) {
mView.dismissLoadingDialog();
mView.getCommunicator().onSplashScreeDismissError();
}
},
() -> {
Log.i(TAG, "onComplete()");
if (mView.getCommunicator() != null) {
mView.dismissLoadingDialog();
mView.getCommunicator().onSplashScreenSuccessDismiss();
}
}
);
}
#Override
public void onPause() {
}
}
To retain the throwable after retrying (instead of emitting a custom one), return an Observable with the appropriate error from the zipWith operator when retryCount is over the specified limit.
.retryWhen(error -> {
Observable<Integer> range = Observable.range(START_RETRY_TIME, STOP_RETRY_TIME);
Observable<Observable<Long>> zipWith = error.zipWith(range, (e, i) ->
i < STOP_RETRY_TIME ?
Observable.timer(i, TimeUnit.SECONDS) :
Observable.error(e));
return Observable.merge(zipWith);
});
When I wrote similar code before, I had manually threw Observable.error() in flatMap
.flatMap(retryCount -> {
if (retryCount >= STOP_RETRY_TIME) {
return Observable.error(someException);
}
return Observable.timer(RETRY_TIMEOUT, TimeUnit.SECONDS);
}))
Pass the retry counter and error into a Pair object if you've reached the retry limit get the exception from pair object and raise it.
source.retryWhen(errors -> errors
.zipWith(Observable.range(1, REQUEST_RETRY), Pair::new)
.flatMap(errPair -> {
int retryTime = errPair.second;
if (retryTime < REQUEST_RETRY) {
return Observable.timer(retryTime * RETRY_DELAY, TimeUnit.MILLISECONDS);
} else {
return Observable.error(errPair.first);
}
})
.doOnError(this::handleError));

Connecting RxJava Observables in layers

I have 3 layers in my app. Layer1 subscribes to Observable from layer2. Layer2 subscribes to layer3 in order to emit returned data to layer1.
Layer1
layer2.getData(data).subscribe(newData -> {Log.d("onNext", "returned");},
throwable -> {Log.d("onError", throwable.getMessage());});
Suppose layer3 has a method called downloadDataFromApi(data);
public Observable<Data> getData(String data) {
return Observable.create(new Observable.OnSubscribe<Data>() {
#Override
public void call(Subscriber<? super Data> subscriber) {
Data data = new Data();
subscriber.onNext(data);
subscriber.onCompleted();
// Can't find a way to connect to layer3.
}
});
}
What do I need to do in layer2's getData() method? I basically want to have logics before returning Observable back to layer1.
Does that make sense?
Just return the Observable directly. Then layer1 handles subscription as usual.
class Layer2 {
public Observable<Data> getData(String data) {
return layer3.getData(data);
}
}
From what I see you have 3 layers (presentation, business logic, data access).
So what you could do is the following:
class PresentationLayer {
private BusinessLogicLayer layer;
PresentationLayer() {
layer = new BusinessLogicLayer();
}
public void showName() {
layer.getNameWithoutRxPrefix()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<String>() {
#Override
public void accept(String name) throws Exception {
// show name somewhere
Log.d("PresentationLayer", "name: " + name);
}
});
}
}
class BusinessLogicLayer {
private DataAccessLayer layer;
BusinessLogicLayer() {
layer = new DataAccessLayer();
}
public Observable<String> getNameWithoutRxPrefix() {
return layer.getName()
.map(new Function<String, String>() {
#Override
public String apply(String name) throws Exception {
return name.replace("Rx", "");
}
});
}
}
class DataAccessLayer {
public Observable<String> getName() {
return Observable.just("RxAndroid");
}
}
As you can see, I return an Observable in my data access layer (getName), and chain another method to it in my business logic method (map) before returning it to the presentation layer.

InterruptedIOException when switching from mainThread() to io()

I have some code that first has to run on AndroidSchedulers.mainThread(), then has to do a HTTP request, so has to run on Schedulers.io(), and handle the result on UI, so back to AndroidSchedulers.mainThread().
I receive InterruptedIOException when switching from AndroidSchedulers.mainThread() to Scheulers.io().
Here's some code:
Model model = getModel();
Completable.fromAction(
new Action0() {
public void call() {
mSubject.onNext(model)
}
})
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
.andThen(fetchFromServer())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(/* handle success and error */);
...
public <T> Single<T> fetchFromServer() {
Request request = new Request(); // some request from server, not important
return bodyFrom2(request);
}
public <T> Single<T> bodyFrom2(final Request<T> request) {
return Single.defer(new Callable<Single<T>>() {
#Override
public Single<T> call() throws Exception {
try {
Response<T> response = request.execute();
if (response.error() != null)
return Single.error(response.error().getMessage());
else {
return Single.just(response.body());
}
} catch (IOException e) {
return Single.error(e);
}
}
});
}
public static <T> Single<T> bodyFrom1(final Request<T> request) {
return Single.create(new Single.OnSubscribe<T>() {
#Override
public void call(SingleSubscriber<? super T> subscriber) {
try {
Response<T> response = request.execute();
if (subscriber.isUnsubscribed())
return;
if (response.error() != null)
subscriber.onError(response.error().getMessage());
else {
subscriber.onSuccess(response.body());
}
} catch (IOException e) {
if (subscriber.isUnsubscribed())
return;
subscriber.onError(e);
}
}
});
}
The exception is thrown in bodyFrom() (1 or 2), at request.execute().
I used bodyFrom1(), but I found this question on SO and thought about trying with the second one. Regardless, I receive the exception.
Trying to find what and where the problem is, I tried this:
Completable.complete()
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
.andThen(fetchFromServer())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(/* handle success and error */);
which still throws InterruptedIOException, and this:
Completable.complete()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.io())
.andThen(fetchFromServer())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(/* handle success and error */);
which works fine.
EDIT:
It seems to work if I'm using Observable or Single instead of Completable.
Added an issue on RxAndroid's Github.

Categories

Resources