io.reactivex.flowable.internal.operators.FlowableReplay Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rxjava3-flowable Show documentation
Show all versions of rxjava3-flowable Show documentation
rxjava3-flowable developed by David Karnok
The newest version!
/**
* 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.flowable.internal.operators;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import org.reactivestreams.*;
import hu.akarnokd.reactivestreams.extensions.RelaxedSubscriber;
import io.reactivex.common.*;
import io.reactivex.common.exceptions.Exceptions;
import io.reactivex.common.functions.*;
import io.reactivex.common.internal.functions.ObjectHelper;
import io.reactivex.common.internal.utils.ExceptionHelper;
import io.reactivex.flowable.*;
import io.reactivex.flowable.extensions.HasUpstreamPublisher;
import io.reactivex.flowable.internal.subscribers.SubscriberResourceWrapper;
import io.reactivex.flowable.internal.subscriptions.*;
import io.reactivex.flowable.internal.utils.*;
public final class FlowableReplay extends ConnectableFlowable implements HasUpstreamPublisher, Disposable {
/** The source observable. */
final Flowable source;
/** Holds the current subscriber that is, will be or just was subscribed to the source observable. */
final AtomicReference> current;
/** A factory that creates the appropriate buffer for the ReplaySubscriber. */
final Callable extends ReplayBuffer> bufferFactory;
final Publisher onSubscribe;
@SuppressWarnings("rawtypes")
static final Callable DEFAULT_UNBOUNDED_FACTORY = new DefaultUnboundedFactory();
/**
* Given a connectable observable factory, it multicasts over the generated
* ConnectableObservable via a selector function.
* @param the connectable observable type
* @param the result type
* @param connectableFactory the factory that returns a ConnectableFlowable for each individual subscriber
* @param selector the function that receives a Flowable and should return another Flowable that will be subscribed to
* @return the new Observable instance
*/
public static Flowable multicastSelector(
final Callable extends ConnectableFlowable> connectableFactory,
final Function super Flowable, ? extends Publisher> selector) {
return Flowable.unsafeCreate(new MultiCastPublisher(connectableFactory, selector));
}
/**
* Child Subscribers will observe the events of the ConnectableObservable on the
* specified scheduler.
* @param the value type
* @param co the ConnectableFlowable to wrap
* @param scheduler the target scheduler
* @return the new ConnectableObservable instance
*/
public static ConnectableFlowable observeOn(final ConnectableFlowable co, final Scheduler scheduler) {
final Flowable observable = co.observeOn(scheduler);
return RxJavaFlowablePlugins.onAssembly(new ConnectableFlowableReplay(co, observable));
}
/**
* Creates a replaying ConnectableObservable with an unbounded buffer.
* @param the value type
* @param source the source Publisher to use
* @return the new ConnectableObservable instance
*/
@SuppressWarnings("unchecked")
public static ConnectableFlowable createFrom(Flowable extends T> source) {
return create(source, DEFAULT_UNBOUNDED_FACTORY);
}
/**
* Creates a replaying ConnectableObservable with a size bound buffer.
* @param the value type
* @param source the source Flowable to use
* @param bufferSize the maximum number of elements to hold
* @return the new ConnectableObservable instance
*/
public static ConnectableFlowable create(Flowable source,
final int bufferSize) {
if (bufferSize == Integer.MAX_VALUE) {
return createFrom(source);
}
return create(source, new ReplayBufferTask(bufferSize));
}
/**
* Creates a replaying ConnectableObservable with a time bound buffer.
* @param the value type
* @param source the source Flowable to use
* @param maxAge the maximum age of entries
* @param unit the unit of measure of the age amount
* @param scheduler the target scheduler providing the current time
* @return the new ConnectableObservable instance
*/
public static ConnectableFlowable create(Flowable source,
long maxAge, TimeUnit unit, Scheduler scheduler) {
return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE);
}
/**
* Creates a replaying ConnectableObservable with a size and time bound buffer.
* @param the value type
* @param source the source Flowable to use
* @param maxAge the maximum age of entries
* @param unit the unit of measure of the age amount
* @param scheduler the target scheduler providing the current time
* @param bufferSize the maximum number of elements to hold
* @return the new ConnectableFlowable instance
*/
public static ConnectableFlowable create(Flowable source,
final long maxAge, final TimeUnit unit, final Scheduler scheduler, final int bufferSize) {
return create(source, new ScheduledReplayBufferTask(bufferSize, maxAge, unit, scheduler));
}
/**
* Creates a OperatorReplay instance to replay values of the given source observable.
* @param source the source observable
* @param bufferFactory the factory to instantiate the appropriate buffer when the observable becomes active
* @return the connectable observable
*/
static ConnectableFlowable create(Flowable source,
final Callable extends ReplayBuffer> bufferFactory) {
// the current connection to source needs to be shared between the operator and its onSubscribe call
final AtomicReference> curr = new AtomicReference>();
Publisher onSubscribe = new ReplayPublisher(curr, bufferFactory);
return RxJavaFlowablePlugins.onAssembly(new FlowableReplay(onSubscribe, source, curr, bufferFactory));
}
private FlowableReplay(Publisher onSubscribe, Flowable source,
final AtomicReference> current,
final Callable extends ReplayBuffer> bufferFactory) {
this.onSubscribe = onSubscribe;
this.source = source;
this.current = current;
this.bufferFactory = bufferFactory;
}
@Override
public Publisher source() {
return source;
}
@Override
protected void subscribeActual(Subscriber super T> s) {
onSubscribe.subscribe(s);
}
@Override
public void dispose() {
current.lazySet(null);
}
@Override
public boolean isDisposed() {
Disposable d = current.get();
return d == null || d.isDisposed();
}
@Override
public void connect(Consumer super Disposable> connection) {
boolean doConnect;
ReplaySubscriber ps;
// we loop because concurrent connect/disconnect and termination may change the state
for (;;) {
// retrieve the current subscriber-to-source instance
ps = current.get();
// if there is none yet or the current was disposed
if (ps == null || ps.isDisposed()) {
ReplayBuffer buf;
try {
buf = bufferFactory.call();
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
throw ExceptionHelper.wrapOrThrow(ex);
}
// create a new subscriber-to-source
ReplaySubscriber u = new ReplaySubscriber(buf);
// try setting it as the current subscriber-to-source
if (!current.compareAndSet(ps, u)) {
// did not work, perhaps a new subscriber arrived
// and created a new subscriber-to-source as well, retry
continue;
}
ps = u;
}
// if connect() was called concurrently, only one of them should actually
// connect to the source
doConnect = !ps.shouldConnect.get() && ps.shouldConnect.compareAndSet(false, true);
break; // NOPMD
}
/*
* Notify the callback that we have a (new) connection which it can dispose
* but since ps is unique to a connection, multiple calls to connect() will return the
* same Subscription and even if there was a connect-disconnect-connect pair, the older
* references won't disconnect the newer connection.
* Synchronous source consumers have the opportunity to disconnect via dispose on the
* Disposable as unsafeSubscribe may never return in its own.
*
* Note however, that asynchronously disconnecting a running source might leave
* child-subscribers without any terminal event; ReplaySubject does not have this
* issue because the cancellation was always triggered by the child-subscribers
* themselves.
*/
try {
connection.accept(ps);
} catch (Throwable ex) {
if (doConnect) {
ps.shouldConnect.compareAndSet(true, false);
}
Exceptions.throwIfFatal(ex);
throw ExceptionHelper.wrapOrThrow(ex);
}
if (doConnect) {
source.subscribe(ps);
}
}
@SuppressWarnings("rawtypes")
static final class ReplaySubscriber
extends AtomicReference
implements RelaxedSubscriber, Disposable {
private static final long serialVersionUID = 7224554242710036740L;
/** Holds notifications from upstream. */
final ReplayBuffer buffer;
/** Indicates this Subscriber received a terminal event. */
boolean done;
/** Indicates an empty array of inner subscriptions. */
static final InnerSubscription[] EMPTY = new InnerSubscription[0];
/** Indicates a terminated ReplaySubscriber. */
static final InnerSubscription[] TERMINATED = new InnerSubscription[0];
/** Tracks the subscribed InnerSubscriptions. */
final AtomicReference[]> subscribers;
/**
* Atomically changed from false to true by connect to make sure the
* connection is only performed by one thread.
*/
final AtomicBoolean shouldConnect;
final AtomicInteger management;
/** Contains the maximum element index the child Subscribers requested so far. Accessed while emitting is true. */
long maxChildRequested;
/** Counts the outstanding upstream requests until the producer arrives. */
long maxUpstreamRequested;
@SuppressWarnings("unchecked")
ReplaySubscriber(ReplayBuffer buffer) {
this.buffer = buffer;
this.management = new AtomicInteger();
this.subscribers = new AtomicReference[]>(EMPTY);
this.shouldConnect = new AtomicBoolean();
}
@Override
public boolean isDisposed() {
return subscribers.get() == TERMINATED;
}
@SuppressWarnings("unchecked")
@Override
public void dispose() {
subscribers.set(TERMINATED);
// unlike OperatorPublish, we can't null out the terminated so
// late subscribers can still get replay
// current.compareAndSet(ReplaySubscriber.this, null);
// we don't care if it fails because it means the current has
// been replaced in the meantime
SubscriptionHelper.cancel(this);
}
/**
* Atomically try adding a new InnerSubscription to this Subscriber or return false if this
* Subscriber was terminated.
* @param producer the producer to add
* @return true if succeeded, false otherwise
*/
@SuppressWarnings("unchecked")
boolean add(InnerSubscription producer) {
if (producer == null) {
throw new NullPointerException();
}
// the state can change so we do a CAS loop to achieve atomicity
for (;;) {
// get the current producer array
InnerSubscription[] c = subscribers.get();
// if this subscriber-to-source reached a terminal state by receiving
// an onError or onComplete, just refuse to add the new producer
if (c == TERMINATED) {
return false;
}
// we perform a copy-on-write logic
int len = c.length;
InnerSubscription[] u = new InnerSubscription[len + 1];
System.arraycopy(c, 0, u, 0, len);
u[len] = producer;
// try setting the subscribers array
if (subscribers.compareAndSet(c, u)) {
return true;
}
// if failed, some other operation succeeded (another add, remove or termination)
// so retry
}
}
/**
* Atomically removes the given InnerSubscription from the subscribers array.
* @param p the InnerSubscription to remove
*/
@SuppressWarnings("unchecked")
void remove(InnerSubscription p) {
// the state can change so we do a CAS loop to achieve atomicity
for (;;) {
// let's read the current subscribers array
InnerSubscription[] c = subscribers.get();
int len = c.length;
// if it is either empty or terminated, there is nothing to remove so we quit
if (len == 0) {
return;
}
// let's find the supplied producer in the array
// although this is O(n), we don't expect too many child subscribers in general
int j = -1;
for (int i = 0; i < len; i++) {
if (c[i].equals(p)) {
j = i;
break;
}
}
// we didn't find it so just quit
if (j < 0) {
return;
}
// we do copy-on-write logic here
InnerSubscription[] u;
// we don't create a new empty array if producer was the single inhabitant
// but rather reuse an empty array
if (len == 1) {
u = EMPTY;
} else {
// otherwise, create a new array one less in size
u = new InnerSubscription[len - 1];
// copy elements being before the given producer
System.arraycopy(c, 0, u, 0, j);
// copy elements being after the given producer
System.arraycopy(c, j + 1, u, j, len - j - 1);
}
// try setting this new array as
if (subscribers.compareAndSet(c, u)) {
return;
}
// if we failed, it means something else happened
// (a concurrent add/remove or termination), we need to retry
}
}
@Override
public void onSubscribe(Subscription p) {
if (SubscriptionHelper.setOnce(this, p)) {
manageRequests();
for (InnerSubscription rp : subscribers.get()) {
buffer.replay(rp);
}
}
}
@Override
public void onNext(T t) {
if (!done) {
buffer.next(t);
for (InnerSubscription rp : subscribers.get()) {
buffer.replay(rp);
}
}
}
@SuppressWarnings("unchecked")
@Override
public void onError(Throwable e) {
// The observer front is accessed serially as required by spec so
// no need to CAS in the terminal value
if (!done) {
done = true;
buffer.error(e);
for (InnerSubscription rp : subscribers.getAndSet(TERMINATED)) {
buffer.replay(rp);
}
} else {
RxJavaCommonPlugins.onError(e);
}
}
@SuppressWarnings("unchecked")
@Override
public void onComplete() {
// The observer front is accessed serially as required by spec so
// no need to CAS in the terminal value
if (!done) {
done = true;
buffer.complete();
for (InnerSubscription rp : subscribers.getAndSet(TERMINATED)) {
buffer.replay(rp);
}
}
}
/**
* Coordinates the request amounts of various child Subscribers.
*/
void manageRequests() {
if (management.getAndIncrement() != 0) {
return;
}
int missed = 1;
for (;;) {
// if the upstream has completed, no more requesting is possible
if (isDisposed()) {
return;
}
InnerSubscription[] a = subscribers.get();
long ri = maxChildRequested;
long maxTotalRequests = ri;
for (InnerSubscription rp : a) {
maxTotalRequests = Math.max(maxTotalRequests, rp.totalRequested.get());
}
long ur = maxUpstreamRequested;
Subscription p = get();
long diff = maxTotalRequests - ri;
if (diff != 0L) {
maxChildRequested = maxTotalRequests;
if (p != null) {
if (ur != 0L) {
maxUpstreamRequested = 0L;
p.request(ur + diff);
} else {
p.request(diff);
}
} else {
// collect upstream request amounts until there is a producer for them
long u = ur + diff;
if (u < 0) {
u = Long.MAX_VALUE;
}
maxUpstreamRequested = u;
}
} else
// if there were outstanding upstream requests and we have a producer
if (ur != 0L && p != null) {
maxUpstreamRequested = 0L;
// fire the accumulated requests
p.request(ur);
}
missed = management.addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
}
/**
* A Subscription that manages the request and cancellation state of a
* child subscriber in thread-safe manner.
* @param the value type
*/
static final class InnerSubscription extends AtomicLong implements Subscription, Disposable {
private static final long serialVersionUID = -4453897557930727610L;
/**
* The parent subscriber-to-source used to allow removing the child in case of
* child cancellation.
*/
final ReplaySubscriber parent;
/** The actual child subscriber. */
final Subscriber super T> child;
/**
* Holds an object that represents the current location in the buffer.
* Guarded by the emitter loop.
*/
Object index;
/**
* Keeps the sum of all requested amounts.
*/
final AtomicLong totalRequested;
/** Indicates an emission state. Guarded by this. */
boolean emitting;
/** Indicates a missed update. Guarded by this. */
boolean missed;
/**
* Indicates this child has been cancelled: the state is swapped in atomically and
* will prevent the dispatch() to emit (too many) values to a terminated child subscriber.
*/
static final long CANCELLED = Long.MIN_VALUE;
InnerSubscription(ReplaySubscriber parent, Subscriber super T> child) {
this.parent = parent;
this.child = child;
this.totalRequested = new AtomicLong();
}
@Override
public void request(long n) {
// ignore negative requests
if (SubscriptionHelper.validate(n)) {
// In general, RxJava doesn't prevent concurrent requests (with each other or with
// a cancel) so we need a CAS-loop, but we need to handle
// request overflow and cancelled/not requested state as well.
for (;;) {
// get the current request amount
long r = get();
// if child called cancel() do nothing
if (r == CANCELLED) {
return;
}
// ignore zero requests except any first that sets in zero
if (r >= 0L && n == 0) {
return;
}
// otherwise, increase the request count
long u = BackpressureHelper.addCap(r, n);
// try setting the new request value
if (compareAndSet(r, u)) {
// increment the total request counter
BackpressureHelper.add(totalRequested, n);
// if successful, notify the parent dispatcher this child can receive more
// elements
parent.manageRequests();
parent.buffer.replay(this);
return;
}
// otherwise, someone else changed the state (perhaps a concurrent
// request or cancellation) so retry
}
}
}
/**
* Indicate that values have been emitted to this child subscriber by the dispatch() method.
* @param n the number of items emitted
* @return the updated request value (may indicate how much can be produced or a terminal state)
*/
public long produced(long n) {
return BackpressureHelper.producedCancel(this, n);
}
@Override
public boolean isDisposed() {
return get() == CANCELLED;
}
@Override
public void cancel() {
dispose();
}
@Override
public void dispose() {
if (getAndSet(CANCELLED) != CANCELLED) {
// remove this from the parent
parent.remove(this);
// After removal, we might have unblocked the other child subscribers:
// let's assume this child had 0 requested before the cancellation while
// the others had non-zero. By removing this 'blocking' child, the others
// are now free to receive events
parent.manageRequests();
}
}
/**
* Convenience method to auto-cast the index object.
* @return the current index object
*/
@SuppressWarnings("unchecked")
U index() {
return (U)index;
}
}
/**
* The interface for interacting with various buffering logic.
*
* @param the value type
*/
interface ReplayBuffer {
/**
* Adds a regular value to the buffer.
* @param value the next value to store
*/
void next(T value);
/**
* Adds a terminal exception to the buffer.
* @param e the Throwable instance
*/
void error(Throwable e);
/**
* Adds a completion event to the buffer.
*/
void complete();
/**
* Tries to replay the buffered values to the
* subscriber inside the output if there
* is new value and requests available at the
* same time.
* @param output the receiver of the events
*/
void replay(InnerSubscription output);
}
/**
* Holds an unbounded list of events.
*
* @param the value type
*/
static final class UnboundedReplayBuffer extends ArrayList