io.reactivex.rxjava3.internal.operators.flowable.FlowableReplay Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rxjava Show documentation
Show all versions of rxjava Show documentation
Reactive Extensions for Java
/*
* 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.internal.operators.flowable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.*;
import org.reactivestreams.*;
import io.reactivex.rxjava3.core.*;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.exceptions.Exceptions;
import io.reactivex.rxjava3.flowables.ConnectableFlowable;
import io.reactivex.rxjava3.functions.*;
import io.reactivex.rxjava3.internal.fuseable.HasUpstreamPublisher;
import io.reactivex.rxjava3.internal.subscribers.SubscriberResourceWrapper;
import io.reactivex.rxjava3.internal.subscriptions.*;
import io.reactivex.rxjava3.internal.util.*;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import io.reactivex.rxjava3.schedulers.Timed;
public final class FlowableReplay extends ConnectableFlowable implements HasUpstreamPublisher {
/** 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 Supplier extends ReplayBuffer> bufferFactory;
final Publisher onSubscribe;
@SuppressWarnings("rawtypes")
static final Supplier 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 Supplier extends ConnectableFlowable> connectableFactory,
final Function super Flowable, ? extends Publisher> selector) {
return new MulticastFlowable<>(connectableFactory, selector);
}
/**
* 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
* @param eagerTruncate if true, the head reference is refreshed to avoid unwanted item retention
* @return the new ConnectableObservable instance
*/
public static ConnectableFlowable create(Flowable source,
final int bufferSize, boolean eagerTruncate) {
if (bufferSize == Integer.MAX_VALUE) {
return createFrom(source);
}
return create(source, new ReplayBufferSupplier<>(bufferSize, eagerTruncate));
}
/**
* 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
* @param eagerTruncate if true, the head reference is refreshed to avoid unwanted item retention
* @return the new ConnectableObservable instance
*/
public static ConnectableFlowable create(Flowable source,
long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) {
return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE, eagerTruncate);
}
/**
* 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
* @param eagerTruncate if true, the head reference is refreshed to avoid unwanted item retention
* @return the new ConnectableFlowable instance
*/
public static ConnectableFlowable create(Flowable source,
final long maxAge, final TimeUnit unit, final Scheduler scheduler, final int bufferSize, boolean eagerTruncate) {
return create(source, new ScheduledReplayBufferSupplier<>(bufferSize, maxAge, unit, scheduler, eagerTruncate));
}
/**
* Creates a OperatorReplay instance to replay values of the given source {@code Flowable}.
* @param the value type
* @param source the source {@code Flowable} to use
* @param bufferFactory the factory to instantiate the appropriate buffer when the {@code Flowable} becomes active
* @return the {@code ConnectableFlowable} instance
*/
static ConnectableFlowable create(Flowable source,
final Supplier 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 RxJavaPlugins.onAssembly(new FlowableReplay<>(onSubscribe, source, curr, bufferFactory));
}
private FlowableReplay(Publisher onSubscribe, Flowable source,
final AtomicReference> current,
final Supplier 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 reset() {
ReplaySubscriber conn = current.get();
if (conn != null && conn.isDisposed()) {
current.compareAndSet(conn, null);
}
}
@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.get();
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
throw ExceptionHelper.wrapOrThrow(ex);
}
// create a new subscriber-to-source
ReplaySubscriber u = new ReplaySubscriber<>(buf, current);
// 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) {
Exceptions.throwIfFatal(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 FlowableSubscriber, 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;
/** Tracks the amount already requested from the upstream. */
long requestedFromUpstream;
/** The current connection. */
final AtomicReference> current;
@SuppressWarnings("unchecked")
ReplaySubscriber(ReplayBuffer buffer, AtomicReference> current) {
this.buffer = buffer;
this.current = current;
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);
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) {
// 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 {
RxJavaPlugins.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() {
AtomicInteger m = management;
if (m.getAndIncrement() != 0) {
return;
}
int missed = 1;
for (;;) {
// if the upstream has completed, no more requesting is possible
if (isDisposed()) {
return;
}
Subscription p = get();
// only request when there is an upstream Subscription available
if (p != null) {
// how many items were requested so far
long alreadyRequested = requestedFromUpstream;
long downstreamMaxRequest = alreadyRequested;
// find out the maximum total requested of the current subscribers
for (InnerSubscription rp : subscribers.get()) {
downstreamMaxRequest = Math.max(downstreamMaxRequest, rp.totalRequested.get());
}
// how much more to request from the upstream
long diff = downstreamMaxRequest - alreadyRequested;
if (diff != 0L) {
// save the new maximum requested
requestedFromUpstream = downstreamMaxRequest;
p.request(diff);
}
}
missed = m.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)) {
// add to the current requested and cap it at MAX_VALUE
// except when there was a concurrent cancellation
if (BackpressureHelper.addCancel(this, n) != CANCELLED) {
// increment the total request counter
BackpressureHelper.add(totalRequested, n);
// if successful, notify the parent dispatcher this child can receive more
// elements
parent.manageRequests();
// try replaying any cached content
parent.buffer.replay(this);
}
}
}
/**
* 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();
// make sure the last known node is not retained
index = null;
}
}
/**
* Convenience method to auto-cast the index object.
* @param type to cast 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
© 2015 - 2024 Weber Informatics LLC | Privacy Policy