Detailed explanation of JDK9 responsive flow

Posted by roonnyy on Thu, 03 Feb 2022 06:25:51 +0100

In the above, we briefly mentioned that the static internal class in the Flow interface in JDK9 implements the JAVA API of responsive Flow, and provides a Publisher implementation class SubmissionPublisher. This article will first sort out the specific processing Flow in the interface, and then use several examples of callers to help you understand.

Implementation in JDK9

Next, let's talk about the interactive process of reactive flow in the above:

  1. Subscribers send subscription requests to publishers.
  2. The publisher generates a token according to the subscription request and sends it to the subscriber.
  3. The subscriber sends the request N data to the publisher according to the token.
  4. The sender returns m (m < = n) pieces of data according to the number of subscribers' requests
  5. Repeat 3, 4
  6. After the data is sent, the publisher sends the end signal to the subscriber

The process is based on the interaction of interface calls. Considering the actual coding work, our call process is actually:

  1. Create publisher
  2. Create subscriber
  3. Subscription token interaction
  4. Send message

Next, let's sort out the code details according to this process.

Create publisher

The first step in implementing the response flow is to create a publisher. As mentioned earlier, a simple implementation of the publisher SubmissionPublisher is provided in JDK9. SubmissionPublisher inherits from flow Publisher has three constructors:

    public SubmissionPublisher() {
        this(ASYNC_POOL, Flow.defaultBufferSize(), null);
    }
    
    public SubmissionPublisher(Executor executor, int maxBufferCapacity) {
        this(executor, maxBufferCapacity, null);
    }

		public SubmissionPublisher(Executor executor, int maxBufferCapacity,
                               BiConsumer<? super Subscriber<? super T>, ? super Throwable> handler)

SubmissionPublisher will use the Executor as the thread pool to send information to subscribers. If you need to set the thread pool, you can pass it in by yourself. Otherwise, the commonPool() method of ForkJoinPool class will be used by default in the nonparametric constructor, that is, async in the nonparametric constructor_ Pool static variable.

Submission publisher will create a separate buffer space for each subscriber, and its size is determined by the input parameter maxBufferCapacity. Flow is used directly by default Defaultbuffersize(), 256 by default. If the buffer is full, it will determine whether to block waiting or discard data according to the policy when sending information.

SubmissionPublisher will call the last parameter handler method when the subscriber has an exception (in onNext processing), and then cancel the subscription. It is null by default, that is, exceptions will not be handled.

The simplest way to create SubmissionPublisher is to directly use the parameterless construction method:

SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();

As mentioned in the previous book, because SubmissionPublisher implements the AutoCloseable interface, you can use try to recycle resources, and you can omit the call to close():

try (SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>()){
}

However, you can also manually call the close() method to display the closed publisher. If you send data after closing, an exception will be thrown:

if (complete)
    throw new IllegalStateException("Closed");

Create subscriber

In the above, instead of manually creating subscribers, we directly call the consume method in SubmissionPublisher to consume messages with its internal subscribers. In this section, you can implement the interface flow Subscriber < T > create a SimpleSubscriber class:

public class SimpleSubscriber implements Flow.Subscriber<Integer> {
    private Flow.Subscription subscription;
    /**
     * Subscriber name
     */
    private String name;
    /**
     * Define maximum consumption quantity
     */
    private final long maxCount;
    /**
     * Counter
     */
    private long counter;
    public SimpleSubscriber(String name, long maxCount) {
        this.name = name;
        this.maxCount = maxCount <= 0 ? 1 : maxCount;
    }
    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        this.subscription = subscription;
        System.out.printf("subscriber:%s,Maximum consumption data: %d. %n", name, maxCount);
        // In fact, it is equal to all consumption data
        subscription.request(maxCount);
    }
    @Override
    public void onNext(Integer item) {
        counter++;
        System.out.printf("subscriber:%s Data received:%d.%n", name, item);
        if (counter >= maxCount) {
            System.out.printf("Preparing to cancel subscriber: %s. Number of processed data:%d. %n", name, counter);
            // Unsubscribe after processing
            subscription.cancel();
        }
    }
    @Override
    public void onError(Throwable t) {
        System.out.printf("subscriber: %s,Exception occurred: %s. %n", name, t.getMessage());
    }
    @Override
    public void onComplete() {
        System.out.printf("subscriber: %s Processing is complete.%n", name);
    }
}

SimpleSubscriber is a simple subscriber class. Its logic is to define its name and maximum processing data value maxCount according to the construction parameters, and process at least one data.

When the publisher makes a subscription, it will generate a token. Subscription is used as a parameter to call the onSubscribe method. The subscriber needs to capture the token as a link for subsequent interaction with the publisher. Generally speaking, request is called at least once in onSubscribe and the parameter needs to be > 0, otherwise the publisher will not be able to send any information to the subscriber, which is why maxCount needs to be greater than 0.

When the publisher starts sending data, it will asynchronously call the onNext method and pass in the data. A counter is used in this class to verify the data quantity. When the maximum value is reached, the publisher will be notified of the end of subscription asynchronously through the token (subscription), and then the sender will asynchronously call the onComplete method of the subscriber to complete the process.

The onError and onComplete methods only print, which will not be mentioned here.

The above subscriber can be regarded as an implementation of the push model, because when starting the subscription, the subscriber will agree on the quantity to be accepted, and then no new data will be requested in the subsequent processing (onNext).

We can create a subscriber named S1 that consumes 2 elements with the following code:

SimpleSubscriber sub1 = new SimpleSubscriber("S1", 2);

Subscription token interaction

After we can create the sender and subscriber, we need to confirm the sequence of interaction. Since the processing of response flow is the processing of events, the sequence of events is very important. The specific sequence is as follows:

  1. We create a publisher and a subscriber
  2. Subscribers subscribe to information by calling the publisher's subscribe() method. If the Subscription is successful, the publisher will generate a token (Subscription) and call the subscriber's Subscription event method onSubscribe() as an input parameter. If an exception is called, the subscriber's onError error handling method will be called directly, and an IllegalStateException will be thrown, and then the Subscription will be ended.
  3. In onSubscribe(), the subscriber needs to asynchronously request data from the publisher by calling the request method request(long) of the token (Subscription).
  4. When the publisher has data to publish, it will call the subscriber's onNext() method asynchronously until the total number of all messages has met the upper limit of the data request called by the subscriber. Therefore, when the number of messages requested by the subscriber is long MAX_ Value actually consumes all data, that is, push mode. If the publisher has no data to publish, it can call the publisher's own close() method and asynchronously call the onComplete() method of all subscribers to notify the end of subscription.
  5. Publishers can request more element requests from publishers at any time (usually in onNext) without waiting until the previous processing is completed, which is generally accumulated with the previous data quantity.
  6. When the publisher encounters an exception, it will call the subscriber's onError() method.

In the above description, only one subscriber is used for description. The following examples will show that publishers can have multiple subscribers (or even 0 subscribers).

Send message

When the publisher needs to push a message, it will call the submit method or offer method. We mentioned above that submit is actually a simple implementation of offer. Let's compare it in this section.

First, their method signature is:

int offer(T item, long timeout, TimeUnit unit, BiPredicate<Flow.Subscriber<? super T>,? super T> onDrop)
int offer(T item, BiPredicate<Flow.Subscriber <? super T>,? super T> onDrop)
int submit(T item)

The direct methods of submit and offer are:

    public int submit(T item) {
        return doOffer(item, Long.MAX_VALUE, null);
    }
    
    public int offer(T item,
                     BiPredicate<Subscriber<? super T>, ? super T> onDrop) {
    return doOffer(item, 0L, onDrop);

It can be seen that their underlying calls are all doffer methods, and the method signature of doffer is:

    private int doOffer(T item, long nanos,
                        BiPredicate<Subscriber<? super T>, ? super T> onDrop)

So we can look directly at the doOffer() method. The doOffer() method is an optional blocking duration, which is determined by the input parameter nano. onDrop() is a delete judge. If the test() method of BiPredicate is called and the result is true, it will try again (according to the combination of nextRetry attribute in the token and retryOffer() method in the publisher, but the specific implementation is not clear); If the result is false, delete the content directly. The results returned by doOffer() are positive and negative. The positive results are the data sent but not consumed by the subscriber (estimated value, because it is asynchronous multithreaded); If it is negative, the number of retries is returned.

Therefore, according to the parameters of submit(), we can find that submit will block until the data can be consumed (because there is no blocking timeout, there is no need to pass in the onDrop() method). We can configure the offer() selector as needed. If you have to consume all the data, you can directly select submit(). If you want to set the number of retries, you can choose to use offer()

Examples of asynchronous calls

Let's look at a specific program example. The program will release data in a period of 3 seconds:

public class PeriodicPublisher {

    public static final int WAIT_TIME = 2;
    public static final int SLEEP_TIME = 3;

    public static void main(String[] args) {
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();
        // Create 4 subscribers
        SimpleSubscriber subscriber1 = new SimpleSubscriber("S1", 2);
        SimpleSubscriber subscriber2 = new SimpleSubscriber("S2", 4);
        SimpleSubscriber subscriber3 = new SimpleSubscriber("S3", 6);
        SimpleSubscriber subscriber4 = new SimpleSubscriber("S4", 10);
        // The first three subscribers subscribe directly
        publisher.subscribe(subscriber1);
        publisher.subscribe(subscriber2);
        publisher.subscribe(subscriber3);
        // The fourth method delays subscription
        delaySubscribeWithWaitTime(publisher, subscriber4);
        // Start sending message
        Thread pubThread = publish(publisher, 5);
        try {
            // Wait for processing to complete
            pubThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static Thread publish(SubmissionPublisher<Integer> publisher, int count) {
        Thread t = new Thread(() -> {
            IntStream.range(1,count)
                    .forEach(item ->{
                        publisher.submit(item);
                        sleep(item);
                    });
            publisher.close();
        });
        t.start();
        return t;
    }
    
    
    private static void sleep(Integer item) {
        try {
            System.out.printf("Push data:%d. Sleep for 3 seconds.%n", item);
            TimeUnit.SECONDS.sleep(SLEEP_TIME);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private static void delaySubscribeWithWaitTime(SubmissionPublisher<Integer> publisher, Flow.Subscriber<Integer> sub) {
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(WAIT_TIME);
                publisher.subscribe(sub);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

}

After the code, the running results are as follows:

subscriber: S1,Maximum consumption data: 2.
Push data: 1. Sleep for 3 seconds.
subscriber: S3,Maximum consumption data: 6.
subscriber: S2,Maximum consumption data: 4.
subscriber: S2 Data received: 1.
subscriber: S3 Data received: 1.
subscriber: S1 Data received: 1.
subscriber: S4,Maximum consumption data: 10.
Push data: 2. Sleep for 3 seconds.
subscriber: S2 Received data: 2.
subscriber: S3 Received data: 2.
subscriber: S1 Received data: 2.
subscriber: S4 Received data: 2.
Preparing to cancel subscriber: S1. Number of processed data: 2.
Push data: 3. Sleep for 3 seconds.
subscriber: S4 Data received: 3.
subscriber: S2 Data received: 3.
subscriber: S3 Data received: 3.
Push data: 4. Sleep for 3 seconds.
subscriber: S4 Data received: 4.
subscriber: S3 Data received: 4.
subscriber: S2 Data received: 4.
Preparing to cancel subscriber: S2. Number of processed data: 4.
Push data: 5. Sleep for 3 seconds.
subscriber: S3 Received data: 5.
subscriber: S4 Received data: 5.
subscriber: S3 Processing is complete.
subscriber: S4 Processing is complete.

Because it is executed asynchronously, the order in the "receive data" section may be different.

Let's analyze the execution process of the program.

  • Create a publisher instance
  • Create four subscriber instances S1, S2, S3 and S4, and the number of data that can be received is 2, 4, 6 and 10 respectively.
  • The first three subscribers immediately subscribe to the message.
  • The subscriber of S4 creates a separate thread to wait for the wait_ Subscribe to data after time seconds (2 seconds).
  • Create a new thread to SLEEP_TIME seconds (3 seconds) is an interval of 5 data releases.
  • Join the publish thread () and wait for the process to end.

The log executed meets the above process, and some key points are:

  • S4 did not subscribe when the sender pushed the data "1", so S4 did not receive the data "1".
  • When sending data "2", S1 has received enough expected data, so the subscription is cancelled. After that, there are only S2, S3 and S4 left.
  • When sending data "4", S2 has received enough expected data, so the subscription is cancelled. After that, there are only S3 and S4 left.
  • When sending data "5", only S3 and S4 are left. After sending, publisher calls the close() method to notify S3 and S4 that the data processing is completed.

It should be noted that if you close() directly after the final submit, the subscriber may not be able to complete the execution. However, since there is a 3-second wait after any submit(), this program can be executed.

last

The example in this article is a simple implementation, which can be used to test the effect of back pressure by adjusting the parameter of request in the subscriber and adding the request call in onNext. You can also adjust submit to offer and add the onDrop method to observe the process of discarding information. At the same time, this article does not provide an example of Processor. You can also learn by yourself.

To summarize the process: the subscriber subscribes to the publisher, and then the publisher sends a token to the subscriber. The subscriber requests the message using the token, and the sender pushes the message according to the number of request messages. Subscribers can add more information asynchronously at any time.

JDK9 implements four interfaces of Java API in the Flow interface, and provides submissionpublisher < T > as a simple implementation of publisher < T > interface.

Topics: Java Back-end