(juc Series) flow responsive flow interface and implementation of submissionpublisher

The source code of this article is based on JDK13

Flow

Official annotation translation

For some interfaces and static methods, in order to establish streaming components, Publisher generates elements, which are consumed by one or more subscribers, and each Subscriber is managed by Subscription

Interface introduction: reactive-streams They are suitable for concurrent and distributed environments. All methods are defined as Wu Xiao's one-way message style

Communication depends on a simple form of flow control. It can be used to avoid resource management problems in push type systems

Example:

A Flow.Publisher usually defines its own Subscription implementation, creates one in the subscribe method, and then asks it to give it to Flow.Subscriber.

Bucket asynchronous message publishing usually uses a thread pool. The following is a simple publisher, which only publishes a TRUE to a single subscriber. Because the subscriber receives only a simple element, this class does not need to use buffering and sequence control

 class OneShotPublisher implements Publisher<Boolean> {
    //Thread pool
   private final ExecutorService executor = ForkJoinPool.commonPool(); // daemon-based
    // Whether to be subscribed, because this publisher can only be subscribed by one person
   private boolean subscribed; // true after first subscribe
    // Subscription method
   public synchronized void subscribe(Subscriber<? super Boolean> subscriber) {
     if (subscribed)
       subscriber.onError(new IllegalStateException()); // only one allowed
     else {
         // Subscription succeeded
       subscribed = true;
       subscriber.onSubscribe(new OneShotSubscription(subscriber, executor));
     }
   }
   // subscription management 
   static class OneShotSubscription implements Subscription {
       // subscriber
     private final Subscriber<? super Boolean> subscriber;
     // Thread pool
     private final ExecutorService executor;
     // result
     private Future<?> future; // to allow cancellation
       // Complete
     private boolean completed;
     // Construction method
     OneShotSubscription(Subscriber<? super Boolean> subscriber,
                         ExecutorService executor) {
       this.subscriber = subscriber;
       this.executor = executor;
     }
     // request
     public synchronized void request(long n) {
         // Not completed
       if (!completed) {
         completed = true;
         if (n <= 0) {
           IllegalArgumentException ex = new IllegalArgumentException();
           executor.execute(() -> subscriber.onError(ex));
         } else {
             // Execution method
           future = executor.submit(() -> {
             subscriber.onNext(Boolean.TRUE);
             subscriber.onComplete();
           });
         }
       }
     }
     // cancel
     public synchronized void cancel() {
       completed = true;
       if (future != null) future.cancel(false);
     }
   }
 }

This is a very simple application scenario. A single publisher publishes messages to a single consumer

A Flow.Subscriber arranges the request and processing of elements. Elements will not be published before calling request, but multiple elements may be requested

Many Subscriber implementations can manage elements according to the following style. The buffer size is usually 1 single step. Larger buffer size usually allows more efficient overlapping processing and less communication at the same time

For example, given a number of 64, the total number of outstanding requests will remain between 32 and 64. Because the calls of Subscriber methods are strictly ordered, these methods do not need to use locks or volatile unless the Subscriber maintains multiple subscriptions

 class SampleSubscriber<T> implements Subscriber<T> {
   final Consumer<? super T> consumer;
   Subscription subscription;
   final long bufferSize;
   long count;
   SampleSubscriber(long bufferSize, Consumer<? super T> consumer) {
     this.bufferSize = bufferSize;
     this.consumer = consumer;
   }
   
   public void onSubscribe(Subscription subscription) {
     long initialRequestSize = bufferSize;
     count = bufferSize - bufferSize / 2; // re-request when half consumed
     (this.subscription = subscription).request(initialRequestSize);
   }
   public void onNext(T item) {
     if (--count <= 0)
       subscription.request(count = bufferSize - bufferSize / 2);
     consumer.accept(item);
   }
   public void onError(Throwable ex) { ex.printStackTrace(); }
   public void onComplete() {}
 }

The default value of defaultBufferSize usually provides a useful seven points for selecting the request size and capacity in the Flow component according to the expected rate and resource usage. Alternatively, when Flow control is not required, subscribers can initialize an unbounded queue collection

 class UnboundedSubscriber<T> implements Subscriber<T> {
   public void onSubscribe(Subscription subscription) {
     subscription.request(Long.MAX_VALUE); // effectively unbounded
   }
   public void onNext(T item) { use(item); }
   public void onError(Throwable ex) { ex.printStackTrace(); }
   public void onComplete() {}
   void use(T item) { ... }
 }

Source code

Publisher publisher

    public static interface Publisher<T> {
        public void subscribe(Subscriber<? super T> subscriber);
    }

Defines adding a subscriber to Publisher

Subscriber subscriber

    public static interface Subscriber<T> {
        public void onSubscribe(Subscription subscription);

        public void onNext(T item);

        public void onError(Throwable throwable);

        public void onComplete();
    }

The subscriber's interface defines:

  • onSubscribe adding a subscription TODO is incorrect
  • onNext handles an element
  • onError error
  • onComplete complete

Subscription subscription

A message manager that links between publishers and subscribers

    public static interface Subscription {
        /**
         * Adds the given number {@code n} of items to the current
         * unfulfilled demand for this subscription.  If {@code n} is
         * less than or equal to zero, the Subscriber will receive an
         * {@code onError} signal with an {@link
         * IllegalArgumentException} argument.  Otherwise, the
         * Subscriber will receive up to {@code n} additional {@code
         * onNext} invocations (or fewer if terminated).
         *
         * @param n the increment of demand; a value of {@code
         * Long.MAX_VALUE} may be considered as effectively unbounded
         */
        public void request(long n);

        /**
         * Causes the Subscriber to (eventually) stop receiving
         * messages.  Implementation is best-effort -- additional
         * messages may be received after invoking this method.
         * A cancelled subscription need not ever receive an
         * {@code onComplete} or {@code onError} signal.
         */
        public void cancel();
    }
  • request adds a given number of elements
  • Cancel cancel

Processor

At the same time, it implements a component class of producer and consumer ~

SubmissionPublisher

Official annotation translation

A Flow.Publisher asynchronously submits non empty elements to its subscribers until they are closed. Each subscriber accepts newly submitted elements in the same order. Unless exceptions are encountered. SubmissionPublisher allows element generation to be compatible with reactive streams. The publisher relies on dop or blocking for flow control

Submission publisher uses a thread pool to submit to its subscribers. The thread pool is selected according to its usage field

If the submitted element runs in a separate thread and the number of subscribers can be estimated, you can use Executors.newFixedThreadPool. Otherwise, ForkJoinPoll.commonPool. Is used by default

Buffers allow producers and consumers to temporarily run at different rates. Each subscriber uses a separate buffer. The buffer is rebuilt on first use and expanded as needed

The call of request does not directly lead to the expansion of the buffer. However, if the filled request exceeds the maximum capacity, there is a risk of saturation. Flow.defaultBufferSize provides seven points of capacity, based on the expected speed, resources and usage

The publish method supports different strategies for buffer saturation. The submit code block knows that resources are available. This is the simplest strategy, but the slowest. The offer method may discard elements, but provides an opportunity to insert processing and retry

If some subscriber's methods throw exceptions, their subscription will be cancelled. If a handler is submitted in the constructor method, the onNext method will call the processing method if an exception occurs, but the onSubscribeOnError and OnComplete methods do not record and handle exceptions

If RejectedExecutionException or other runtime exceptions occur when submitting to the thread pool, or an exception is thrown by a discard processor. Not all subscribers can receive the published elements

The consume method simplifies support for common situations where the subscriber's only action is to request and process all items using the provided function

This class can also serve as a basis for subclasses of generated items and use the methods in this class to publish them. For example:

Here is a class that periodically publishes publishing elements. (in fact, you can add methods to start and stop independently, share thread pools among publishers, etc., or use SubmissionPublisher as a component instead of a superclass.)

 class PeriodicPublisher<T> extends SubmissionPublisher<T> {
    // Periodic task
    final ScheduledFuture<?> periodicTask;
    // Thread pool
    final ScheduledExecutorService scheduler;

    PeriodicPublisher(Executor executor, int maxBufferCapacity,
                      Supplier<? extends T> supplier,
                      long period, TimeUnit unit) {
        super(executor, maxBufferCapacity);
        scheduler = new ScheduledThreadPoolExecutor(1);
        periodicTask = scheduler.scheduleAtFixedRate(
                () -> submit(supplier.get()), 0, period, unit);
    }

    public void close() {
        periodicTask.cancel(false);
        scheduler.shutdown();
        super.close();
    }
}

Here is an implementation example of Flow.Processor. It uses one-step request to its publisher. The more adaptable version can use the delay of submission and return and other methods to monitor the flow

 class TransformProcessor<S,T> extends SubmissionPublisher<T>
   implements Flow.Processor<S,T> {
   final Function<? super S, ? extends T> function;
   Flow.Subscription subscription;
   TransformProcessor(Executor executor, int maxBufferCapacity,
                      Function<? super S, ? extends T> function) {
     super(executor, maxBufferCapacity);
     this.function = function;
   }
   public void onSubscribe(Flow.Subscription subscription) {
     (this.subscription = subscription).request(1);
   }
   public void onNext(S item) {
     subscription.request(1);
     submit(function.apply(item));
   }
   public void onError(Throwable ex) { closeExceptionally(ex); }
   public void onComplete() { close(); }
 }

It's hard to understand... After translation, it became more difficult to understand

I highly recommend this article. I have read it clearly:

Java9 reactive stream

Source code introduction

SubmissionPublisher publisher feature

This class is also the outermost class

attribute
    // Linked list of subscribers
    BufferedSubscription<T> clients;

    // Is it closed
    volatile boolean closed;
    // Exception causing shutdown
    volatile Throwable closedException;

    // Thread pool
    final Executor executor;
    // handler processor
    final BiConsumer<? super Subscriber<? super T>, ? super Throwable> onNextHandler;
    // Maximum buffer capacity
    final int maxBufferCapacity;

A publisher can be subscribed by multiple subscribers. These subscribers use a linked list to save. In addition, some states of the current publisher are recorded, which are detailed in the comments

Construction method
    public SubmissionPublisher(Executor executor, int maxBufferCapacity,
                               BiConsumer<? super Subscriber<? super T>, ? super Throwable> handler) {
        if (executor == null)
            throw new NullPointerException();
        if (maxBufferCapacity <= 0)
            throw new IllegalArgumentException("capacity must be positive");
        this.executor = executor;
        this.onNextHandler = handler;
        this.maxBufferCapacity = roundCapacity(maxBufferCapacity);
    }

    public SubmissionPublisher(Executor executor, int maxBufferCapacity) {
        this(executor, maxBufferCapacity, null);
    }

    public SubmissionPublisher() {
        this(ASYNC_POOL, Flow.defaultBufferSize(), null);
    }

Assign value after parameter verification

subscribe subscription method

This is the implementation method as the publisher interface

    public void subscribe(Subscriber<? super T> subscriber) {
        if (subscriber == null) throw new NullPointerException();
        int max = maxBufferCapacity; // allocate initial array
        Object[] array = new Object[max < INITIAL_CAPACITY ?
                                    max : INITIAL_CAPACITY];
        // Create subscription token
        BufferedSubscription<T> subscription =
            new BufferedSubscription<T>(subscriber, executor, onNextHandler,
                                        array, max);
        // Lock execution
        synchronized (this) {
            // The thread that records the first subscriber
            if (!subscribed) {
                subscribed = true;
                owner = Thread.currentThread();
            }
            for (BufferedSubscription<T> b = clients, pred = null;;) {
                // The current subscriber is the first
                if (b == null) {
                    Throwable ex;
                    subscription.onSubscribe();
                    if ((ex = closedException) != null)
                        subscription.onError(ex);
                    else if (closed)
                        subscription.onComplete();
                    else if (pred == null)
                        clients = subscription;
                    else
                        pred.next = subscription;
                    break;
                }
                // Link to back
                BufferedSubscription<T> next = b.next;
                if (b.isClosed()) {   // remove
                    b.next = null;    // detach
                    if (pred == null)
                        clients = next;
                    else
                        pred.next = next;
                }
                else if (subscriber.equals(b.subscriber)) {
                    b.onError(new IllegalStateException("Duplicate subscribe"));
                    break;
                }
                else
                    pred = b;
                b = next;
            }
        }
    }
  1. First, construct a subscription token based on the current subscriber
  2. Find the tail of the linked list and insert the current subscriber
  3. After that, we call the OnSubscribe method of the subscription token. We will look at the code of the subscriber and token later.
The submit submission element is published by the publisher
    public int submit(T item) {
        return doOffer(item, Long.MAX_VALUE, null);
    }

    private int doOffer(T item, long nanos,
                        BiPredicate<Subscriber<? super T>, ? super T> onDrop) {
        if (item == null) throw new NullPointerException();
        int lag = 0;
        boolean complete, unowned;
        synchronized (this) {
            Thread t = Thread.currentThread(), o;
            BufferedSubscription<T> b = clients;
            if ((unowned = ((o = owner) != t)) && o != null)
                owner = null;                     // disable bias
            if (b == null)
                complete = closed;
            else {
                complete = false;
                boolean cleanMe = false;
                BufferedSubscription<T> retries = null, rtail = null, next;
                // Circularly call the offer method of the token to publish the message
                do {
                    next = b.next;
                    int stat = b.offer(item, unowned);
                    if (stat == 0) {              // saturated; add to retry list
                        b.nextRetry = null;       // avoid garbage on exceptions
                        if (rtail == null)
                            retries = b;
                        else
                            rtail.nextRetry = b;
                        rtail = b;
                    }
                    else if (stat < 0)            // closed
                        cleanMe = true;           // remove later
                    else if (stat > lag)
                        lag = stat;
                } while ((b = next) != null);

                if (retries != null || cleanMe)
                    lag = retryOffer(item, nanos, onDrop, retries, lag, cleanMe);
            }
        }
        if (complete)
            throw new IllegalStateException("Closed");
        else
            return lag;
    }

ConsumerSubscriber subscriber implementation

    static final class ConsumerSubscriber<T> implements Subscriber<T> {
        final CompletableFuture<Void> status;
        final Consumer<? super T> consumer;
        Subscription subscription;
        // Save the Consumer, status, and token
        ConsumerSubscriber(CompletableFuture<Void> status,
                           Consumer<? super T> consumer) {
            this.status = status; this.consumer = consumer;
        }
        // The issuer sends the token back to the subscriber
        public final void onSubscribe(Subscription subscription) {
            this.subscription = subscription;
            status.whenComplete((v, e) -> subscription.cancel());
            if (!status.isDone())
                subscription.request(Long.MAX_VALUE);
        }
        // error handling
        public final void onError(Throwable ex) {
            status.completeExceptionally(ex);
        }
        // complete
        public final void onComplete() {
            status.complete(null);
        }
        // Process the next element, Consumer execution
        public final void onNext(T item) {
            try {
                consumer.accept(item);
            } catch (Throwable ex) {
                subscription.cancel();
                status.completeExceptionally(ex);
            }
        }
    }

This class is relatively simple, because no specific business implementation is implemented, it only accepts the token, handles the error, completes, and receives the publisher's message every time, then calls the initialization Consumer to consume. "

Implementation of BufferedSubscription subscription token

        long timeout;                      // Long.MAX_VALUE if untimed wait
        int head;                          // next position to take
        int tail;                          // next position to put
        final int maxCapacity;             // max buffer size
        volatile int ctl;                  // atomic run state flags
        Object[] array;                    // buffer
        final Subscriber<? super T> subscriber;
        final BiConsumer<? super Subscriber<? super T>, ? super Throwable> onNextHandler;
        Executor executor;                 // null on error
        Thread waiter;                     // blocked producer thread
        Throwable pendingError;            // holds until onError issued
        BufferedSubscription<T> next;      // used only by publisher
        BufferedSubscription<T> nextRetry; // used only by publisher

        @jdk.internal.vm.annotation.Contended("c") // segregate
        volatile long demand;              // # unfilled requests
        @jdk.internal.vm.annotation.Contended("c")
        volatile int waiting;              // nonzero if producer blocked

        // ctl bit values
        static final int CLOSED   = 0x01;  // if set, other bits ignored
        static final int ACTIVE   = 0x02;  // keep-alive for consumer task
        static final int REQS     = 0x04;  // (possibly) nonzero demand
        static final int ERROR    = 0x08;  // issues onError when noticed
        static final int COMPLETE = 0x10;  // issues onComplete when done
        static final int RUN      = 0x20;  // task is or will be running
        static final int OPEN     = 0x40;  // true after subscribe

        static final long INTERRUPTED = -1L; // timeout vs interrupt sentinel

This is actually the subscription implementation saved in the publisher, which is a linked list node

  • array saves the messages in the current subscription token
  • Next implements the next node pointer of the linked list node
offer accept message

In the publisher, the message is published through the offer of the internal linked list node, which is here

        // Writes elements to an array
        final int offer(T item, boolean unowned) {
            Object[] a;
            int stat = 0, cap = ((a = array) == null) ? 0 : a.length;
            int t = tail, i = t & (cap - 1), n = t + 1 - head;
            if (cap > 0) {
                boolean added;
                if (n >= cap && cap < maxCapacity) // resize
                    added = growAndOffer(item, a, t);
                else if (n >= cap || unowned)      // need volatile CAS
                    added = QA.compareAndSet(a, i, null, item);
                else {                             // can use release mode
                    QA.setRelease(a, i, item);
                    added = true;
                }
                if (added) {
                    tail = t + 1;
                    stat = n;
                }
            }
            return startOnOffer(stat);
        }
        // After the element joins the team, try to start a task consumption
        final int startOnOffer(int stat) {
            int c; // start or keep alive if requests exist and not active
            if (((c = ctl) & (REQS | ACTIVE)) == REQS &&
                ((c = getAndBitwiseOrCtl(RUN | ACTIVE)) & (RUN | CLOSED)) == 0)
                tryStart();
            else if ((c & CLOSED) != 0)
                stat = -1;
            return stat;
        }

         // Try to start a task and call the current consumer method
        final void tryStart() {
            try {
                Executor e;
                ConsumerTask<T> task = new ConsumerTask<T>(this);
                if ((e = executor) != null)   // skip if disabled on error
                    e.execute(task);
            } catch (RuntimeException | Error ex) {
                getAndBitwiseOrCtl(ERROR | CLOSED);
                throw ex;
            }
        }

summary

It's complicated. I didn't look at the code carefully. I just need to know the general implementation

SubmissionPublisher implements the interface defined in the Flow class and provides a set of responsive API s. Its call chain is about:

Note that all operations are asynchronous

  1. Subscriber registers himself with Publisher and calls Publisher.subscribe()
  2. Publisher accepts registration, generates a token, returns it to Subscriber, and calls Subscriber.onSubscribe()
  3. The Subscriber tells Publisher how many messages he needs through the token Subscription.request() (note that this step can tell the maximum value at one time or in batches)
  4. The program publishes a message through Publisher.submit(). Publisher calls their offer methods one by one through the internally saved Subscription linked list. The number of messages required by each subscriber needs to be considered
  5. Subscription starts the task according to its strategy, whether to buffer or not, and invokes the Subscriber.onNext execution method in the task.

Reference articles

End.

Contact me

Finally, welcome to my personal official account, Yan Yan ten, which will update many learning notes from the backend engineers. I also welcome direct official account or personal mail or email to contact me.

The above are all personal thoughts. If there are any mistakes, please correct them in the comment area.

Welcome to reprint, please sign and keep the original link.

Contact email: huyanshi2580@gmail.com

For more study notes, see personal blog or WeChat official account, Yan Yan ten > > Huyan ten

Posted on Thu, 11 Nov 2021 03:03:19 -0500 by MrLister