io.reactivex.rxjava3.processors.UnicastProcessor Maven / Gradle / Ivy
Show all versions of rxjava Show documentation
/*
* Copyright (c) 2016-present, RxJava Contributors.
*
* 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 io.reactivex.rxjava3.processors;
import java.util.Objects;
import java.util.concurrent.atomic.*;
import org.reactivestreams.*;
import io.reactivex.rxjava3.annotations.*;
import io.reactivex.rxjava3.internal.functions.*;
import io.reactivex.rxjava3.internal.subscriptions.*;
import io.reactivex.rxjava3.internal.util.*;
import io.reactivex.rxjava3.operators.QueueSubscription;
import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
/**
* A {@link FlowableProcessor} variant that queues up events until a single {@link Subscriber} subscribes to it, replays
* those events to it until the {@code Subscriber} catches up and then switches to relaying events live to
* this single {@code Subscriber} until this {@code UnicastProcessor} terminates or the {@code Subscriber} cancels
* its subscription.
*
*
*
* This processor does not have a public constructor by design; a new empty instance of this
* {@code UnicastProcessor} can be created via the following {@code create} methods that
* allow specifying the retention policy for items:
*
* - {@link #create()} - creates an empty, unbounded {@code UnicastProcessor} that
* caches all items and the terminal event it receives.
* - {@link #create(int)} - creates an empty, unbounded {@code UnicastProcessor}
* with a hint about how many total items one expects to retain.
* - {@link #create(boolean)} - creates an empty, unbounded {@code UnicastProcessor} that
* optionally delays an error it receives and replays it after the regular items have been emitted.
* - {@link #create(int, Runnable)} - creates an empty, unbounded {@code UnicastProcessor}
* with a hint about how many total items one expects to retain and a callback that will be
* called exactly once when the {@code UnicastProcessor} gets terminated or the single {@code Subscriber} cancels.
* - {@link #create(int, Runnable, boolean)} - creates an empty, unbounded {@code UnicastProcessor}
* with a hint about how many total items one expects to retain and a callback that will be
* called exactly once when the {@code UnicastProcessor} gets terminated or the single {@code Subscriber} cancels
* and optionally delays an error it receives and replays it after the regular items have been emitted.
*
*
* If more than one {@code Subscriber} attempts to subscribe to this Processor, they
* will receive an {@link IllegalStateException} if this {@link UnicastProcessor} hasn't terminated yet,
* or the Subscribers receive the terminal event (error or completion) if this
* Processor has terminated.
*
* The {@code UnicastProcessor} buffers notifications and replays them to the single {@code Subscriber} as requested,
* for which it holds upstream items an unbounded internal buffer until they can be emitted.
*
* Since a {@code UnicastProcessor} is a Reactive Streams {@code Processor},
* {@code null}s are not allowed (Rule 2.13) as
* parameters to {@link #onNext(Object)} and {@link #onError(Throwable)}. Such calls will result in a
* {@link NullPointerException} being thrown and the processor's state is not changed.
*
* Since a {@code UnicastProcessor} is a {@link io.reactivex.rxjava3.core.Flowable} as well as a {@link FlowableProcessor}, it
* honors the downstream backpressure but consumes an upstream source in an unbounded manner (requesting {@link Long#MAX_VALUE}).
*
* When this {@code UnicastProcessor} is terminated via {@link #onError(Throwable)} the current or late single {@code Subscriber}
* may receive the {@code Throwable} before any available items could be emitted. To make sure an {@code onError} event is delivered
* to the {@code Subscriber} after the normal items, create a {@code UnicastProcessor} with the {@link #create(boolean)} or
* {@link #create(int, Runnable, boolean)} factory methods.
*
* Even though {@code UnicastProcessor} implements the {@code Subscriber} interface, calling
* {@code onSubscribe} is not required (Rule 2.12)
* if the processor is used as a standalone source. However, calling {@code onSubscribe}
* after the {@code UnicastProcessor} reached its terminal state will result in the
* given {@code Subscription} being canceled immediately.
*
* Calling {@link #onNext(Object)}, {@link #onError(Throwable)} and {@link #onComplete()}
* is required to be serialized (called from the same thread or called non-overlappingly from different threads
* through external means of serialization). The {@link #toSerialized()} method available to all {@link FlowableProcessor}s
* provides such serialization and also protects against reentrance (i.e., when a downstream {@code Subscriber}
* consuming this processor also wants to call {@link #onNext(Object)} on this processor recursively).
*
* This {@code UnicastProcessor} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()},
* {@link #getThrowable()} and {@link #hasSubscribers()}.
*
* - Backpressure:
* - {@code UnicastProcessor} honors the downstream backpressure but consumes an upstream source
* (if any) in an unbounded manner (requesting {@link Long#MAX_VALUE}).
* - Scheduler:
* - {@code UnicastProcessor} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and
* the single {@code Subscriber} gets notified on the thread the respective {@code onXXX} methods were invoked.
* - Error handling:
* - When the {@link #onError(Throwable)} is called, the {@code UnicastProcessor} enters into a terminal state
* and emits the same {@code Throwable} instance to the current single {@code Subscriber}. During this emission,
* if the single {@code Subscriber}s cancels its respective {@code Subscription}s, the
* {@code Throwable} is delivered to the global error handler via
* {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}.
* If there were no {@code Subscriber}s subscribed to this {@code UnicastProcessor} when the {@code onError()}
* was called, the global error handler is not invoked.
*
*
*
* Example usage:
*
* UnicastProcessor<Integer> processor = UnicastProcessor.create();
*
* TestSubscriber<Integer> ts1 = processor.test();
*
* // fresh UnicastProcessors are empty
* ts1.assertEmpty();
*
* TestSubscriber<Integer> ts2 = processor.test();
*
* // A UnicastProcessor only allows one Subscriber during its lifetime
* ts2.assertFailure(IllegalStateException.class);
*
* processor.onNext(1);
* ts1.assertValue(1);
*
* processor.onNext(2);
* ts1.assertValues(1, 2);
*
* processor.onComplete();
* ts1.assertResult(1, 2);
*
* // ----------------------------------------------------
*
* UnicastProcessor<Integer> processor2 = UnicastProcessor.create();
*
* // a UnicastProcessor caches events until its single Subscriber subscribes
* processor2.onNext(1);
* processor2.onNext(2);
* processor2.onComplete();
*
* TestSubscriber<Integer> ts3 = processor2.test();
*
* // the cached events are emitted in order
* ts3.assertResult(1, 2);
*
*
* @param the value type received and emitted by this Processor subclass
* @since 2.0
*/
public final class UnicastProcessor<@NonNull T> extends FlowableProcessor {
final SpscLinkedArrayQueue queue;
final AtomicReference onTerminate;
final boolean delayError;
volatile boolean done;
Throwable error;
final AtomicReference> downstream;
volatile boolean cancelled;
final AtomicBoolean once;
final BasicIntQueueSubscription wip;
final AtomicLong requested;
boolean enableOperatorFusion;
/**
* Creates an UnicastSubject with an internal buffer capacity hint 16.
* @param the value type
* @return an UnicastSubject instance
*/
@CheckReturnValue
@NonNull
public static UnicastProcessor create() {
return new UnicastProcessor<>(bufferSize(), null, true);
}
/**
* Creates an UnicastProcessor with the given internal buffer capacity hint.
* @param the value type
* @param capacityHint the hint to size the internal unbounded buffer
* @return an UnicastProcessor instance
* @throws IllegalArgumentException if {@code capacityHint} is non-positive
*/
@CheckReturnValue
@NonNull
public static UnicastProcessor create(int capacityHint) {
ObjectHelper.verifyPositive(capacityHint, "capacityHint");
return new UnicastProcessor<>(capacityHint, null, true);
}
/**
* Creates an UnicastProcessor with default internal buffer capacity hint and delay error flag.
* History: 2.0.8 - experimental
* @param the value type
* @param delayError deliver pending onNext events before onError
* @return an UnicastProcessor instance
* @since 2.2
*/
@CheckReturnValue
@NonNull
public static UnicastProcessor create(boolean delayError) {
return new UnicastProcessor<>(bufferSize(), null, delayError);
}
/**
* Creates an UnicastProcessor with the given internal buffer capacity hint and a callback for
* the case when the single Subscriber cancels its subscription or the
* processor is terminated.
*
* The callback, if not null, is called exactly once and
* non-overlapped with any active replay.
*
* @param the value type
* @param capacityHint the hint to size the internal unbounded buffer
* @param onTerminate the non null callback
* @return an UnicastProcessor instance
* @throws NullPointerException if {@code onTerminate} is {@code null}
* @throws IllegalArgumentException if {@code capacityHint} is non-positive
*/
@CheckReturnValue
@NonNull
public static UnicastProcessor create(int capacityHint, @NonNull Runnable onTerminate) {
return create(capacityHint, onTerminate, true);
}
/**
* Creates an UnicastProcessor with the given internal buffer capacity hint, delay error flag and a callback for
* the case when the single Subscriber cancels its subscription or
* the processor is terminated.
*
* The callback, if not null, is called exactly once and
* non-overlapped with any active replay.
*
History: 2.0.8 - experimental
* @param the value type
* @param capacityHint the hint to size the internal unbounded buffer
* @param onTerminate the non null callback
* @param delayError deliver pending onNext events before onError
* @return an UnicastProcessor instance
* @throws NullPointerException if {@code onTerminate} is {@code null}
* @throws IllegalArgumentException if {@code capacityHint} is non-positive
* @since 2.2
*/
@CheckReturnValue
@NonNull
public static UnicastProcessor create(int capacityHint, @NonNull Runnable onTerminate, boolean delayError) {
Objects.requireNonNull(onTerminate, "onTerminate");
ObjectHelper.verifyPositive(capacityHint, "capacityHint");
return new UnicastProcessor<>(capacityHint, onTerminate, delayError);
}
/**
* Creates an UnicastProcessor with the given capacity hint and callback
* for when the Processor is terminated normally or its single Subscriber cancels.
* History: 2.0.8 - experimental
* @param capacityHint the capacity hint for the internal, unbounded queue
* @param onTerminate the callback to run when the Processor is terminated or cancelled, null not allowed
* @param delayError deliver pending onNext events before onError
* @since 2.2
*/
UnicastProcessor(int capacityHint, Runnable onTerminate, boolean delayError) {
this.queue = new SpscLinkedArrayQueue<>(capacityHint);
this.onTerminate = new AtomicReference<>(onTerminate);
this.delayError = delayError;
this.downstream = new AtomicReference<>();
this.once = new AtomicBoolean();
this.wip = new UnicastQueueSubscription();
this.requested = new AtomicLong();
}
void doTerminate() {
Runnable r = onTerminate.getAndSet(null);
if (r != null) {
r.run();
}
}
void drainRegular(Subscriber super T> a) {
int missed = 1;
final SpscLinkedArrayQueue q = queue;
final boolean failFast = !delayError;
for (;;) {
long r = requested.get();
long e = 0L;
while (r != e) {
boolean d = done;
T t = q.poll();
boolean empty = t == null;
if (checkTerminated(failFast, d, empty, a, q)) {
return;
}
if (empty) {
break;
}
a.onNext(t);
e++;
}
if (r == e && checkTerminated(failFast, done, q.isEmpty(), a, q)) {
return;
}
if (e != 0 && r != Long.MAX_VALUE) {
requested.addAndGet(-e);
}
missed = wip.addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
void drainFused(Subscriber super T> a) {
int missed = 1;
final SpscLinkedArrayQueue q = queue;
final boolean failFast = !delayError;
for (;;) {
if (cancelled) {
downstream.lazySet(null);
return;
}
boolean d = done;
if (failFast && d && error != null) {
q.clear();
downstream.lazySet(null);
a.onError(error);
return;
}
a.onNext(null);
if (d) {
downstream.lazySet(null);
Throwable ex = error;
if (ex != null) {
a.onError(ex);
} else {
a.onComplete();
}
return;
}
missed = wip.addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
void drain() {
if (wip.getAndIncrement() != 0) {
return;
}
int missed = 1;
Subscriber super T> a = downstream.get();
for (;;) {
if (a != null) {
if (enableOperatorFusion) {
drainFused(a);
} else {
drainRegular(a);
}
return;
}
missed = wip.addAndGet(-missed);
if (missed == 0) {
break;
}
a = downstream.get();
}
}
boolean checkTerminated(boolean failFast, boolean d, boolean empty, Subscriber super T> a, SpscLinkedArrayQueue q) {
if (cancelled) {
q.clear();
downstream.lazySet(null);
return true;
}
if (d) {
if (failFast && error != null) {
q.clear();
downstream.lazySet(null);
a.onError(error);
return true;
}
if (empty) {
Throwable e = error;
downstream.lazySet(null);
if (e != null) {
a.onError(e);
} else {
a.onComplete();
}
return true;
}
}
return false;
}
@Override
public void onSubscribe(Subscription s) {
if (done || cancelled) {
s.cancel();
} else {
s.request(Long.MAX_VALUE);
}
}
@Override
public void onNext(T t) {
ExceptionHelper.nullCheck(t, "onNext called with a null value.");
if (done || cancelled) {
return;
}
queue.offer(t);
drain();
}
@Override
public void onError(Throwable t) {
ExceptionHelper.nullCheck(t, "onError called with a null Throwable.");
if (done || cancelled) {
RxJavaPlugins.onError(t);
return;
}
error = t;
done = true;
doTerminate();
drain();
}
@Override
public void onComplete() {
if (done || cancelled) {
return;
}
done = true;
doTerminate();
drain();
}
@Override
protected void subscribeActual(Subscriber super T> s) {
if (!once.get() && once.compareAndSet(false, true)) {
s.onSubscribe(wip);
downstream.set(s);
if (cancelled) {
downstream.lazySet(null);
} else {
drain();
}
} else {
EmptySubscription.error(new IllegalStateException("This processor allows only a single Subscriber"), s);
}
}
final class UnicastQueueSubscription extends BasicIntQueueSubscription {
private static final long serialVersionUID = -4896760517184205454L;
@Nullable
@Override
public T poll() {
return queue.poll();
}
@Override
public boolean isEmpty() {
return queue.isEmpty();
}
@Override
public void clear() {
queue.clear();
}
@Override
public int requestFusion(int requestedMode) {
if ((requestedMode & QueueSubscription.ASYNC) != 0) {
enableOperatorFusion = true;
return QueueSubscription.ASYNC;
}
return QueueSubscription.NONE;
}
@Override
public void request(long n) {
if (SubscriptionHelper.validate(n)) {
BackpressureHelper.add(requested, n);
drain();
}
}
@Override
public void cancel() {
if (cancelled) {
return;
}
cancelled = true;
doTerminate();
downstream.lazySet(null);
if (wip.getAndIncrement() == 0) {
downstream.lazySet(null);
if (!enableOperatorFusion) {
queue.clear();
}
}
}
}
@Override
@CheckReturnValue
public boolean hasSubscribers() {
return downstream.get() != null;
}
@Override
@Nullable
@CheckReturnValue
public Throwable getThrowable() {
if (done) {
return error;
}
return null;
}
@Override
@CheckReturnValue
public boolean hasComplete() {
return done && error == null;
}
@Override
@CheckReturnValue
public boolean hasThrowable() {
return done && error != null;
}
}