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 redisson-all Show documentation
Show all versions of redisson-all Show documentation
Easy Redis Java client and Real-Time Data Platform. Valkey compatible. Sync/Async/RxJava3/Reactive API. Client side caching. Over 50 Redis based Java objects and services: JCache API, Apache Tomcat, Hibernate, Spring, Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Scheduler, RPC
/*
* 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