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.
RxJava code is the embodiment of observer mode + decorator mode.
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.
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) {...}
}
CreateEmitter
onSubscribe
method of the observer and pass in this emitter.subscribe
method of source (the production code interface) and pass in this emitterIn 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:
The most basic principle is to decouple the relationship with asynchronous callback and multithreading.
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:
map
method returns an ObservableMap, packaging the original observer t and transformation function.AbstractObservableWithUpstream
. (It inherits from Observable.)subscribe()
of Observable calls the subscribeActual
of the implementation class.ObservableMap.subscribeActual
creates a MapObserver (packaging the original observer) and subscribes to the original Observable.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.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));
}
}
observeOn(Scheduler)
returns ObservableObserveOn
.ObservableObserveOn
inherits from Observable.subscribeActual
method rewritten by ObservableObserveOn
eventually.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.
}
}
onNext
is called in the final producer code, the schedule method is called.ObserveOnObserver
is submitted to the thread pool.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.
Now, let’s discuss a few precautions that we summarized in the development to avoid making repetitive mistakes.
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:
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:
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.
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.
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.
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.
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.
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.
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.
How Idle Fish Uses RxJava to Improve the Asynchronous Programming Capability - Part1
56 posts | 4 followers
FollowXianYu Tech - December 27, 2021
Alibaba Clouder - February 2, 2021
hujt - April 1, 2021
XianYu Tech - December 13, 2021
XianYu Tech - December 24, 2021
XianYu Tech - December 13, 2021
56 posts | 4 followers
FollowResource management and task scheduling for large-scale batch processing
Learn MoreExplore Web Hosting solutions that can power your personal website or empower your online business.
Learn MoreTranscode multimedia data into media files in various resolutions, bitrates, and formats that are suitable for playback on PCs, TVs, and mobile devices.
Learn MoreExplore how our Web Hosting solutions help small and medium sized companies power their websites and online businesses.
Learn MoreMore Posts by XianYu Tech