org.glassfish.jersey.internal.util.JerseyPublisher Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2017 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://oss.oracle.com/licenses/CDDL+GPL-1.1
* or LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.glassfish.jersey.internal.util;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import org.glassfish.jersey.internal.LocalizationMessages;
import org.glassfish.jersey.internal.jsr166.Flow;
import org.glassfish.jersey.internal.jsr166.SubmissionPublisher;
/**
* Implementation of {@link Flow.Publisher} corresponding to reactive streams specification.
*
* Delegates to {@link SubmissionPublisher} repackaged from jsr166.
*
* @author Adam Lindenthal (adam.lindenthal at oracle.com)
*/
public class JerseyPublisher implements Flow.Publisher {
private static final int DEFAULT_BUFFER_CAPACITY = 256;
private SubmissionPublisher submissionPublisher = new SubmissionPublisher<>();
private final PublisherStrategy strategy;
/**
* Creates a new JerseyPublisher using the {@link ForkJoinPool#commonPool()} for async delivery to subscribers
* (unless it does not support a parallelism level of at least two, in which case, a new Thread is created to run
* each task), with maximum buffer capacity of {@value DEFAULT_BUFFER_CAPACITY} and default {@link PublisherStrategy},
* which is {@link PublisherStrategy#BEST_EFFORT}.
*/
public JerseyPublisher() {
this(ForkJoinPool.commonPool(), DEFAULT_BUFFER_CAPACITY, PublisherStrategy.BEST_EFFORT);
}
/**
* Creates a new JerseyPublisher using the {@link ForkJoinPool#commonPool()} for async delivery to subscribers
* (unless it does not support a parallelism level of at least two, in which case, a new Thread is created to run
* each task), with maximum buffer capacity of {@value DEFAULT_BUFFER_CAPACITY} and given {@link PublisherStrategy}.
*
* @param strategy publisher delivering strategy
*/
public JerseyPublisher(final PublisherStrategy strategy) {
this(ForkJoinPool.commonPool(), DEFAULT_BUFFER_CAPACITY, strategy);
}
/**
* Creates a new JerseyPublisher using the given {@link Executor} for async delivery to subscribers, with the default
* maximum buffer size of {@value DEFAULT_BUFFER_CAPACITY} and default {@link PublisherStrategy}, which is
* {@link PublisherStrategy#BEST_EFFORT}.
*
* @param executor {@code Executor} the executor to use for async delivery,
* supporting creation of at least one independent thread
* @throws NullPointerException if executor is null
* @throws IllegalArgumentException if maxBufferCapacity not positive
*/
public JerseyPublisher(final Executor executor) {
this(executor, PublisherStrategy.BEST_EFFORT);
}
/**
* Creates a new JerseyPublisher using the given {@link Executor} for async delivery to subscribers, with the default
* maximum buffer size of {@value DEFAULT_BUFFER_CAPACITY} and given {@link PublisherStrategy}.
*
* @param executor {@code Executor} the executor to use for async delivery,
* supporting creation of at least one independent thread
* @param strategy publisher delivering strategy
* @throws NullPointerException if executor is null
* @throws IllegalArgumentException if maxBufferCapacity not positive
*/
public JerseyPublisher(final Executor executor, final PublisherStrategy strategy) {
this.strategy = strategy;
submissionPublisher = new SubmissionPublisher<>(executor, DEFAULT_BUFFER_CAPACITY);
}
/**
* Creates a new JerseyPublisher using the {@link ForkJoinPool#commonPool()} for async delivery to subscribers
* (unless it does not support a parallelism level of at least two, in which case, a new Thread is created to run
* each task), with specified maximum buffer capacity and default {@link PublisherStrategy}, which is
* {@link PublisherStrategy#BEST_EFFORT}.
*
* @param maxBufferCapacity the maximum capacity for each
* subscriber's buffer (the enforced capacity may be rounded up to
* the nearest power of two and/or bounded by the largest value
* supported by this implementation; method {@link #getMaxBufferCapacity}
* returns the actual value)
*/
public JerseyPublisher(final int maxBufferCapacity) {
this(ForkJoinPool.commonPool(), maxBufferCapacity, PublisherStrategy.BEST_EFFORT);
}
/**
* Creates a new JerseyPublisher using the given {@link Executor} for async delivery to subscribers, with the given
* maximum buffer size for each subscriber and given {@link PublisherStrategy}.
*
* @param executor {@code Executor} the executor to use for async delivery,
* supporting creation of at least one independent thread
* @param maxBufferCapacity the maximum capacity for each
* subscriber's buffer (the enforced capacity may be rounded up to
* the nearest power of two and/or bounded by the largest value
* supported by this implementation; method {@link #getMaxBufferCapacity}
* returns the actual value)
* @param strategy publisher delivering strategy
* @throws NullPointerException if executor is null
* @throws IllegalArgumentException if maxBufferCapacity not positive
*/
public JerseyPublisher(final Executor executor, final int maxBufferCapacity, PublisherStrategy strategy) {
this.strategy = strategy;
submissionPublisher = new SubmissionPublisher<>(executor, maxBufferCapacity);
}
@Override
public void subscribe(final Flow.Subscriber super T> subscriber) {
submissionPublisher.subscribe(new SubscriberWrapper(subscriber));
}
/**
* Publishes the given item to each current subscriber by asynchronously invoking its onNext method.
*
* Blocks uninterruptibly while resources for any subscriber are unavailable.
*
* @param data published data
* @return the estimated maximum lag among subscribers
* @throws IllegalStateException if closed
* @throws NullPointerException if data is null
* @throws java.util.concurrent.RejectedExecutionException if thrown by Executor
*/
private int submit(final T data) {
return submissionPublisher.submit(data);
}
/**
* Processes all published items using the given Consumer function. Returns a CompletableFuture that is completed
* normally when this publisher signals {@code onComplete()}, or completed exceptionally upon any error, or an
* exception is thrown by the Consumer, or the returned CompletableFuture is cancelled, in which case no further
* items are processed.
*
* @param consumer function to process all published data
* @return a {@link CompletableFuture} that is completed normally when the publisher signals onComplete,
* and exceptionally upon any error or cancellation
* @throws NullPointerException if consumer is null
*/
public CompletableFuture consume(final Consumer super T> consumer) {
return submissionPublisher.consume(consumer);
}
/**
* Publishes the given item, if possible, to each current subscriber
* by asynchronously invoking its
* {@link Flow.Subscriber#onNext(Object) onNext} method.
* The item may be dropped by one or more subscribers if resource
* limits are exceeded, in which case the given handler (if non-null)
* is invoked, and if it returns true, retried once. Other calls to
* methods in this class by other threads are blocked while the
* handler is invoked. Unless recovery is assured, options are
* usually limited to logging the error and/or issuing an {@link
* Flow.Subscriber#onError(Throwable) onError}
* signal to the subscriber.
*
* This method returns a status indicator: If negative, it
* represents the (negative) number of drops (failed attempts to
* issue the item to a subscriber). Otherwise it is an estimate of
* the maximum lag (number of items submitted but not yet
* consumed) among all current subscribers. This value is at least
* one (accounting for this submitted item) if there are any
* subscribers, else zero.
*
* If the Executor for this publisher throws a
* RejectedExecutionException (or any other RuntimeException or
* Error) when attempting to asynchronously notify subscribers, or
* the drop handler throws an exception when processing a dropped
* item, then this exception is rethrown.
*
* @param item the (non-null) item to publish
* @param onDrop if non-null, the handler invoked upon a drop to a
* subscriber, with arguments of the subscriber and item; if it
* returns true, an offer is re-attempted (once)
* @return if negative, the (negative) number of drops; otherwise
* an estimate of maximum lag
* @throws IllegalStateException if closed
* @throws NullPointerException if item is null
* @throws RejectedExecutionException if thrown by Executor
*/
private int offer(T item, BiPredicate, ? super T> onDrop) {
return offer(item, 0, TimeUnit.MILLISECONDS, onDrop);
}
/**
* Publishes the given item, if possible, to each current subscriber
* by asynchronously invoking its {@link
* Flow.Subscriber#onNext(Object) onNext} method,
* blocking while resources for any subscription are unavailable,
* up to the specified timeout or until the caller thread is
* interrupted, at which point the given handler (if non-null) is
* invoked, and if it returns true, retried once. (The drop handler
* may distinguish timeouts from interrupts by checking whether
* the current thread is interrupted.)
* Other calls to methods in this class by other
* threads are blocked while the handler is invoked. Unless
* recovery is assured, options are usually limited to logging the
* error and/or issuing an
* {@link Flow.Subscriber#onError(Throwable) onError}
* signal to the subscriber.
*
* This method returns a status indicator: If negative, it
* represents the (negative) number of drops (failed attempts to
* issue the item to a subscriber). Otherwise it is an estimate of
* the maximum lag (number of items submitted but not yet
* consumed) among all current subscribers. This value is at least
* one (accounting for this submitted item) if there are any
* subscribers, else zero.
*
* If the Executor for this publisher throws a
* RejectedExecutionException (or any other RuntimeException or
* Error) when attempting to asynchronously notify subscribers, or
* the drop handler throws an exception when processing a dropped
* item, then this exception is rethrown.
*
* @param item the (non-null) item to publish
* @param timeout how long to wait for resources for any subscriber
* before giving up, in units of {@code unit}
* @param unit a {@code TimeUnit} determining how to interpret the
* {@code timeout} parameter
* @param onDrop if non-null, the handler invoked upon a drop to a
* subscriber, with arguments of the subscriber and item; if it
* returns true, an offer is re-attempted (once)
* @return if negative, the (negative) number of drops; otherwise
* an estimate of maximum lag
* @throws IllegalStateException if closed
* @throws NullPointerException if item is null
* @throws RejectedExecutionException if thrown by Executor
*/
private int offer(T item,
long timeout,
TimeUnit unit,
BiPredicate, ? super T> onDrop) {
BiPredicate, ? super T> callback;
callback = onDrop == null
? this::onDrop
: (BiPredicate, T>)
(subscriber, data) -> {
onDrop.test(getSubscriberWrapper(subscriber).getWrappedSubscriber(), data);
return false;
};
return submissionPublisher.offer(item, timeout, unit, callback);
}
private boolean onDrop(Flow.Subscriber super T> subscriber, T t) {
subscriber.onError(new IllegalStateException(LocalizationMessages.SLOW_SUBSCRIBER(t)));
getSubscriberWrapper(subscriber).getSubscription().cancel();
return false;
}
private SubscriberWrapper getSubscriberWrapper(Flow.Subscriber subscriber) {
if (subscriber instanceof SubscriberWrapper) {
return ((SubscriberWrapper) subscriber);
} else {
throw new IllegalArgumentException(LocalizationMessages.UNKNOWN_SUBSCRIBER());
}
}
/**
* Publishes the given item to all current subscribers by invoking its {@code onNext() method} using {@code Executor}
* provided as constructor parameter (or the default {@code Executor} if not provided).
*
* Concrete behaviour is specified by {@link PublisherStrategy} selected upon {@code JerseyPublisher} creation.
*
* @param item the (non-null) item to publish.
* @return if negative, the (negative) number of drops; otherwise an estimate of maximum lag.
* @throws IllegalStateException if closed
* @throws NullPointerException if item is null
* @throws RejectedExecutionException if thrown by {@code Executor}
*/
public int publish(T item) {
if (PublisherStrategy.BLOCKING == strategy) {
return submit(item);
} else {
// PublisherStrategy.BEST_EFFORT
return submissionPublisher.offer(item, this::onDrop);
}
}
/**
* Unless already closed, issues {@code onComplete()} signals to current subscribers, and disallows subsequent
* attempts to publish. Upon return, this method does NOT guarantee that all subscribers have yet
* completed.
*/
public void close() {
submissionPublisher.close();
}
/**
* Issues onError signals to current subscribers with the given error, and disallows subsequent attempts to publish.
*
* @param error the {@code onError} argument sent to subscribers
* @throws NullPointerException if error is null
*/
public void closeExceptionally(final Throwable error) {
submissionPublisher.closeExceptionally(error);
}
/**
* Returns an estimate of the maximum number of items produced but not yet consumed among all current subscribers.
*
* @return estimated maximum lag
*/
public int estimateMaximumLag() {
return submissionPublisher.estimateMaximumLag();
}
/**
* Returns an estimate of the minimum number of items requested but not yet produced, among all current subscribers.
*
* @return estimated minimum demand
*/
public long estimateMinimumDemand() {
return submissionPublisher.estimateMinimumDemand();
}
/**
* Returns the exception associated with {@link #closeExceptionally}, or null if not closed or if closed normally.
*
* @return exception thrown on closing or {@code null}
*/
public Throwable getClosedException() {
return submissionPublisher.getClosedException();
}
/**
* Returns the maximum per-subscriber buffer capacity.
*
* @return the maximum per-subscriber buffer capacity
*/
public int getMaxBufferCapacity() {
return submissionPublisher.getMaxBufferCapacity();
}
public static class SubscriberWrapper implements Flow.Subscriber {
private Flow.Subscriber super T> subscriber;
private Flow.Subscription subscription = null;
public SubscriberWrapper(Flow.Subscriber super T> subscriber) {
this.subscriber = subscriber;
}
@Override
public void onSubscribe(final Flow.Subscription subscription) {
this.subscription = subscription;
subscriber.onSubscribe(new Flow.Subscription() {
@Override
public void request(final long n) {
subscription.request(n);
}
@Override
public void cancel() {
subscription.cancel();
}
});
}
@Override
public void onNext(final T item) {
subscriber.onNext(item);
}
@Override
public void onError(final Throwable throwable) {
subscriber.onError(throwable);
}
@Override
public void onComplete() {
subscriber.onComplete();
}
public Flow.Subscriber super T> getWrappedSubscriber() {
return subscriber;
}
/**
* Get reference to subscriber's {@link Flow.Subscription}.
*
* @return subscriber's {@code subscription}
*/
public Flow.Subscription getSubscription() {
return this.subscription;
}
}
public enum PublisherStrategy{
/**
* Blocking publisher strategy - tries to deliver to all subscribers regardless the cost.
*
* The thread is blocked uninterruptibly while resources for any subscriber are unavailable.
* This strategy comes with a risk of thread exhaustion, that will lead to publisher being completely blocked by slow
* or incorrectly implemented subscribers.
*/
BLOCKING,
/**
* Best effort publisher strategy - tries to deliver to all subscribers if possible without blocking the processing.
*
* If the buffer is full, publisher invokes {@code onError()} and cancels subscription on a subscriber, that is not
* capable of read the messages at a speed sufficient to unblock the processing.
*/
BEST_EFFORT,
}
}