rx.internal.operators.OnSubscribePublishMulticast Maven / Gradle / Ivy
/**
* Copyright 2016 Netflix, 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 rx.internal.operators;
import java.util.Queue;
import java.util.concurrent.atomic.*;
import rx.*;
import rx.exceptions.MissingBackpressureException;
import rx.internal.util.atomic.SpscAtomicArrayQueue;
import rx.internal.util.unsafe.*;
/**
* Multicasts notifications coming through its input Subscriber view to its
* client Subscribers via lockstep backpressure mode.
*
* The difference between this class and OperatorPublish is that this
* class doesn't consume the upstream if there are no child subscribers but
* waits for them to show up. Plus if the upstream source terminates, late
* subscribers will be immediately terminated with the same terminal event
* unlike OperatorPublish which just waits for the next connection.
*
*
The class extends AtomicInteger which is the work-in-progress gate
* for the drain-loop serializing subscriptions and child request changes.
*
* @param the input and output type
*/
public final class OnSubscribePublishMulticast extends AtomicInteger
implements Observable.OnSubscribe, Observer, Subscription {
/** */
private static final long serialVersionUID = -3741892510772238743L;
/**
* The prefetch queue holding onto a fixed amount of items until all
* all child subscribers have requested something.
*/
final Queue queue;
/**
* The number of items to prefetch from the upstreams source.
*/
final int prefetch;
/**
* Delays the error delivery to happen only after all values have been consumed.
*/
final boolean delayError;
/**
* The subscriber that can be 'connected' to the upstream source.
*/
final ParentSubscriber parent;
/** Indicates the upstream has completed. */
volatile boolean done;
/**
* Holds onto the upstream's exception if done is true and this field is non-null.
* This field must be read after done or if subscribers == TERMINATED to
* establish a proper happens-before.
*/
Throwable error;
/**
* Holds the upstream producer if any, set through the parent subscriber.
*/
volatile Producer producer;
/**
* A copy-on-write array of currently subscribed child subscribers' wrapper structure.
*/
volatile PublishProducer[] subscribers;
/**
* Represents an empty array of subscriber wrapper,
* helps avoid allocating an empty array all the time.
*/
static final PublishProducer>[] EMPTY = new PublishProducer[0];
/**
* Represents a final state for this class that prevents new subscribers
* from subscribing to it.
*/
static final PublishProducer>[] TERMINATED = new PublishProducer[0];
/**
* Constructor, initializes the fields
* @param prefetch the prefetch amount, > 0 required
* @param delayError delay the error delivery after the normal items?
* @throws IllegalArgumentException if prefetch <= 0
*/
@SuppressWarnings("unchecked")
public OnSubscribePublishMulticast(int prefetch, boolean delayError) {
if (prefetch <= 0) {
throw new IllegalArgumentException("prefetch > 0 required but it was " + prefetch);
}
this.prefetch = prefetch;
this.delayError = delayError;
if (UnsafeAccess.isUnsafeAvailable()) {
this.queue = new SpscArrayQueue(prefetch);
} else {
this.queue = new SpscAtomicArrayQueue(prefetch);
}
this.subscribers = (PublishProducer[]) EMPTY;
this.parent = new ParentSubscriber(this);
}
@Override
public void call(Subscriber super T> t) {
PublishProducer pp = new PublishProducer(t, this);
t.add(pp);
t.setProducer(pp);
if (add(pp)) {
if (pp.isUnsubscribed()) {
remove(pp);
} else {
drain();
}
} else {
Throwable e = error;
if (e != null) {
t.onError(e);
} else {
t.onCompleted();
}
}
}
@Override
public void onNext(T t) {
if (!queue.offer(t)) {
parent.unsubscribe();
error = new MissingBackpressureException("Queue full?!");
done = true;
}
drain();
}
@Override
public void onError(Throwable e) {
error = e;
done = true;
drain();
}
@Override
public void onCompleted() {
done = true;
drain();
}
/**
* Sets the main producer and issues the prefetch amount.
* @param p the producer to set
*/
void setProducer(Producer p) {
this.producer = p;
p.request(prefetch);
}
/**
* The serialization loop that determines the minimum request of
* all subscribers and tries to emit as many items from the queue if
* they are available.
*
* The execution of the drain-loop is guaranteed to be thread-safe.
*/
void drain() {
if (getAndIncrement() != 0) {
return;
}
final Queue q = queue;
int missed = 0;
for (;;) {
long r = Long.MAX_VALUE;
PublishProducer[] a = subscribers;
int n = a.length;
for (PublishProducer inner : a) {
r = Math.min(r, inner.get());
}
if (n != 0) {
long e = 0L;
while (e != r) {
boolean d = done;
T v = q.poll();
boolean empty = v == null;
if (checkTerminated(d, empty)) {
return;
}
if (empty) {
break;
}
for (PublishProducer inner : a) {
inner.actual.onNext(v);
}
e++;
}
if (e == r) {
if (checkTerminated(done, q.isEmpty())) {
return;
}
}
if (e != 0L) {
Producer p = producer;
if (p != null) {
p.request(e);
}
for (PublishProducer inner : a) {
BackpressureUtils.produced(inner, e);
}
}
}
missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
/**
* Given the current source state, terminates all child subscribers.
* @param d the source-done indicator
* @param empty the queue-emptiness indicator
* @return true if the class reached its terminal state
*/
boolean checkTerminated(boolean d, boolean empty) {
if (d) {
if (delayError) {
if (empty) {
PublishProducer[] a = terminate();
Throwable ex = error;
if (ex != null) {
for (PublishProducer inner : a) {
inner.actual.onError(ex);
}
} else {
for (PublishProducer inner : a) {
inner.actual.onCompleted();
}
}
return true;
}
} else {
Throwable ex = error;
if (ex != null) {
queue.clear();
PublishProducer[] a = terminate();
for (PublishProducer inner : a) {
inner.actual.onError(ex);
}
return true;
} else
if (empty) {
PublishProducer[] a = terminate();
for (PublishProducer inner : a) {
inner.actual.onCompleted();
}
return true;
}
}
}
return false;
}
/**
* Atomically swaps in the terminated state.
* @return the last set of subscribers before the state change or an empty array
*/
@SuppressWarnings("unchecked")
PublishProducer[] terminate() {
PublishProducer[] a = subscribers;
if (a != TERMINATED) {
synchronized (this) {
a = subscribers;
if (a != TERMINATED) {
subscribers = (PublishProducer[]) TERMINATED;
}
}
}
return a;
}
/**
* Atomically adds the given wrapper of a child Subscriber to the subscribers array.
* @param inner the wrapper
* @return true if successful, false if the terminal state has been reached in the meantime
*/
boolean add(PublishProducer inner) {
PublishProducer[] a = subscribers;
if (a == TERMINATED) {
return false;
}
synchronized (this) {
a = subscribers;
if (a == TERMINATED) {
return false;
}
int n = a.length;
@SuppressWarnings("unchecked")
PublishProducer[] b = new PublishProducer[n + 1];
System.arraycopy(a, 0, b, 0, n);
b[n] = inner;
subscribers = b;
return true;
}
}
/**
* Atomically removes the given wrapper, if present, from the subscribers array.
* @param inner the wrapper to remove
*/
@SuppressWarnings("unchecked")
void remove(PublishProducer inner) {
PublishProducer[] a = subscribers;
if (a == TERMINATED || a == EMPTY) {
return;
}
synchronized (this) {
a = subscribers;
if (a == TERMINATED || a == EMPTY) {
return;
}
int j = -1;
int n = a.length;
for (int i = 0; i < n; i++) {
if (a[i] == inner) {
j = i;
break;
}
}
if (j < 0) {
return;
}
PublishProducer[] b;
if (n == 1) {
b = (PublishProducer[])EMPTY;
} else {
b = new PublishProducer[n - 1];
System.arraycopy(a, 0, b, 0, j);
System.arraycopy(a, j + 1, b, j, n - j - 1);
}
subscribers = b;
}
}
/**
* The subscriber that must be used for subscribing to the upstream source.
* @param the input value type;
*/
static final class ParentSubscriber extends Subscriber {
/** The reference to the parent state where the events are forwarded to. */
final OnSubscribePublishMulticast state;
public ParentSubscriber(OnSubscribePublishMulticast state) {
super();
this.state = state;
}
@Override
public void onNext(T t) {
state.onNext(t);
}
@Override
public void onError(Throwable e) {
state.onError(e);
}
@Override
public void onCompleted() {
state.onCompleted();
}
@Override
public void setProducer(Producer p) {
state.setProducer(p);
}
}
/**
* Returns the input subscriber of this class that must be subscribed
* to the upstream source.
* @return the subscriber instance
*/
public Subscriber subscriber() {
return parent;
}
@Override
public void unsubscribe() {
parent.unsubscribe();
}
@Override
public boolean isUnsubscribed() {
return parent.isUnsubscribed();
}
/**
* A Producer and Subscription that wraps a child Subscriber and manages
* its backpressure requests along with its unsubscription from the parent
* class.
*
* The class extends AtomicLong that holds onto the child's requested amount.
*
* @param the output value type
*/
static final class PublishProducer
extends AtomicLong
implements Producer, Subscription {
/** */
private static final long serialVersionUID = 960704844171597367L;
/** The actual subscriber to receive the events. */
final Subscriber super T> actual;
/** The parent object to request draining or removal. */
final OnSubscribePublishMulticast parent;
/** Makes sure unsubscription happens only once. */
final AtomicBoolean once;
public PublishProducer(Subscriber super T> actual, OnSubscribePublishMulticast parent) {
this.actual = actual;
this.parent = parent;
this.once = new AtomicBoolean();
}
@Override
public void request(long n) {
if (n < 0) {
throw new IllegalArgumentException("n >= 0 required but it was " + n);
} else
if (n != 0) {
BackpressureUtils.getAndAddRequest(this, n);
parent.drain();
}
}
@Override
public boolean isUnsubscribed() {
return once.get();
}
@Override
public void unsubscribe() {
if (once.compareAndSet(false, true)) {
parent.remove(this);
}
}
}
}