×
Community Blog How Idle Fish Uses RxJava to Improve the Asynchronous Programming Capability - Part2

How Idle Fish Uses RxJava to Improve the Asynchronous Programming Capability - Part2

Part 2 of this 2-part article explores the basic principles and precautions of RxJava.

By Kunming, from Idle Fish Technology

In Part 1 of this 2-part article, we introduced RxJava and explored its usage. We'll explore the basic principles and precautions of RxJava in this article.

4. Basic Principles

RxJava code is the embodiment of observer mode + decorator mode.

4.1 Observable.create

Please see Code 3.3. The create method receives an ObserverableOnSubscribe interface object. We define the code that sends the element, and the create method returns an ObserverableCreate type object (inherited from the Observerable abstract class.) Follow up the original code of the create method and return the new ObserverableCreate directly, which packages a source object, the imported ObserverableOnSubscribe.

Code 4.1

    public static <T> Observable<T> create(ObservableOnSubscribe<T> source) {
        ObjectHelper.requireNonNull(source, "source is null");
        //onAssembly directly returns ObservableCreate by default
        return RxJavaPlugins.onAssembly(new ObservableCreate<T>(source)); 
    }

The create method is as simple as that. Just remember that it returns an Observable that packages a source.

4.2 Observerable.subscribe(observer)

Let’s see what happens when you create a subscription relationship (observerable.subscribe) in Code 3.3:

Code 4.2

 public final void subscribe(Observer<? super T> observer) {
     ObjectHelper.requireNonNull(observer, "observer is null");
     try {
         observer = RxJavaPlugins.onSubscribe(this, observer);
         ObjectHelper.requireNonNull(observer, "Plugin returned null Observer");
         subscribeActual(observer);
     } catch (NullPointerException e) {... } catch (Throwable e) {... }
 }

Observable is an abstract class that defines the final method of subscribe and eventually calls the subscribeActual(observer). subscribeActual is a method implemented by subclasses. Naturally, we need to look at the method implemented by ObserverableCreate.

Code 4.3

//The subscribeActual method implemented by ObserverableCreate.
protected void subscribeActual(Observer<? super T> observer) {
    CreateEmitter<T> parent = new CreateEmitter<T>(observer);
    observer.onSubscribe(parent);

    try {
        source.subscribe(parent); //Source is ObservableOnSubscribe, which is the code of the production element.
    } catch (Throwable ex) {...}
}
  1. Package the observer into a CreateEmitter
  2. Call the onSubscribe method of the observer and pass in this emitter.
  3. Call the subscribe method of source (the production code interface) and pass in this emitter

In the second step, the consumer's onSubscribe method is called directly, which is easy to understand. It refers to the callback method to create the subscription relationship.

The key is the third step, source.subscribe(parent). This parent is the emitter that packages the observer. Remember that source is the code we wrote to send events, in which emitter.onNext() is called to send data manually. So, what did we do with CreateEmitter.onNext()?

Code 4.4

public void onNext(T t) {
    if (t == null) {...}
    if (!isDisposed()) { observer.onNext(t); }
}

!isDisposed() determines that if the subscription relationship has not been canceled, it calls observer.onNext(t). This observer is the consumer we wrote. In Code 3.3, we rewrote its onNext method to print the received elements.

The section above is the basic principle of RxJava. The logic is very simple. When you create subscription relationship, you call the production logic code directly and then call the observer.onNext observer in the production logic onNext. The following figure shows the sequence diagram:

1

The most basic principle is to decouple the relationship with asynchronous callback and multithreading.

4.3 Observable.map

Let’s look at what the conversion API does through the simplest map method. For example, in Code 2.1, call the map method and pass in a conversion function to convert upstream elements into elements of other types one-to-one.

Code 4.5

    public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {
        ObjectHelper.requireNonNull(mapper, "mapper is null");
        return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));
    }

Code 4.5 is the final map method defined by Observable. The map method packages this (the original observer) and the conversion function mapper into an ObservableMap (ObservableMap also inherits Observable) and then returns this ObservableMap (onAssembly does nothing by default.)

Since ObservableMap is also an Observable, its subscribe method will be called layer by layer when creating subscribers. Subscribe is the final method defined by Observable and will be called to the subscribeAcutal method it implements eventually.

Code 4.6

//The subscribeActual of ObservableMap.
public void subscribeActual(Observer<? super U> t) {
    source.subscribe(new MapObserver<T, U>(t, function));
}

You can see that in the subscribeActual of ObservableMap, the original observer t and the transformation function are packaged into a new observer MapObserver and subscribed to the observer source.

We know that when sending data, the observer's onNext will be called, so let’s look at the onNext method of MapObserver.

Code 4.7

@Override
public void onNext(T t) {
    if (done) {return; }
    if (sourceMode != NONE) { actual.onNext(null);return;}
    U v;
    try {
        v = ObjectHelper.requireNonNull(mapper.apply(t), "The mapper function returned a null value.");
    } catch (Throwable ex) {...}
    actual.onNext(v);
}

In Code 4.7, we can see that mapper.apply(t) applies the transformation function mapper to each element t. After the transformation, v is obtained. Finally, actual.onNext(v) is called to send v to the downstream observer actual (t passed in when MapObserver was created in Code 4.6.)

The principles of API transformation, such as map:

  1. The map method returns an ObservableMap, packaging the original observer t and transformation function.
  2. ObservableMap inherits from AbstractObservableWithUpstream. (It inherits from Observable.)
  3. When the subscription occurs, the final method subscribe() of Observable calls the subscribeActual of the implementation class.
  4. ObservableMap.subscribeActual creates a MapObserver (packaging the original observer) and subscribes to the original Observable.
  5. When onNext is called, apply the transformation operation first and then call the onNext of the original observer. In other words, send it to the downstream observer.

4.4 Thread Scheduling

An example of thread scheduling is given in Code 2.2. subscribeOn(Schedulers.io()) specifies the thread pool executed by the observed. observeOn(Schedulers.single()) specifies the thread pool that the downstream observer executes. After the study above, we can understand that the principle is to package Observable and Observer layer by layer through the decorator mode and throw them into the thread pool for execution. Let's use observeOn() as an example.

Code 4.8

public final Observable<T> observeOn(Scheduler scheduler, boolean delayError, int bufferSize) {
    ObjectHelper.requireNonNull(scheduler, "scheduler is null");
    ObjectHelper.verifyPositive(bufferSize, "bufferSize");
//observeOn(Scheduler) returns ObservableObserveOn (inherited from Observable).
    return RxJavaPlugins.onAssembly(new ObservableObserveOn<T>(this, scheduler, delayError, bufferSize));
}

//The subscribe method of Observable will eventually be called to the ObservableObserveOn.subscribeActual method.
protected void subscribeActual(Observer<? super T> observer) {
    if (scheduler instanceof TrampolineScheduler) {
        source.subscribe(observer);
    } else {
        Scheduler.Worker w = scheduler.createWorker();
//Create an ObserveOnObserver to package the original observer and worker, and subscribe it to the source (original observable).
        source.subscribe(new ObserveOnObserver<T>(observer, w, delayError, bufferSize));
    }
}
  1. observeOn(Scheduler) returns ObservableObserveOn.
  2. ObservableObserveOn inherits from Observable.
  3. So, the subscribe method will be called to the subscribeActual method rewritten by ObservableObserveOn eventually.
  4. subscribeActual returns an ObserveOnObserver (an Observer) that packages the real observer and worker.

According to the logic of Observer, the onNext method will be called when sending data. Let’s look at the onNext method of ObserveOnObserver.

Code 4.9

public void onNext(T t) {
    if (done) { return; }
    if (sourceMode != QueueDisposable.ASYNC) { queue.offer(t);}
    schedule();
}

void schedule() {
    if (getAndIncrement() == 0) {
        worker.schedule(this); //this is ObserveOnObserver, which also implements Runable.
    }
}

public void run() {
    if (outputFused) {
        drainFused();
    } else {
        drainNormal(); //In the end, actual.onNext(v) is called. In other words, the encapsulated downstream observer is called, and v is emmiter.
    }
}
  1. When onNext is called in the final producer code, the schedule method is called.
  2. In the schedule method, ObserveOnObserver is submitted to the thread pool.
  3. The run method calls onNext(emmiter).

It shows that the thread scheduling mechanism of RxJava is to use observeOn(Scheduler) to submit the onNext(emmiter) code that sends elements to the thread pool for execution.

5. Precautions for Use

Now, let’s discuss a few precautions that we summarized in the development to avoid making repetitive mistakes.

5.1 Applicable Scenarios

Not all I/O operations and asynchronous callbacks need to be solved by RxJava. For example, the introduction of RxJava will not offer many benefits to the combination of one or two RPC services or the request for separate processing logic. Some of the best applicable scenarios are listed below:

  • Handle UI events
  • Respond asynchronously and process I/O results
  • Events or data pushed by uncontrollable producers
  • Combine received events

The following part is a scenario that adds data to Idle Fish products in batches.

Background: The algorithm recommends some products of users. Currently, it only has basic information. We need to call multiple business interfaces to supplement the additional business information of users and products, such as user avatars, product video links, and product cover pictures. According to the type of products, we need to fill in different vertical business information.

Difficulties:

  1. Multiple interfaces have multiple dependencies or cross-dependencies.
  2. Each interface may time out or report an error, which may affect the subsequent logic.
  3. We need to control the timeout and fallback separately according to the characteristics of different dependent interfaces. The entire interface also needs to set the overall timeout and fallback.

Solution: If multiple interfaces are used for independent asynchronous queries, CompletableFuture can be used. However, its unfriendly support for combination, timeout, and fallback makes it inapplicable for this scenario. We finally adopted RxJava to implement it. The following is the general code logic. HsfInvoker in the code is a tool class that converts common HSF interfaces into Rx interfaces used inside Alibaba. It runs in a separate thread pool by default, so concurrent calls can be realized.

//Find all products of the current user.
Single<List<IdleItemDO>> userItemsFlow =
    HSFInvoker.invoke(() -> idleItemReadService.queryUserItems(userId, userItemsQueryParameter))
    .timeout(300, TimeUnit.MILLISECONDS)
    .onErrorReturnItem(errorRes)
    .map(res -> {
        if (!res.isSuccess()) {
            return emptyList;
        }
        return res.getResult();
    })
    .singleOrError();

//Supplement products, which depends on userItemsFlow.
Single<List<FilledItemInfo>> fillInfoFlow =
    userItemsFlow.flatMap(userItems -> {

        if (userItems.isEmpty()) {
            return Single.just(emptyList);
        }

        Single<List<FilledItemInfo>> extraInfo =
            Flowable.fromIterable(userItems)
            .flatMap(item -> {

// Find the product extendsDo.
                Flowable<Optional<ItemExtendsDO>> itemFlow =
                    HSFInvoker.invoke(() -> newItemReadService.query(item.getItemId(), new ItemQueryParameter()))
                    .timeout(300, TimeUnit.MILLISECONDS)
                    .onErrorReturnItem(errorRes)
                    .map(res -> Optional.ofNullable(res.getData()));

//Video URL.
                Single<String> injectFillVideoFlow = 
                    HSFInvoker.invoke(() -> videoFillManager.getVideoUrl(item))
                              .timeout(100, TimeUnit.MILLISECONDS)
                              .onErrorReturnItem(fallbackUrl);

//Fill in the cover picture.
                Single<Map<Long, FrontCoverPageDO>> frontPageFlow =
                    itemFlow.flatMap(item -> {
                        ...
                        return frontCoverPageManager.rxGetFrontCoverPageWithTpp(item.id);
                    })
                    .timeout(200, TimeUnit.MILLISECONDS)
                    .onErrorReturnItem(fallbackPage);

                return Single.zip(itemFlow, injectFillVideoFlow, frontPageFlow, (a, b, c) -> fillInfo(item, a, b, c));
            })
            .toList(); //Convert to a product list.

        return extraInfo;
    });

//The avatar information.
Single<Avater> userAvaterFlow =
    userAvaterFlow = userInfoManager.rxGetUserAvaters(userId).timeout(150, TimeUnit.MILLISECONDS).singleOrError().onErrorReturnItem(fallbackAvater);

//Combine the user avatar and product information and return them together.
return Single.zip(fillInfoFlow, userAvaterFlow,(info,avater) -> fillResult(info,avater))
             .timeout(300, TimeUnit.MILLISECONDS)
             .onErrorReturn(t -> errorResult)
             .blockingGet(); //Return in a non-blocking manner.

As we can see, fter introducing RxJava, it is more convenient to support timeout control, guarantee policies, request callbacks, and result combinations.

5.2 Scheduler Thread Pool

RxJava 2 has a built-in implementation of multiple schedulers, but we recommend using Schedulers.from(executor) to specify the thread pool. This avoids using the default public thread pool provided by the framework and prevents a single long-tail task from blocking other threads in execution or OOM due to the creation of too many threads.

5.3 CompletableFuture

When the logic is relatively simple, and we only want to call one or two RPC services asynchronously, so we can use the CompletableFuture implementation provided by Java8. Compared with Future, it is executed asynchronously and can also implement simple combined logic.

5.4 Concurrency

A single Observable is always executed sequentially, and onNext() is not allowed to be called concurrently.

Code 5.1

Observable.create(emitter->{
    new Thread(()->emitter.onNext("a1")).start();
    new Thread(()->emitter.onNext("a2")).start();
})

However, each Observable can be executed independently and concurrently.

Code 5.2

Observable ob1 = Observable.create(e->new Thread(()->e.onNext("a1")).start());
Observable ob2 = Observable.create(e->new Thread(()->e.onNext("a2")).start());
Observable ob3 = Observable.merge(ob1,ob2);

ob3 combines streams ob1 and ob2, each of which is independent. Note: These two streams can be executed concurrently, and another condition is that their code sending scripts run on different threads. Like the examples in Code 3.1 and Code 3.2, although the two streams are independent, they are executed sequentially if they are not committed to different threads.

5.5 Back Pressure

In RxJava 2.x, only the Flowable type supports back pressure. The problems that Observable can solve can also be solved with Flowable. However, the additional logic added to support back pressure causes Flowable to run much slower than Observable. Therefore, Flowable is only recommended when you need to handle back pressure. If it can be determined whether the upstream and downstream work in the same thread and the speed of data processing in the downstream is higher than the speed of data transmission in the upstream, there is no back pressure problem. Thus, there is no need to use Flowable. The use of Flowable will not be explained in this article.

5.6 Timeout

We strongly recommend setting the timeout period for asynchronous calls and using the timeout and onErrorReturn methods to set the timeout guarantee logic. Otherwise, this request will always occupy an Observable thread and will also cause OOM when a large number of requests arrive.

6. Conclusion

RxJava is used for asynchronization in many business scenarios of Idle Fish, which reduces the asynchronous development cost of developers substantially. At the same time, it has good performance in multi-request response combination and concurrent processing. Its timeout logic and guarantee strategy can ensure reliability in batch business data processing and provide a smooth user experience.

0 0 0
Share on

XianYu Tech

58 posts | 3 followers

You may also like

Comments

XianYu Tech

58 posts | 3 followers

Related Products