13

My requirements:

  • N Retrofit calls in parallel
  • Wait for all calls to finish (success or failure)
  • If k (0<= k < N) calls fail, they should NOT block the others. Imagine failed calls can return null or error objects while successful ones return the ResponseBody objects.
  • Optional: if RxJava has an easy way to distinguish which call is which for each success or failure, great, if not, I'll parse the responses and figure it out myself

What I have:

        Observable<ResponseBody> api1Call = api1.fetchData();
        Observable<ResponseBody> api2Call = api2.fetchData();
        Observable<ResponseBody> api3Call = api3.fetchData();

        Observable.combineLatest(api1Call, api2Call, api3Call, new Func2<ResponseBody, ResponseBody, ResponseBody, Object>() {
            @Override
            public Object call(ResponseBody responseBody1, ResponseBody responseBody2, ResponseBody responseBody3) {
                Logger.i("what does this do? - '%s', '%s', '%s'", responseBody1, responseBody2, responseBody3);
                return null;
            }
        }).onErrorResumeNext(new Func1<Throwable, Observable<?>>() {
            @Override
            public Observable<?> call(Throwable throwable) {
                Logger.e(throwable, "some error with one of the apis?");
                return Observable.empty();
            }
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Object>() {
                    @Override
                    public void onCompleted() {
                        Logger.i("onCompleted");
                    }

                    @Override
                    public void onError(Throwable e) {
                        Logger.e(e, "onError");
                    }

                    @Override
                    public void onNext(Object o) {
                        Logger.i("onNext " + o);
                    }
                });

The output I got:

some error with one of the apis?
// stacktrace of the error
onCompleted

I'm new to RxJava and very confused. I found some answers on StackOverflow saying zip does similar thing but it's even further from my requirements. I'm guessing one of the "combine" operators + proper exception handing will do what I need. It's just been really hard to figure it out so far

Versions I'm using:

compile 'io.reactivex:rxjava:1.3.0'
compile 'io.reactivex:rxandroid:1.2.1'
compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
ericn
  • 12,476
  • 16
  • 84
  • 127
  • Why are you using RxJava2 instead of RxJava1? – Quang Nguyen Jun 20 '17 at 04:18
  • 1
    "If 1 call fails, it should blocked the others" If 2 call fails, Should it blocked the others? – vmtrue Jun 20 '17 at 04:57
  • Where do u see RxJava2 @QuangNguyen? I'm using Retrofit 2 with RxJava 1 – ericn Jun 20 '17 at 05:21
  • 1
    I am sorry for making mistake here. I mean why not using RxJava2? – Quang Nguyen Jun 20 '17 at 07:49
  • Besides, for `zip` and `combineLastest`, they produce the same result at this case, because all above Retrofit calls only emit one item. So, no matter which operator you use. – Quang Nguyen Jun 20 '17 at 07:53
  • I'm open to using RxJava 2 of course since I'm starting now :D Could you flesh out your hints in an answer so that I can accept it? @QuangNguyen – ericn Jun 20 '17 at 08:02
  • the only reason why I'm using RxJava 1 here is because most of the documentations and discussions I found online still refer to RxJava 1, maybe it's already the right time to pick up RxJava 2 :) – ericn Jun 20 '17 at 08:12
  • Have you checked this: https://stackoverflow.com/questions/35012030/execute-http-request-in-parallel-with-retrofit-2? – Quang Nguyen Jun 20 '17 at 08:20
  • It is quite too long for discussion now, so I would like to write in an answer and update it. – Quang Nguyen Jun 20 '17 at 08:24

5 Answers5

7
  1. You can not achieve parallel via combineLast or zip, rxjava will execute and emit your items in sequence in my testing.

  2. If one of your task fail, your Func2#call will not get called and onError will submitted instead. You even can not get the results of other successful tasks in this way.

  3. The solution is flatMap, it's the traditional way to achieve concurrent in rxjava. It also meet your other requirements.

Here is a small but completed example.

I use a simple website service to test.

I use a Semaphore to wait for all task done, you can completely ignore it. And I add logging to the http request for better understanding, you can complete ignore it also.

public interface WebsiteService {

    @GET
    Observable<ResponseBody> website(@Url String url);

}

Then I use the following to test the result with rxjava.

   HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
    loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);

    Retrofit retrofit = new Retrofit.Builder().baseUrl("https://www.google.com")
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(new OkHttpClient.Builder().addInterceptor(loggingInterceptor).build())
            .build();
    WebsiteService websiteService = retrofit.create(WebsiteService.class);

    final Semaphore s = new Semaphore(1);
    try {
        s.acquire();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    Observable<ResponseBody> first = websiteService.website("http://github.com");
    Observable<ResponseBody> second = websiteService.website("http://stackoverflow.com");
    Observable<ResponseBody> third = websiteService.website("http://notexisting.com");

    final int numberOfCalls = 3; // testing for three calls

    Observable.just(first, second, third)
            .flatMap(new Function<Observable<ResponseBody>, ObservableSource<ResponseBody>>() {
                @Override
                public ObservableSource<ResponseBody> apply(@NonNull Observable<ResponseBody> responseBodyObservable) throws Exception {
                    return responseBodyObservable.subscribeOn(Schedulers.computation());
                }
            })
            .subscribeOn(Schedulers.computation())
            .subscribe(new Observer<ResponseBody>() {

                private int currentDoneCalls = 0;

                private void checkShouldReleaseSemaphore() {
                    if (currentDoneCalls >= numberOfCalls) {
                        s.release();
                    }
                }

                @Override
                public void onSubscribe(@NonNull Disposable d) {

                }

                @Override
                public void onNext(@NonNull ResponseBody responseBody) {
                    System.out.println("Retrofit call success " + responseBody.contentType());
                    synchronized (this) {
                        currentDoneCalls++;
                    }
                    checkShouldReleaseSemaphore();
                }

                @Override
                public void onError(@NonNull Throwable e) {
                    System.out.println("Retrofit call failed " + e.getMessage());
                    synchronized (this) {
                        currentDoneCalls++;
                    }
                    checkShouldReleaseSemaphore();
                }

                @Override
                public void onComplete() {
                    System.out.println("onComplete, All request success");
                    checkShouldReleaseSemaphore();
                }

            });

    try {
        s.acquire();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("All request done");
        s.release();
    }

I use rxjava2 and retrofit adapter-rxjava2 for testing.

compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.1.0'
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.8.1'

Updated

The introduction page of RxJava2 from github has pointed out the practical way to implement paralellism.

Practically, paralellism in RxJava means running independent flows and merging their results back into a single flow. The operator flatMap does this...

Although this example is based on RxJava2, the operation flatMap is already existing in RxJava.

alijandro
  • 11,627
  • 2
  • 58
  • 74
  • it's impressive what you shared here @alijandro but it only prints out `Retrofit call failed ...` and the whole thing stops there – ericn Jun 26 '17 at 15:44
  • That is weird, did other calls worked? You might need to subscribe on `Schedulers.computation()`. – alijandro Jun 27 '17 at 02:42
  • Replacing both `Schedulers.io()` or which on @alijandro? :) – ericn Jun 27 '17 at 18:27
  • omg, it works! @alijandro Can you do me a favour so that I can accept your answer 1) Update your answer 2) Is this something only available in RxJava 2 (link to documentations would be great) or is this some hack of your own? – ericn Jun 27 '17 at 18:49
  • Sorry @alijandro, why `Schedulers.io()` did not work for me and `Schedulers.computation()` works? – ericn Jun 28 '17 at 13:53
  • 1
    From the documentation `Schedulers.io is backed by a pool of single-threaded {link ScheduledExecutorService} instances that will try to reuse previously started instances used by the worker returned by Scheduler.createWorker()...` and `Scheduler.computation has a backing pool of single-threaded ScheduledExecutorService instances equal to the number of available processors (Runtime.availableProcessors()) to the Java VM.`. I didn't investigate deeply, so I think the key is the former will try to `reuse` but the latter will always started in a new thread. – alijandro Jun 29 '17 at 02:14
  • 3 months down the road, I've learnt more about RxJava and found the correct solution, hence my own answer. I can't remove the bounty and take back 50 reputation, just want to let you know @alijandro. Thanks again :) – ericn Sep 17 '17 at 05:30
  • Thanks for your 3 bullet points. I was getting pretty confused about .zip parallel requests, but this cleared it up – DIRTY DAVE Jan 28 '22 at 05:32
1

I think in your use case Zip operator it´s more suitable

Here you can see running in the main thread, but also it´s possible make it run every one of them in another thread if you use observerOn

/**
 * Since every observable into the zip is created to subscribeOn a different thread, it´s means all of them will run in parallel.
 * By default Rx is not async, only if you explicitly use subscribeOn.
  */
@Test
public void testAsyncZip() {
    scheduler = Schedulers.newThread();
    scheduler1 = Schedulers.newThread();
    scheduler2 = Schedulers.newThread();
    long start = System.currentTimeMillis();
    Observable.zip(obAsyncString(), obAsyncString1(), obAsyncString2(), (s, s2, s3) -> s.concat(s2)
                                                                                        .concat(s3))
              .subscribe(result -> showResult("Async in:", start, result));
}



private Observable<String> obAsyncString() {
    return Observable.just("")
                     .observeOn(scheduler)
                     .doOnNext(val -> {
                         System.out.println("Thread " + Thread.currentThread()
                                                              .getName());
                     })
                     .map(val -> "Hello");
}

private Observable<String> obAsyncString1() {
    return Observable.just("")
                     .observeOn(scheduler1)
                     .doOnNext(val -> {
                         System.out.println("Thread " + Thread.currentThread()
                                                              .getName());
                     })
                     .map(val -> " World");
}

private Observable<String> obAsyncString2() {
    return Observable.just("")
                     .observeOn(scheduler2)
                     .doOnNext(val -> {
                         System.out.println("Thread " + Thread.currentThread()
                                                              .getName());
                     })
                     .map(val -> "!");
}

You can see more examples here https://github.com/politrons/reactive

paul
  • 12,873
  • 23
  • 91
  • 153
1

You can use Observable.mergeDelayError(api1Call, api2Call, api3Call).

Bonus: You can also specify how many maximum parallels calls can be run at the same time. For example:

Observable .mergeDelayError(Observable.from(api1Call, api2Call, api3Call), 5).

Tin Tran
  • 2,265
  • 1
  • 14
  • 17
0
  1. For multiple parallel calls in Retrofit, we need to set it up from OkHttp layer when initializing Retrofit. Please check this.
  2. If you use combineLatest or zip operators in this case (for Retrofit call), each call only emits an item. So, both of operators will wait for all calls to finish. So, we do not need to worry about this point. For more information, check out combineLatest and zip.
  3. If you meant 1 call fail about the RxJava stream error, this error will be thrown, none of combined item will be emitted. But 1 call fail is http request fail, the stream always emit one item when 3 calls finish. we can not use combineLast or zip operator here.
Quang Nguyen
  • 2,600
  • 2
  • 17
  • 24
0

Thanks to @TinTran and this, here is the correct solution:
(I can't put up the exact syntax for Retrofit Observables now but that shouldn't matter, logic remains the same Retrofit or not)

Observable.mergeDelayError(getData1(), getData2()).doAfterTerminate(new Action0() {
            @Override
            public void call() {
                Logger.i("end of all streams");
                tvTheText.setText("all streams finished");
            }
        }).subscribe(new PrintSubscriber<>("merge" +
                " delay w error"));

The observables (Retrofit ones should work the same way):

private Observable<String> getData1() {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> singleSubscriber) {
                try {
                    long responseTime = 120 + new Random().nextInt(30);
                    Thread.sleep(responseTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                singleSubscriber.onNext("data 1");
                singleSubscriber.onCompleted();
            }
        }).observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io());
    }

    private Observable<String> getData2() {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> singleSubscriber) {
                try {
                    long responseTime = 100 + new Random().nextInt(19);
                    Thread.sleep(responseTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                singleSubscriber.onError(new Exception());// this one never blocks the other Observables' streams
            }
        }).observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io());
    }

Output logs:

    10-24 15:27:23.335 D: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
10-24 15:27:23.335 D: │ Thread: main
10-24 15:27:23.335 D: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
10-24 15:27:23.335 D: │ SafeSubscriber.onNext  (SafeSubscriber.java:134)
10-24 15:27:23.335 D: │    PrintSubscriber.onNext  (PrintSubscriber.java:32)
10-24 15:27:23.335 D: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
10-24 15:27:23.336 D: │ merge delay w error - onNext - data 1
10-24 15:27:23.336 D: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────
10-24 15:27:23.342 V: ⇢ onError(e=java.lang.Exception)
10-24 15:27:23.342 V: ⇠ onError [0ms]
10-24 15:27:23.343 I: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
10-24 15:27:23.343 I: │ Thread: main
10-24 15:27:23.343 I: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
10-24 15:27:23.343 I: │ OperatorDoAfterTerminate$1.callAction  (OperatorDoAfterTerminate.java:73)
10-24 15:27:23.343 I: │    MainActivity$1.call  (MainActivity.java:37)
10-24 15:27:23.343 I: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
10-24 15:27:23.344 I: │ end of all streams
10-24 15:27:23.344 I: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────
ericn
  • 12,476
  • 16
  • 84
  • 127