io.reactivex.internal.operators.flowable.FlowableCache Maven / Gradle / Ivy
/**
* 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.internal.operators.flowable;
import java.util.concurrent.atomic.*;
import org.reactivestreams.*;
import io.reactivex.*;
import io.reactivex.internal.subscriptions.SubscriptionHelper;
import io.reactivex.internal.util.*;
import io.reactivex.plugins.RxJavaPlugins;
/**
* An observable which auto-connects to another observable, caches the elements
* from that observable but allows terminating the connection and completing the cache.
*
* @param the source element type
*/
public final class FlowableCache extends AbstractFlowableWithUpstream {
/** The cache and replay state. */
final CacheState state;
final AtomicBoolean once;
/**
* Private constructor because state needs to be shared between the Observable body and
* the onSubscribe function.
* @param source the upstream source whose signals to cache
* @param capacityHint the capacity hint
*/
public FlowableCache(Flowable source, int capacityHint) {
super(source);
this.state = new CacheState(source, capacityHint);
this.once = new AtomicBoolean();
}
@Override
protected void subscribeActual(Subscriber super T> t) {
// we can connect first because we replay everything anyway
ReplaySubscription rp = new ReplaySubscription(t, state);
t.onSubscribe(rp);
boolean doReplay = true;
if (state.addChild(rp)) {
if (rp.requested.get() == ReplaySubscription.CANCELLED) {
state.removeChild(rp);
doReplay = false;
}
}
// we ensure a single connection here to save an instance field of AtomicBoolean in state.
if (!once.get() && once.compareAndSet(false, true)) {
state.connect();
}
if (doReplay) {
rp.replay();
}
}
/**
* Check if this cached observable is connected to its source.
* @return true if already connected
*/
/* public */boolean isConnected() {
return state.isConnected;
}
/**
* Returns true if there are observers subscribed to this observable.
* @return true if the cache has Subscribers
*/
/* public */ boolean hasSubscribers() {
return state.subscribers.get().length != 0;
}
/**
* Returns the number of events currently cached.
* @return the number of currently cached event count
*/
/* public */ int cachedEventCount() {
return state.size();
}
/**
* Contains the active child subscribers and the values to replay.
*
* @param the value type of the cached items
*/
static final class CacheState extends LinkedArrayList implements FlowableSubscriber {
/** The source observable to connect to. */
final Flowable source;
/** Holds onto the subscriber connected to source. */
final AtomicReference connection = new AtomicReference();
/** Guarded by connection (not this). */
final AtomicReference[]> subscribers;
/** The default empty array of subscribers. */
@SuppressWarnings("rawtypes")
static final ReplaySubscription[] EMPTY = new ReplaySubscription[0];
/** The default empty array of subscribers. */
@SuppressWarnings("rawtypes")
static final ReplaySubscription[] TERMINATED = new ReplaySubscription[0];
/** Set to true after connection. */
volatile boolean isConnected;
/**
* Indicates that the source has completed emitting values or the
* Observable was forcefully terminated.
*/
boolean sourceDone;
@SuppressWarnings("unchecked")
CacheState(Flowable source, int capacityHint) {
super(capacityHint);
this.source = source;
this.subscribers = new AtomicReference[]>(EMPTY);
}
/**
* Adds a ReplaySubscription to the subscribers array atomically.
* @param p the target ReplaySubscription wrapping a downstream Subscriber with state
* @return true if the ReplaySubscription was added or false if the cache is already terminated
*/
public boolean addChild(ReplaySubscription p) {
// guarding by connection to save on allocating another object
// thus there are two distinct locks guarding the value-addition and child come-and-go
for (;;) {
ReplaySubscription[] a = subscribers.get();
if (a == TERMINATED) {
return false;
}
int n = a.length;
@SuppressWarnings("unchecked")
ReplaySubscription[] b = new ReplaySubscription[n + 1];
System.arraycopy(a, 0, b, 0, n);
b[n] = p;
if (subscribers.compareAndSet(a, b)) {
return true;
}
}
}
/**
* Removes the ReplaySubscription (if present) from the subscribers array atomically.
* @param p the target ReplaySubscription wrapping a downstream Subscriber with state
*/
@SuppressWarnings("unchecked")
public void removeChild(ReplaySubscription p) {
for (;;) {
ReplaySubscription[] a = subscribers.get();
int n = a.length;
if (n == 0) {
return;
}
int j = -1;
for (int i = 0; i < n; i++) {
if (a[i].equals(p)) {
j = i;
break;
}
}
if (j < 0) {
return;
}
ReplaySubscription[] b;
if (n == 1) {
b = EMPTY;
} else {
b = new ReplaySubscription[n - 1];
System.arraycopy(a, 0, b, 0, j);
System.arraycopy(a, j + 1, b, j, n - j - 1);
}
if (subscribers.compareAndSet(a, b)) {
return;
}
}
}
@Override
public void onSubscribe(Subscription s) {
SubscriptionHelper.setOnce(connection, s, Long.MAX_VALUE);
}
/**
* Connects the cache to the source.
* Make sure this is called only once.
*/
public void connect() {
source.subscribe(this);
isConnected = true;
}
@Override
public void onNext(T t) {
if (!sourceDone) {
Object o = NotificationLite.next(t);
add(o);
for (ReplaySubscription> rp : subscribers.get()) {
rp.replay();
}
}
}
@SuppressWarnings("unchecked")
@Override
public void onError(Throwable e) {
if (!sourceDone) {
sourceDone = true;
Object o = NotificationLite.error(e);
add(o);
SubscriptionHelper.cancel(connection);
for (ReplaySubscription> rp : subscribers.getAndSet(TERMINATED)) {
rp.replay();
}
} else {
RxJavaPlugins.onError(e);
}
}
@SuppressWarnings("unchecked")
@Override
public void onComplete() {
if (!sourceDone) {
sourceDone = true;
Object o = NotificationLite.complete();
add(o);
SubscriptionHelper.cancel(connection);
for (ReplaySubscription> rp : subscribers.getAndSet(TERMINATED)) {
rp.replay();
}
}
}
}
/**
* Keeps track of the current request amount and the replay position for a child Subscriber.
*
* @param
*/
static final class ReplaySubscription
extends AtomicInteger implements Subscription {
private static final long serialVersionUID = -2557562030197141021L;
private static final long CANCELLED = Long.MIN_VALUE;
/** The actual child subscriber. */
final Subscriber super T> child;
/** The cache state object. */
final CacheState state;
/**
* Number of items requested and also the cancelled indicator if
* it contains {@link #CANCELLED}.
*/
final AtomicLong requested;
/**
* Contains the reference to the buffer segment in replay.
* Accessed after reading state.size() and when emitting == true.
*/
Object[] currentBuffer;
/**
* Contains the index into the currentBuffer where the next value is expected.
* Accessed after reading state.size() and when emitting == true.
*/
int currentIndexInBuffer;
/**
* Contains the absolute index up until the values have been replayed so far.
*/
int index;
/** Number of items emitted so far. */
long emitted;
ReplaySubscription(Subscriber super T> child, CacheState state) {
this.child = child;
this.state = state;
this.requested = new AtomicLong();
}
@Override
public void request(long n) {
if (SubscriptionHelper.validate(n)) {
BackpressureHelper.addCancel(requested, n);
replay();
}
}
@Override
public void cancel() {
if (requested.getAndSet(CANCELLED) != CANCELLED) {
state.removeChild(this);
}
}
/**
* Continue replaying available values if there are requests for them.
*/
public void replay() {
if (getAndIncrement() != 0) {
return;
}
int missed = 1;
final Subscriber super T> child = this.child;
AtomicLong rq = requested;
long e = emitted;
for (;;) {
long r = rq.get();
if (r == CANCELLED) {
return;
}
// read the size, if it is non-zero, we can safely read the head and
// read values up to the given absolute index
int s = state.size();
if (s != 0) {
Object[] b = currentBuffer;
// latch onto the very first buffer now that it is available.
if (b == null) {
b = state.head();
currentBuffer = b;
}
final int n = b.length - 1;
int j = index;
int k = currentIndexInBuffer;
while (j < s && e != r) {
if (rq.get() == CANCELLED) {
return;
}
if (k == n) {
b = (Object[])b[n];
k = 0;
}
Object o = b[k];
if (NotificationLite.accept(o, child)) {
return;
}
k++;
j++;
e++;
}
if (rq.get() == CANCELLED) {
return;
}
if (r == e) {
Object o = b[k];
if (NotificationLite.isComplete(o)) {
child.onComplete();
return;
} else
if (NotificationLite.isError(o)) {
child.onError(NotificationLite.getError(o));
return;
}
}
index = j;
currentIndexInBuffer = k;
currentBuffer = b;
}
emitted = e;
missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy