I'm working on a large codebase with a lot of network calls. It currently works with Retrofit 1.9 and RxJava 1.x. But, we're trying to go to RxJava 2.x.
I currently keep getting a NetworkOnMainThreadException on our RxJava 2 code. I'm using RxJava 2.x and Retrofit 1.9. The plan is to go to Retrofit 2.x later. But, for now, we need to make this work with RxJava 2 + Retrofit 1.9.
The code in question is:
observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new DisposableObserver<Void>() {
#Override
public void onNext(Void response) {
if (listener != null) {
listener.onSuccess(response);
}
}
#Override
public void onError(Throwable e) {
if (listener != null) {
listener.onError(e);
}
}
#Override
public void onComplete() {
//do nothing for now
}
});
Any ideas how the network operation is ending up on the UI thread when I told it to subscribeOn(Schedulers.io())?
Edit: by request here's the stacktrace. I genericized filenames and such because I'm not allowed to divulge those details...
retrofit.RetrofitError
at retrofit.RestAdapter$RestHandler.invokeRequest(RestAdapter.java:400)
at retrofit.RestAdapter$RestHandler.invoke(RestAdapter.java:240)
at java.lang.reflect.Proxy.invoke(Proxy.java:913)
at <package-name>.network.api.$Proxy40.someApiMethod(Unknown Source)
at <package-name>.SomeApi.someApiMethod(SomeApi.java:30)
Caused by: android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1425)
at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:102)
at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:90)
at java.net.InetAddress.getAllByName(InetAddress.java:787)
at com.squareup.okhttp.internal.Network$1.resolveInetAddresses(Network.java:29)
at com.squareup.okhttp.internal.http.RouteSelector.resetNextInetSocketAddress(RouteSelector.java:224)
at com.squareup.okhttp.internal.http.RouteSelector.nextProxy(RouteSelector.java:193)
at com.squareup.okhttp.internal.http.RouteSelector.next(RouteSelector.java:113)
at com.squareup.okhttp.internal.http.HttpEngine.createNextConnection(HttpEngine.java:344)
at com.squareup.okhttp.internal.http.HttpEngine.nextConnection(HttpEngine.java:329)
at com.squareup.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:319)
at com.squareup.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:241)
at com.squareup.okhttp.Call.getResponse(Call.java:271)
at com.squareup.okhttp.Call$ApplicationInterceptorChain.proceed(Call.java:228)
at com.squareup.okhttp.Call.getResponseWithInterceptorChain(Call.java:199)
at com.squareup.okhttp.Call.execute(Call.java:79)
at retrofit.client.OkClient.execute(OkClient.java:53)
Related
I am new to using RxAndroid and RxJava. I am using RxJava + Retrofit2 to make GET requests. I am doing approximately 1500 GET requests using the following code and getting Out of memory error. However the same code this time with only retrofit, NO RxAndroid and it works. So my conclusion was I am doing something wrong in RxAndroid. Can you please help with what I am missing?
Code Sample:
Subject<Story> mStoryEmitter = PublishSubject.create();
private void getStory(int storyID) {
HNApi.Factory.getInstance().getStory(storyID).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getStoryObserver());
}
mStoryListEmitter.subscribe(new Observer<List<Integer>>() {
#Override
public void onSubscribe(Disposable d) {}
#Override
public void onNext(List<Integer> value) {
if(mRecyclerView != null) {
mRecyclerView.setAdapter(null);
if(mAdapter != null) {
mAdapter.clear();
mAdapter = null;
}
}
mAdapter = new SimpleRecyclerViewAdapter();
mRecyclerView.setAdapter(mAdapter);
for(Integer storyID : value) {
getStory(storyID);
}
}
#Override
public void onError(Throwable e) {}
#Override
public void onComplete() {}
});
private DisposableObserver<Story> getStoryObserver() {
DisposableObserver<Story> observer = new DisposableObserver<Story>() {
#Override
public void onNext(Story value) {
mStoryEmitter.onNext(value);
dispose();
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
};
return observer;
}
Error:
Throwing OutOfMemoryError "Could not allocate JNI Env"
java.lang.IllegalStateException: Fatal Exception thrown on Scheduler.
at io.reactivex.android.schedulers.HandlerScheduler$ScheduledRunnable.run(HandlerScheduler.java:111)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
Caused by: java.lang.OutOfMemoryError: Could not allocate JNI Env
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1063)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:921)
at java.util.concurrent.ThreadPoolExecutor.ensurePrestart(ThreadPoolExecutor.java:1556)
at java.util.concurrent.ScheduledThreadPoolExecutor.delayedExecute(ScheduledThreadPoolExecutor.java:310)
at java.util.concurrent.ScheduledThreadPoolExecutor.schedule(ScheduledThreadPoolExecutor.java:543)
at java.util.concurrent.ScheduledThreadPoolExecutor.submit(ScheduledThreadPoolExecutor.java:642)
at io.reactivex.internal.schedulers.NewThreadWorker.scheduleActual(NewThreadWorker.java:120)
at io.reactivex.internal.schedulers.IoScheduler$EventLoopWorker.schedule(IoScheduler.java:221)
at io.reactivex.Scheduler.scheduleDirect(Scheduler.java:130)
at io.reactivex.Scheduler.scheduleDirect(Scheduler.java:109)
AppData::create pipe(2) failed: Too many open files
at io.reactivex.internal.operators.observable.ObservableSubscribeOn.subscribeActual(ObservableSubscribeOn.java:36)
at io.reactivex.Observable.subscribe(Observable.java:10514)
at io.reactivex.internal.operators.observable.ObservableObserveOn.subscribeActual(ObservableObserveOn.java:44)
at io.reactivex.Observable.subscribe(Observable.java:10514)
at com.example.MainActivity.getStory(MainActivity.java:100)
at com.example.MainActivity.access$300(MainActivity.java:25)
at com.example.MainActivity$2.onNext(MainActivity.java:67)
at com.example.MainActivity$2.onNext(MainActivity.java:49)
at io.reactivex.subjects.PublishSubject$PublishDisposable.onNext(PublishSubject.java:263)
at io.reactivex.subjects.PublishSubject.onNext(PublishSubject.java:182)
at com.example.MainActivity$5.onNext(MainActivity.java:147)
at com.example.MainActivity$5.onNext(MainActivity.java:138)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.drainNormal(ObservableObserveOn.java:198)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.run(ObservableObserveOn.java:250)
at io.reactivex.android.schedulers.HandlerScheduler$ScheduledRunnable.run(HandlerScheduler.java:109)
... 7 more
AppData::create pipe(2) failed: Too many open files
FATAL EXCEPTION: main
Process: com.example, PID: 15857
java.lang.IllegalStateException: Fatal Exception thrown on Scheduler.
at io.reactivex.android.schedulers.HandlerScheduler$ScheduledRunnable.run(HandlerScheduler.java:111)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
Caused by: java.lang.OutOfMemoryError: Could not allocate JNI Env
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1063)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:921)
at java.util.concurrent.ThreadPoolExecutor.ensurePrestart(ThreadPoolExecutor.java:1556)
at java.util.concurrent.ScheduledThreadPoolExecutor.delayedExecute(ScheduledThreadPoolExecutor.java:310)
at java.util.concurrent.ScheduledThreadPoolExecutor.schedule(ScheduledThreadPoolExecutor.java:543)
at java.util.concurrent.ScheduledThreadPoolExecutor.submit(ScheduledThreadPoolExecutor.java:642)
at io.reactivex.internal.schedulers.NewThreadWorker.scheduleActual(NewThreadWorker.java:120)
at io.reactivex.internal.schedulers.IoScheduler$EventLoopWorker.schedule(IoScheduler.java:221)
at io.reactivex.Scheduler.scheduleDirect(Scheduler.java:130)
at io.reactivex.Scheduler.scheduleDirect(Scheduler.java:109)
at io.reactivex.internal.operators.observable.ObservableSubscribeOn.subscribeActual(ObservableSubscribeOn.java:36)
at io.reactivex.Observable.subscribe(Observable.java:10514)
at io.reactivex.internal.operators.observable.ObservableObserveOn.subscribeActual(ObservableObserveOn.java:44)
at io.reactivex.Observable.subscribe(Observable.java:10514)
at com.example.MainActivity.getStory(MainActivity.java:100)
at com.example.MainActivity.access$300(MainActivity.java:25)
at com.example.MainActivity$2.onNext(MainActivity.java:67)
at com.example.MainActivity$2.onNext(MainActivity.java:49)
at io.reactivex.subjects.PublishSubject$PublishDisposable.onNext(PublishSubject.java:263)
at io.reactivex.subjects.PublishSubject.onNext(PublishSubject.java:182)
at com.example.MainActivity$5.onNext(MainActivity.java:147)
at com.example.MainActivity$5.onNext(MainActivity.java:138)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.drainNormal(ObservableObserveOn.java:198)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.run(ObservableObserveOn.java:250)
at io.reactivex.android.schedulers.HandlerScheduler$ScheduledRunnable.run(HandlerScheduler.java:109)
... 7 more
Posted the same question at RxAndroid github.
And JakeWharton reply acually heped
The problem is that Schedulers.io() uses a cached thread pool without a limit and thus is trying to create 1500 threads. You should consider using a Scheduler that has a fixed limit of threads, or using RxJava 2.x's parallel() operator to parallelize the operation to a fixed number of workers.
If you're using raw Retrofit by default it uses OkHttp's dispatcher which limits the threads to something like 64 (with a max of 5 per host). That's why you aren't seeing it fail.
If you use createAsync() when creating the RxJava2CallAdapterFactory it will create fully-async Observable instances that don't require a subscribeOn and which use OkHttp's Dispatcher just like Retrofit would otherwise. Then you only need observeOn to move back to the main thread, and you avoid all additional thread creation.
Add all your Disposables to CompositeDisposable and dispose it for every cycle
CompositeDisposable disposable = new CompositeDisposable();
mStoryListEmitter.subscribe(new Observer<List<Integer>>() {
#Override
public void onSubscribe(Disposable d) {
disposable.add(d); // adding disposable
}
#Override
public void onNext(List<Integer> value) {
if(mRecyclerView != null) {
mRecyclerView.setAdapter(null);
if(mAdapter != null) {
mAdapter.clear();
mAdapter = null;
}
}
mAdapter = new SimpleRecyclerViewAdapter();
mRecyclerView.setAdapter(mAdapter);
for(Integer storyID : value) {
getStory(storyID);
}
}
#Override
public void onError(Throwable e) {}
#Override
public void onComplete() {
diposable.dispose(); // <--- Disposing on complete
}
})
;
I'm having tons of ConcurrentModificationException when doing some update operations with my database using ORMlite. I'm wrapping the code that does the actual update into rx.Observables to make it asynchronously. Looks like this:
#Override
public void setFavoriteTeams(List<Team> teams) {
final Iterator<Team> it = teams.iterator();
Observable.create(new Observable.OnSubscribe<Object>() {
#Override
public void call(final Subscriber<? super Object> subscriber) {
try {
List<String> teamIds = new ArrayList<>();
while (it.hasNext()) {
Team team = it.next();
teamIds.add(team.getmSipId());
}
setFavoriteTeamsSql(teamIds);
} catch (Exception e) {
handleException(e);
} finally {
subscriber.onNext(null);
subscriber.onCompleted();
}
}
}).subscribeOn(Schedulers.computation()).subscribe();
}
Ironically, a while ago I started using Interator instead of a for-loop to avoid any ConcurrenceModificationException that may happen, but instead they increased a lot. The setFavoriteTeamsSql(List<String>) method simply uses ORMlite's UpdateBuilder class to update the table. The exception is being thrown at the Team team = it.next(); line.
Any idea about how to fix this? Right now I'm rolling back the changes and going back to the for-loop. But I'd like doing this the right way.
UPDATE:
This is how the Stacktrace looks like:
Non-fatal Exception: java.util.ConcurrentModificationException
at java.util.ArrayList$ArrayListIterator.next(ArrayList.java:573)
at com.siplay.android_siplay.data.cache.db.DBTeamCache$4.call(DBTeamCache.java:164)
at com.siplay.android_siplay.data.cache.db.DBTeamCache$4.call(DBTeamCache.java:157)
at rx.Observable.unsafeSubscribe(Observable.java:10150)
at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94)
at rx.internal.schedulers.EventLoopsScheduler$EventLoopWorker$1.call(EventLoopsScheduler.java:172)
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:154)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:269)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
UPDATE 2:
...
mNetworkTeamRepository
.subscribeOn(Schedulers.io())
.obseveOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultSubscriber<List<Teams>>() {
#Override
public void onSuccess(List<Team> teams) {
mTeamsCache.setFavoriteTeams(teams);
}
public void onResult(List<Team> teams) {
callback.showTeams(teams);
}
});
...
The TeamRepository internally uses a Retrofit service to get the server-side response.
Your code is equivalent to
public void setFavoriteTeams(List<Team> teams) {
Observable
.fromIterable(teams)
.map(Team::getmSipId)
.toList()
.doOnNext(this::setFavoriteTeamsSql)
.subscribeOn(Schedulers.computation())
.subscribe();
}
But I still can't see how your code emits ConcurrentModificationException - maybe a stack trace will help?
I have a following class that my coworker created while we were using Retrofit 1.9
public class SomeApiCallAction {
private Subscription subscription;
private NoInternetConnectionInterface noInternetConnectionInterface;
public interface NoInternetConnectionInterface {
PublishSubject<Integer> noInternetConnection(Throwable throwable);
}
public void execute(Subscriber subscriber, NoInternetConnectionInterface noInternetConnectionInterface) {
this.noInternetConnectionInterface = noInternetConnectionInterface;
this.subscription = retrofit.someService().someApiCall()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber)
.retryWhen(retryFunction);
}
public void cancel() {
if (this.subscription != null) {
this.subscription.unsubscribe();
}
}
private Func1<Observable<? extends Throwable>, Observable<?>> retryFunction = new Func1<Observable<? extends Throwable>, Observable<?>>() {
#Override
public Observable<?> call(Observable<? extends Throwable> observable) {
return observable.flatMap(new Func1<Throwable, Observable<?>>() {
#Override
public Observable<?> call(final Throwable throwable) {
if (noInternetConnectionInterface!= null && (throwable instanceof IOException || throwable instanceof SocketTimeoutException)) {
return noInternetConnectionInterface.noInternetConnection(throwable);
}else{
return Observable.error(throwable);
}
}
});
}
}
SomeApiCallAction is just a simple class that wrap retrofit api call inside, the only thing special is its retry function. The retry function will check if throwable is kind of IOException or SocketTimeoutException or not, if it is, it will call the interface so that we can present retry dialog to user to ask whether they want to retry the operation or not. Our usage is similar to following snippet
public class SomeActivity implement NoInternetConnectionInterface {
#OnClick(R.id.button)
public void do(View v) {
new SomeApiCallAction().execute(
new Subscriber(),
this
)
}
#Override
public PublishSubject<Integer> noInternetConnection(final Throwable throwable) {
Log.i("Dev", Thread.currentThread() + " Error!");
final PublishSubject<Integer> subject = PublishSubject.create();
runOnUiThread(new Runnable() {
#Override
public void run() {
NoInternetDialogFragment dialog = NoInternetDialogFragment.newInstance();
dialog.setNoInternetDialogFragmentListener(new NoInternetDialogFragmentListener{
#Override
public void onUserChoice(boolean retry, NoInternetDialogFragment dialog) {
Log.i("Dev", Thread.currentThread() + " Button Click!");
if (retry) {
subject.onNext(1);
} else {
subject.onError(throwable);
}
dialog.dismiss();
}
});
dialog.show(getSupportFragmentManager(), NoInternetDialogFragment.TAG);
}
});
return subject;
}
}
When we were using Retrofit 1.9.0, this implementation was working perfectly. We test by turn on Airplane Mode and press the button to execute api call.
first execution fail and I got UnknownHostException in retry function.
so, I call the interface (Activity) to present retry dialog
I press retry button while still on Airplane mode to repeat the execution
as expected, every execution that happen after user press retry button failed to, I always get UnknownHostException in retry function.
If I keep pressing the retry button, retry dialog will appears forever until I turn off the airplane mode.
But after we update our dependencies to
'com.squareup.retrofit2:retrofit:2.0.2'
'com.squareup.retrofit2:adapter-rxjava:2.0.2'
We try again but this time the behaviour change,
first execution fail and I got UnknownHostException in retry function same as before.
so, I call the interface (Activity) to present retry dialog
I press retry button while still on Airplane mode to repeat the execution
But this time, in the retry function, instead of receiving UnknowHostException like what it was, I got NetworkOnMainThreadException instead
so the condition is not match, interface not gets call, and result as only 1 retry dialog presented to user.
Following is the log from above code
Thread[android_0,5,main] Error!
Thread[main,5,main] Button Click!
Do you have any idea what would cause this? Any suggestion, comment will be very appreciate.
Note : Following are other dependencies that we been using and might related. But they are not recently updated, been using these version since the beginning of this project.
'com.jakewharton:butterknife:8.0.1'
'io.reactivex:rxandroid:1.1.0'
'io.reactivex:rxjava:1.1.0'
'com.google.dagger:dagger-compiler:2.0'
'com.google.dagger:dagger:2.0'
'javax.annotation:jsr250-api:1.0'
More Info
I just reset my code back to the point when we were using Retrofit 1.9, I found the the print log is different
Thread[Retrofit-Idle,5,main] Error!
Thread[main,5,main] Button Click!
Not sure if this relevant to the issue or not, but clearly that in 1.9.0 I call interface in different thread compare to 2.0.0
Final Edit
After reading the answer from #JohnWowUs and follow to the link he provide I found that in Retrofit 2, network call will be synchronous by default
To resolve my issue, there are 2 ways to do this
1.) Do as #JohnWowUs suggest by specify the thread for retryFunction
this.subscription = retrofit.someService().someApiCall()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber)
.retryWhen(retryFunction, Schedulers.io());
2.) When create retrofit object, specify thread when create RxJavaCallAdapterFactory
retrofit = new Retrofit.Builder()
.baseUrl(AppConfig.BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(getGson()))
.addCallAdapterFactory(
RxJavaCallAdapterFactory.createWithScheduler(
Schedulers.from(threadExecutor)
)
)
.build();
I think the problem is that when you resubscribe you're subscribing on the main thread as a consequence of using the default trampoline scheduler in retryWhen. Retrofit 1.9 handled the scheduling for you so using subscribeOn was pointless. The issue discussion is here. In Retrofit 2 I believe this has changed so you should try something like
this.subscription = retrofit.someService().someApiCall()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber)
.retryWhen(retryFunction, Schedulers.io());
For a quick solution this is what I do to resolve this issue
new Thread(new Runnable(){
public void run() {
choice.onNext(1);
}
}).start();
The app work as we expect again. But, I don't think this is the right way to resolve this issue so I'll keep this question open for further comment.
Many RxJava tutorials with RxTextView.textChanges examples and debounce, use 'live search'. For example: Improving UX with RxJava. So I've implemented this example and I tried to play around:
RxTextView.textChanges(searchView)
.observeOn(Schedulers.io())
.skip(1)
.debounce(DELAY_BEFORE_REQUEST_MS, TimeUnit.MILLISECONDS)
.map(new Func1<CharSequence, String>() {
#Override public String call(CharSequence charSequence) {
return charSequence.toString();
}
})
.switchMap(new Func1<String, Observable<Response>>() {
#Override public Observable<Response> call(String query) {
return retrofitService.search(query);
}
})
.subscribe();
Everything looked good, until I decided to simulate GPRS network type on Android emulator.
First api call was triggered, and when I added next letter to 'searchView', app crashed with InterruptedIOException:
java.lang.IllegalStateException: Exception thrown on Scheduler.Worker thread. Add `onError` handling.
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:60)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:269)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: rx.exceptions.OnErrorNotImplementedException: thread interrupted
at rx.Observable$27.onError(Observable.java:7923)
at rx.observers.SafeSubscriber._onError(SafeSubscriber.java:159)
at rx.observers.SafeSubscriber.onError(SafeSubscriber.java:120)
at rx.internal.operators.OperatorSubscribeOn$1$1$1.onError(OperatorSubscribeOn.java:71)
at rx.observers.SerializedObserver.onError(SerializedObserver.java:159)
at rx.observers.SerializedSubscriber.onError(SerializedSubscriber.java:79)
at rx.internal.operators.OperatorSwitch$SwitchSubscriber.error(OperatorSwitch.java:223)
at rx.internal.operators.OperatorSwitch$InnerSubscriber.onError(OperatorSwitch.java:282)
at rx.internal.operators.OperatorMerge$MergeSubscriber.reportError(OperatorMerge.java:240)
at rx.internal.operators.OperatorMerge$MergeSubscriber.checkTerminate(OperatorMerge.java:776)
at rx.internal.operators.OperatorMerge$MergeSubscriber.emitLoop(OperatorMerge.java:537)
at rx.internal.operators.OperatorMerge$MergeSubscriber.emit(OperatorMerge.java:526)
at rx.internal.operators.OperatorMerge$MergeSubscriber.onError(OperatorMerge.java:250)
at rx.internal.operators.OperatorMap$1.onError(OperatorMap.java:48)
at retrofit2.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:114)
at retrofit2.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:88)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable.unsafeSubscribe(Observable.java:8098)
at rx.internal.operators.OperatorSwitch$SwitchSubscriber.onNext(OperatorSwitch.java:105)
at rx.internal.operators.OperatorSwitch$SwitchSubscriber.onNext(OperatorSwitch.java:60)
at rx.internal.operators.OperatorMap$1.onNext(OperatorMap.java:54)
at rx.internal.operators.OperatorDoOnEach$1.onNext(OperatorDoOnEach.java:85)
at rx.internal.operators.OperatorMap$1.onNext(OperatorMap.java:54)
at rx.observers.SerializedObserver.onNext(SerializedObserver.java:95)
at rx.observers.SerializedSubscriber.onNext(SerializedSubscriber.java:95)
at rx.internal.operators.OperatorDebounceWithTime$DebounceState.emit(OperatorDebounceWithTime.java:132)
at rx.internal.operators.OperatorDebounceWithTime$1$1.call(OperatorDebounceWithTime.java:79)
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:269)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.io.InterruptedIOException: thread interrupted
at okio.Timeout.throwIfReached(Timeout.java:145)
at okio.Okio$2.read(Okio.java:136)
at okio.AsyncTimeout$2.read(AsyncTimeout.java:211)
at okio.RealBufferedSource.indexOf(RealBufferedSource.java:306)
at okio.RealBufferedSource.indexOf(RealBufferedSource.java:300)
at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:196)
at okhttp3.internal.http.Http1xStream.readResponse(Http1xStream.java:184)
at okhttp3.internal.http.Http1xStream.readResponseHeaders(Http1xStream.java:125)
at okhttp3.internal.http.HttpEngine.readNetworkResponse(HttpEngine.java:723)
at okhttp3.internal.http.HttpEngine.access$200(HttpEngine.java:81)
at okhttp3.internal.http.HttpEngine$NetworkInterceptorChain.proceed(HttpEngine.java:708)
at okhttp3.internal.http.HttpEngine.readResponse(HttpEngine.java:563)
at okhttp3.RealCall.ge
I have searched a little, and it looks like I'm not alone: first and second.
Author of that first question solved this problem this by wrapping retrofit request with try-catch block.
For me it is attempt of covering bad architecture. And I'm looking for cleaner solution.
Is there a way of ignoring first API call result, and starting new one using RxJava? Or I should try to switch over new Retrofit Call API, and try to cancel previous request (and break a reactive approach)?
I have using Retrofit 2 beta 3, with newest Okio and OkHttp.
Well, the error is pretty explicit, you should add onError handling. It could look something like this:
.subscribe(new Observer<Response>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(Response response) {
}
});
However, your subscription will be terminated once an error is emitted, but you can avoid this by handling the errors of the API call like this:
.switchMap(new Func1<String, Observable<Response>>() {
#Override public Observable<Response> call(String query) {
return retrofitService.search(query)
.onErrorResumeNext(Observable.<Response>empty());
}
})
So currently, Im making an async network request with a date parameter (using Retrofit), and if that request returns with a response code that isnt 200 (or if its 429, 400, or if the response body is just null, whatever is easiest to determine), I make a new request with a date parameter 1 day earlier. Again, if this request comes back with a response code that isnt 200, I make one more request with a date 1 day earlier than the previous, for a total of 3 possible requests if the first two fail.
I'm currently achieving this with a bunch of callbacks and calling a new method set up to perform the request with the day -1 for each try.
I know that I can achieve a cleaner solution with Rx and Retrofits built in Rx features. How would this be done?
First you need to add RxAndroid and RxJava adapter in your dependencies
compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2'
compile 'io.reactivex:rxandroid:1.0.1'
and then you need to register the call adapter to your Retrofit declaration
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(myBaseurl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
And then you can change your service interface return type from Call to Observable
public interface MyAPIService {
#POST("user")
Call<User> getUser();
#POST("user")
Observable<Response<User>> getUserWithRxJava();
#POST("user_friends")
Observable<Response<List<User>>> getUserFriends();
}
And this is an example for chaining call
myService.getUserWithRxJava()
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<Response<User>, Observable<Response<List<User>>>>() {
#Override
public Observable<List<Home>> call(Response<User> responseUser) {
// You can also use responseUser.code to get the response code
// but isSuccess() function will return true if the code
// is in the range [200..300)
if (responseUser.isSuccess()) {
return myService.getUserFriends();
} else {
// You can also use Observable.empty() if you want to ignore unsuccessful response
return Observable.error(myThrowable);
}
}
})
.subscribe(new Subscriber<Response<List<User>>>() {
#Override
public void onCompleted() {
// TODO Completed
}
#Override
public void onError(Throwable e) {
// TODO Handle the error
}
#Override
public void onNext(Response<List<User>> friendListResponse) {
// TODO do something with the data
// To get the serialized data you can use friendListResponse.body();
}
});
The non-200 responses should come back as RetrofitError objects, which contain Response objects with a status code.
You could do something like this:
observable
.retryWhen(new RetryStrategy())
.subscribe(...);
and RetryStrategy might look like this (note, I'm using retrolambda, so wherever you see -> just replace with a new anonymous inner class):
public class RetryStrategy implements Func1<Observable<? extends Throwable>, Observable<?>> {
public RetryStrategy() {}
#Override
public Observable<?> call(Observable<? extends Throwable> attempts) {
return attempts.flatMap((throwable) -> {
if (throwable instanceof RetrofitError) {
RetrofitError error = (RetrofitError) throwable;
if (error.getKind() == RetrofitError.Kind.HTTP) {
if (error.getResponse().getStatus() == 401) {
// This is where you attempt to recover
return someRecoveryObservable???;
}
}
}
// Bubble any other errors back up, e.g. connection loss.
return Observable.error(throwable);
});
}
}
You could also implement Exponential Backoff here by adding support for a retry count, along with using Observable.timer(long, TimeUnit) in your recovery phase (and compute the timing based on the current number of tries). Spotty connection problems sometimes benefit greatly from this approach - especially with fire & forget tasks that run in the background.