reactor.rx.action.Action Maven / Gradle / Ivy
/*
* Copyright (c) 2011-2014 Pivotal Software, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package reactor.rx.action;
import org.reactivestreams.Processor;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.Environment;
import reactor.core.Dispatcher;
import reactor.core.alloc.Recyclable;
import reactor.core.dispatch.SynchronousDispatcher;
import reactor.core.dispatch.TailRecurseDispatcher;
import reactor.core.processor.CancelException;
import reactor.core.queue.CompletableLinkedQueue;
import reactor.core.queue.CompletableQueue;
import reactor.core.support.Exceptions;
import reactor.core.support.NonBlocking;
import reactor.core.support.SpecificationExceptions;
import reactor.fn.Consumer;
import reactor.fn.Supplier;
import reactor.fn.tuple.Tuple;
import reactor.fn.tuple.Tuple2;
import reactor.rx.Stream;
import reactor.rx.StreamUtils;
import reactor.rx.action.combination.FanInAction;
import reactor.rx.broadcast.Broadcaster;
import reactor.rx.subscription.DropSubscription;
import reactor.rx.subscription.FanOutSubscription;
import reactor.rx.subscription.PushSubscription;
import reactor.rx.subscription.ReactiveSubscription;
/**
* An Action is a reactive component to subscribe to a {@link org.reactivestreams.Publisher} and in particular
* to a {@link reactor.rx.Stream}. Stream is usually the place where actions are created.
*
* An Action is also a data producer, and therefore implements {@link org.reactivestreams.Processor}.
* An imperative programming equivalent of an action is a method or function. The main difference is that it also
* reacts on various {@link org.reactivestreams.Subscriber} signals and produce an output data {@param O} for
* any downstream subscription.
*
* The implementation specifics of an Action lies in two core features:
* - Its signal scheduler on {@link reactor.core.Dispatcher}
* - Its smart capacity awareness to prevent {@link reactor.core.Dispatcher} overflow
*
* Up to a maximum capacity defined with {@link this#capacity(long)} will be allowed to be dispatched by requesting
* the tracked remaining slots to the upstream {@link org.reactivestreams.Subscription}. This maximum in-flight data
* is a value to tune accordingly with the system and the requirements. An Action will bypass this feature anytime it is
* not the root of stream processing chain e.g.:
*
* stream.filter(..).map(..) :
*
* In that Stream, filter is a FilterAction and has no upstream action, only the publisher it is attached to.
* The FilterAction will decide to be capacity aware and will track demand.
* The MapAction will however behave like a firehose and will not track the demand, passing any request upstream.
*
* Implementing an Action is highly recommended to work with Stream without dealing with tracking issues and other
* threading matters. Usually an implementation will override any doXXXXX method where 'do' is an hint that logic will
* safely be dispatched to avoid race-conditions.
*
* @param The input {@link this#onNext(Object)} signal
* @param The output type to listen for with {@link this#subscribe(org.reactivestreams.Subscriber)}
* @author Stephane Maldini
* @since 1.1, 2.0
*/
public abstract class Action extends Stream
implements Processor, Consumer, Recyclable, Control {
/**
* onComplete, onError, request, onSubscribe are dispatched events, therefore up to capacity + 4 events can be
* in-flight
* stacking into a Dispatcher.
*/
public static final int RESERVED_SLOTS = 4;
public static final int NO_CAPACITY = -1;
/**
* The upstream request tracker to avoid dispatcher overrun, based on the current {@link this#capacity}
*/
protected PushSubscription upstreamSubscription;
protected PushSubscription downstreamSubscription;
protected long capacity;
public static void checkRequest(long n) {
if (n <= 0l) {
throw SpecificationExceptions.spec_3_09_exception(n);
}
}
public static long evaluateCapacity(long n) {
return n != Long.MAX_VALUE ?
Math.max(Action.RESERVED_SLOTS, n - Action.RESERVED_SLOTS) :
Long.MAX_VALUE;
}
public Action() {
this(Long.MAX_VALUE);
}
public Action(long batchSize) {
this.capacity = batchSize;
}
/**
* --------------------------------------------------------------------------------------------------------
* ACTION SIGNAL HANDLING
* --------------------------------------------------------------------------------------------------------
*/
@Override
public void subscribe(final Subscriber super O> subscriber) {
try {
final NonBlocking asyncSubscriber = NonBlocking.class.isAssignableFrom(subscriber.getClass()) ?
(NonBlocking) subscriber :
null;
boolean isReactiveCapacity = null == asyncSubscriber || asyncSubscriber.isReactivePull(getDispatcher(),
capacity);
final PushSubscription subscription = createSubscription(subscriber,
isReactiveCapacity);
if (subscription == null)
return;
if (null != asyncSubscriber && isReactiveCapacity) {
subscription.maxCapacity(asyncSubscriber.getCapacity());
}
subscribeWithSubscription(subscriber, subscription);
}catch (Throwable throwable){
Exceptions.throwIfFatal(throwable);
subscriber.onError(throwable);
}
}
@Override
public void onSubscribe(Subscription subscription) {
if (subscription == null) {
throw new NullPointerException("Spec 2.13: Subscription cannot be null");
}
final boolean hasRequestTracker = upstreamSubscription != null;
//if request tracker was connected to another subscription
if (hasRequestTracker) {
subscription.cancel();
return;
}
upstreamSubscription = createTrackingSubscription(subscription);
upstreamSubscription.maxCapacity(getCapacity());
try {
doOnSubscribe(subscription);
doStart();
} catch (Throwable t) {
Exceptions.throwIfFatal(t);
doError(t);
}
}
protected final void doStart() {
final PushSubscription downSub = downstreamSubscription;
if (downSub != null) {
downSub.start();
}
}
@Override
public final void accept(I i) {
onNext(i);
}
@Override
public void onNext(I ev) {
if (ev == null) {
throw new NullPointerException("Spec 2.13: Signal cannot be null");
}
if (upstreamSubscription == null && downstreamSubscription == null) {
throw CancelException.get();
}
try {
doNext(ev);
} catch (CancelException uae){
throw uae;
} catch (Throwable cause) {
doError(Exceptions.addValueAsLastCause(cause, ev));
}
}
@Override
public void onComplete() {
try {
doComplete();
doShutdown();
} catch (Throwable t) {
doError(t);
}
}
@Override
public void onError(Throwable cause) {
if (cause == null) {
throw new NullPointerException("Spec 2.13: Signal cannot be null");
}
if (upstreamSubscription != null) upstreamSubscription.updatePendingRequests(0l);
doError(cause);
doShutdown();
}
/**
* --------------------------------------------------------------------------------------------------------
* ACTION MODIFIERS
* --------------------------------------------------------------------------------------------------------
*/
@Override
public Action capacity(long elements) {
Dispatcher dispatcher = getDispatcher();
if (dispatcher != SynchronousDispatcher.INSTANCE && dispatcher.getClass() != TailRecurseDispatcher.class) {
long dispatcherCapacity = evaluateCapacity(dispatcher.backlogSize());
capacity = elements > dispatcherCapacity ? dispatcherCapacity : elements;
} else {
capacity = elements;
}
if (upstreamSubscription != null) {
upstreamSubscription.maxCapacity(capacity);
}
return this;
}
/**
* Send an element of parameterized type {link O} to all the attached {@link Subscriber}.
* A Stream must be in READY state to dispatch signals and will fail fast otherwise (IllegalStateException).
*
* @param ev the data to forward
* @since 2.0
*/
protected void broadcastNext(final O ev) {
//log.debug("event [" + ev + "] by: " + getClass().getSimpleName());
PushSubscription downstreamSubscription = this.downstreamSubscription;
if (downstreamSubscription == null) {
throw CancelException.get();
}
try {
downstreamSubscription.onNext(ev);
} catch(CancelException ce){
throw ce;
} catch (Throwable throwable) {
doError(Exceptions.addValueAsLastCause(throwable, ev));
}
}
/**
* Send an error to all the attached {@link Subscriber}.
* A Stream must be in READY state to dispatch signals and will fail fast otherwise (IllegalStateException).
*
* @param throwable the error to forward
* @since 2.0
*/
protected void broadcastError(final Throwable throwable) {
//log.debug("event [" + throwable + "] by: " + getClass().getSimpleName());
/*if (!isRunning()) {
if (log.isTraceEnabled()) {
log.trace("error dropped by: " + getClass().getSimpleName() + ":" + this, throwable);
}
}*/
if (downstreamSubscription == null) {
if (Environment.alive()) {
Environment.get().routeError(throwable);
}
return;
}
downstreamSubscription.onError(throwable);
}
/**
* Send a complete event to all the attached {@link Subscriber} ONLY IF the underlying state is READY.
* Unlike {@link #broadcastNext(Object)} and {@link #broadcastError(Throwable)} it will simply ignore the signal.
*
* @since 2.0
*/
protected void broadcastComplete() {
//log.debug("event [complete] by: " + getClass().getSimpleName());
if (downstreamSubscription == null) {
return;
}
try {
downstreamSubscription.onComplete();
} catch (Throwable throwable) {
doError(throwable);
}
}
@Override
public boolean isPublishing() {
PushSubscription parentSubscription = upstreamSubscription;
return parentSubscription != null && !parentSubscription.isComplete();
}
public void cancel() {
PushSubscription parentSub = upstreamSubscription;
if (parentSub != null) {
upstreamSubscription = null;
parentSub.cancel();
}
}
@Override
public void start() {
if (downstreamSubscription == null) {
requestMore(Long.MAX_VALUE);
}
}
/**
* Print a debugged form of the root action relative to this one. The output will be an acyclic directed graph of
* composed actions.
*
* @since 2.0
*/
@SuppressWarnings("unchecked")
public StreamUtils.StreamVisitor debug() {
return StreamUtils.browse(findOldestUpstream(Action.class));
}
/**
* --------------------------------------------------------------------------------------------------------
* STREAM ACTION-SPECIFIC EXTENSIONS
* --------------------------------------------------------------------------------------------------------
*/
/**
* Consume a Stream to allow for dynamic {@link Action} update. Everytime
* the {@param controlStream} receives a next signal, the current Action and the input data will be published as a
* {@link reactor.fn.tuple.Tuple2} to the attached {@param controller}.
*
* This is particulary useful to dynamically adapt the {@link Stream} instance : capacity(), pause(), resume()...
*
* @param controlStream The consumed stream, each signal will trigger the passed controller
* @param controller The consumer accepting a pair of Stream and user-provided signal type
* @return the current {@link Stream} instance
* @since 2.0
*/
public final Action control(Stream controlStream, final Consumer,
? super E>> controller) {
final Action thiz = this;
controlStream.consume(new Consumer() {
@Override
public void accept(E e) {
controller.accept(Tuple.of(thiz, e));
}
});
return this;
}
@Override
public final Stream onOverflowBuffer(final Supplier extends CompletableQueue> queueSupplier) {
return lift(new Supplier>() {
@Override
public Action get() {
Broadcaster newStream = Broadcaster.create(getEnvironment(), getDispatcher()).capacity(capacity);
if (queueSupplier == null) {
subscribeWithSubscription(newStream, new DropSubscription(Action.this, newStream) {
@Override
public void request(long elements) {
super.request(elements);
requestUpstream(capacity, isComplete(), elements);
}
});
} else {
subscribeWithSubscription(newStream,
createSubscription(newStream, queueSupplier.get()));
}
return newStream;
}
});
}
@SuppressWarnings("unchecked")
@Override
public final CompositeAction combine() {
final Action subscriber = (Action) findOldestUpstream(Action.class);
subscriber.upstreamSubscription = null;
return new CompositeAction(subscriber, this);
}
/**
* Create a consumer that broadcast complete signal from any accepted value.
*
* @return a new {@link Consumer} ready to forward complete signal to this stream
* @since 2.0
*/
public final Consumer> toBroadcastCompleteConsumer() {
return new Consumer