/**
* Copyright 2014 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.subjects;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import rx.Observer;
import rx.Scheduler;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.functions.Functions;
import rx.internal.operators.NotificationLite;
import rx.schedulers.Timestamped;
import rx.subjects.ReplaySubject.NodeList.Node;
import rx.subjects.SubjectSubscriptionManager.SubjectObserver;
/**
* Subject that buffers all items it observes and replays them to any {@link Observer} that subscribes.
* <p>
* <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/S.ReplaySubject.png" alt="">
* <p>
* Example usage:
* <p>
* <pre> {@code
ReplaySubject<Object> subject = ReplaySubject.create();
subject.onNext("one");
subject.onNext("two");
subject.onNext("three");
subject.onCompleted();
// both of the following will get the onNext/onCompleted calls from above
subject.subscribe(observer1);
subject.subscribe(observer2);
} </pre>
*
* @param <T>
* the type of items observed and emitted by the Subject
*/
public final class ReplaySubject<T> extends Subject<T, T> {
/**
* Creates an unbounded replay subject.
* <p>
* The internal buffer is backed by an {@link ArrayList} and starts with an initial capacity of 16. Once the
* number of items reaches this capacity, it will grow as necessary (usually by 50%). However, as the
* number of items grows, this causes frequent array reallocation and copying, and may hurt performance
* and latency. This can be avoided with the {@link #create(int)} overload which takes an initial capacity
* parameter and can be tuned to reduce the array reallocation frequency as needed.
*
* @param <T>
* the type of items observed and emitted by the Subject
* @return the created subject
*/
public static <T> ReplaySubject<T> create() {
return create(16);
}
/**
* Creates an unbounded replay subject with the specified initial buffer capacity.
* <p>
* Use this method to avoid excessive array reallocation while the internal buffer grows to accomodate new
* items. For example, if you know that the buffer will hold 32k items, you can ask the
* {@code ReplaySubject} to preallocate its internal array with a capacity to hold that many items. Once
* the items start to arrive, the internal array won't need to grow, creating less garbage and no overhead
* due to frequent array-copying.
*
* @param <T>
* the type of items observed and emitted by the Subject
* @param capacity
* the initial buffer capacity
* @return the created subject
*/
public static <T> ReplaySubject<T> create(int capacity) {
final UnboundedReplayState<T> state = new UnboundedReplayState<T>(capacity);
SubjectSubscriptionManager<T> ssm = new SubjectSubscriptionManager<T>();
ssm.onStart = new Action1<SubjectObserver<T>>() {
@Override
public void call(SubjectObserver<T> o) {
// replay history for this observer using the subscribing thread
int lastIndex = state.replayObserverFromIndex(0, o);
// now that it is caught up add to observers
o.index(lastIndex);
}
};
ssm.onTerminated = new Action1<SubjectObserver<T>>() {
@Override
public void call(SubjectObserver<T> o) {
Integer idx = o.index();
if (idx == null) {
idx = 0;
}
// we will finish replaying if there is anything left
state.replayObserverFromIndex(idx, o);
}
};
return new ReplaySubject<T>(ssm, ssm, state);
}
/**
* Creates an unbounded replay subject with the bounded-implementation for testing purposes.
* <p>
* This variant behaves like the regular unbounded {@code ReplaySubject} created via {@link #create()} but
* uses the structures of the bounded-implementation. This is by no means intended for the replacement of
* the original, array-backed and unbounded {@code ReplaySubject} due to the additional overhead of the
* linked-list based internal buffer. The sole purpose is to allow testing and reasoning about the behavior
* of the bounded implementations without the interference of the eviction policies.
*
* @param <T>
* the type of items observed and emitted by the Subject
* @return the created subject
*/
/* public */ static <T> ReplaySubject<T> createUnbounded() {
final BoundedState<T> state = new BoundedState<T>(
new EmptyEvictionPolicy(),
Functions.identity(),
Functions.identity()
);
return createWithState(state, new DefaultOnAdd<T>(state));
}
/**
* Creates a size-bounded replay subject.
* <p>
* In this setting, the {@code ReplaySubject} holds at most {@code size} items in its internal buffer and
* discards the oldest item.
* <p>
* When observers subscribe to a terminated {@code ReplaySubject}, they are guaranteed to see at most
* {@code size} {@code onNext} events followed by a termination event.
* <p>
* If an observer subscribes while the {@code ReplaySubject} is active, it will observe all items in the
* buffer at that point in time and each item observed afterwards, even if the buffer evicts items due to
* the size constraint in the mean time. In other words, once an Observer subscribes, it will receive items
* without gaps in the sequence.
*
* @param <T>
* the type of items observed and emitted by the Subject
* @param size
* the maximum number of buffered items
* @return the created subject
*/
public static <T> ReplaySubject<T> createWithSize(int size) {
final BoundedState<T> state = new BoundedState<T>(
new SizeEvictionPolicy(size),
Functions.identity(),
Functions.identity()
);
return createWithState(state, new DefaultOnAdd<T>(state));
}
/**
* Creates a time-bounded replay subject.
* <p>
* In this setting, the {@code ReplaySubject} internally tags each observed item with a timestamp value
* supplied by the {@link Scheduler} and keeps only those whose age is less than the supplied time value
* converted to milliseconds. For example, an item arrives at T=0 and the max age is set to 5; at T>=5
* this first item is then evicted by any subsequent item or termination event, leaving the buffer empty.
* <p>
* Once the subject is terminated, observers subscribing to it will receive items that remained in the
* buffer after the terminal event, regardless of their age.
* <p>
* If an observer subscribes while the {@code ReplaySubject} is active, it will observe only those items
* from within the buffer that have an age less than the specified time, and each item observed thereafter,
* even if the buffer evicts items due to the time constraint in the mean time. In other words, once an
* observer subscribes, it observes items without gaps in the sequence except for any outdated items at the
* beginning of the sequence.
* <p>
* Note that terminal notifications ({@code onError} and {@code onCompleted}) trigger eviction as well. For
* example, with a max age of 5, the first item is observed at T=0, then an {@code onCompleted} notification
* arrives at T=10. If an observer subscribes at T=11, it will find an empty {@code ReplaySubject} with just
* an {@code onCompleted} notification.
*
* @param <T>
* the type of items observed and emitted by the Subject
* @param time
* the maximum age of the contained items
* @param unit
* the time unit of {@code time}
* @param scheduler
* the {@link Scheduler} that provides the current time
* @return the created subject
*/
public static <T> ReplaySubject<T> createWithTime(long time, TimeUnit unit, final Scheduler scheduler) {
final BoundedState<T> state = new BoundedState<T>(
new TimeEvictionPolicy(unit.toMillis(time), scheduler),
new AddTimestamped(scheduler),
new RemoveTimestamped()
);
return createWithState(state, new TimedOnAdd<T>(state, scheduler));
}
/**
* Creates a time- and size-bounded replay subject.
* <p>
* In this setting, the {@code ReplaySubject} internally tags each received item with a timestamp value
* supplied by the {@link Scheduler} and holds at most {@code size} items in its internal buffer. It evicts
* items from the start of the buffer if their age becomes less-than or equal to the supplied age in
* milliseconds or the buffer reaches its {@code size} limit.
* <p>
* When observers subscribe to a terminated {@code ReplaySubject}, they observe the items that remained in
* the buffer after the terminal notification, regardless of their age, but at most {@code size} items.
* <p>
* If an observer subscribes while the {@code ReplaySubject} is active, it will observe only those items
* from within the buffer that have age less than the specified time and each subsequent item, even if the
* buffer evicts items due to the time constraint in the mean time. In other words, once an observer
* subscribes, it observes items without gaps in the sequence except for the outdated items at the beginning
* of the sequence.
* <p>
* Note that terminal notifications ({@code onError} and {@code onCompleted}) trigger eviction as well. For
* example, with a max age of 5, the first item is observed at T=0, then an {@code onCompleted} notification
* arrives at T=10. If an observer subscribes at T=11, it will find an empty {@code ReplaySubject} with just
* an {@code onCompleted} notification.
*
* @param <T>
* the type of items observed and emitted by the Subject
* @param time
* the maximum age of the contained items
* @param unit
* the time unit of {@code time}
* @param size
* the maximum number of buffered items
* @param scheduler
* the {@link Scheduler} that provides the current time
* @return the created subject
*/
public static <T> ReplaySubject<T> createWithTimeAndSize(long time, TimeUnit unit, int size, final Scheduler scheduler) {
final BoundedState<T> state = new BoundedState<T>(
new PairEvictionPolicy(
new SizeEvictionPolicy(size),
new TimeEvictionPolicy(unit.toMillis(time), scheduler)
),
new AddTimestamped(scheduler),
new RemoveTimestamped()
);
return createWithState(state, new TimedOnAdd<T>(state, scheduler));
}
/**
* Creates a bounded replay subject with the given state shared between the subject and the
* {@link OnSubscribe} functions.
*
* @param <T>
* the type of items observed and emitted by the Subject
* @param state
* the shared state
* @return the created subject
*/
static final <T> ReplaySubject<T> createWithState(final BoundedState<T> state,
Action1<SubjectObserver<T>> onStart) {
SubjectSubscriptionManager<T> ssm = new SubjectSubscriptionManager<T>();
ssm.onStart = onStart;
ssm.onTerminated = new Action1<SubjectObserver<T>>() {
@Override
public void call(SubjectObserver<T> t1) {
NodeList.Node<Object> l = t1.index();
if (l == null) {
l = state.head();
}
state.replayObserverFromIndex(l, t1);
}
};
return new ReplaySubject<T>(ssm, ssm, state);
}
/** The state storing the history and the references. */
final ReplayState<T, ?> state;
/** The manager of subscribers. */
final SubjectSubscriptionManager<T> ssm;
ReplaySubject(OnSubscribe<T> onSubscribe, SubjectSubscriptionManager<T> ssm, ReplayState<T, ?> state) {
super(onSubscribe);
this.ssm = ssm;
this.state = state;
}
@Override
public void onNext(T t) {
if (ssm.active) {
state.next(t);
for (SubjectSubscriptionManager.SubjectObserver<? super T> o : ssm.observers()) {
if (caughtUp(o)) {
o.onNext(t);
}
}
}
}
@Override
public void onError(final Throwable e) {
if (ssm.active) {
state.error(e);
for (SubjectObserver<? super T> o : ssm.terminate(NotificationLite.instance().error(e))) {
if (caughtUp(o)) {
o.onError(e);
}
}
}
}
@Override
public void onCompleted() {
if (ssm.active) {
state.complete();
for (SubjectObserver<? super T> o : ssm.terminate(NotificationLite.instance().completed())) {
if (caughtUp(o)) {
o.onCompleted();
}
}
}
}
/**
* @return Returns the number of subscribers.
*/
/* Support test. */int subscriberCount() {
return ssm.state.observers.length;
}
private boolean caughtUp(SubjectObserver<? super T> o) {
if (!o.caughtUp) {
o.caughtUp = true;
state.replayObserver(o);
return false;
} else {
// it was caught up so proceed the "raw route"
return true;
}
}
// *********************
// State implementations
// *********************
/**
* The unbounded replay state.
* @param <T> the input and output type
*/
static final class UnboundedReplayState<T> implements ReplayState<T, Integer> {
private final NotificationLite<T> nl = NotificationLite.instance();
/** The buffer. */
private final ArrayList<Object> list;
/** The termination flag. */
private volatile boolean terminated;
/** The size of the buffer. */
volatile int index;
@SuppressWarnings("rawtypes")
static final AtomicIntegerFieldUpdater<UnboundedReplayState> INDEX_UPDATER
= AtomicIntegerFieldUpdater.newUpdater(UnboundedReplayState.class, "index");
public UnboundedReplayState(int initialCapacity) {
list = new ArrayList<Object>(initialCapacity);
}
@Override
public void next(T n) {
if (!terminated) {
list.add(nl.next(n));
INDEX_UPDATER.getAndIncrement(this);
}
}
public void accept(Observer<? super T> o, int idx) {
nl.accept(o, list.get(idx));
}
@Override
public void complete() {
if (!terminated) {
terminated = true;
list.add(nl.completed());
INDEX_UPDATER.getAndIncrement(this);
}
}
@Override
public void error(Throwable e) {
if (!terminated) {
terminated = true;
list.add(nl.error(e));
INDEX_UPDATER.getAndIncrement(this);
}
}
@Override
public boolean terminated() {
return terminated;
}
@Override
public void replayObserver(SubjectObserver<? super T> observer) {
Integer lastEmittedLink = observer.index();
if (lastEmittedLink != null) {
int l = replayObserverFromIndex(lastEmittedLink, observer);
observer.index(l);
} else {
throw new IllegalStateException("failed to find lastEmittedLink for: " + observer);
}
}
@Override
public Integer replayObserverFromIndex(Integer idx, SubjectObserver<? super T> observer) {
int i = idx;
while (i < index) {
accept(observer, i);
i++;
}
return i;
}
@Override
public Integer replayObserverFromIndexTest(Integer idx, SubjectObserver<? super T> observer, long now) {
return replayObserverFromIndex(idx, observer);
}
}
/**
* The bounded replay state.
* @param <T> the input and output type
*/
static final class BoundedState<T> implements ReplayState<T, NodeList.Node<Object>> {
final NodeList<Object> list;
final EvictionPolicy evictionPolicy;
final Func1<Object, Object> enterTransform;
final Func1<Object, Object> leaveTransform;
final NotificationLite<T> nl = NotificationLite.instance();
volatile boolean terminated;
volatile NodeList.Node<Object> tail;
public BoundedState(EvictionPolicy evictionPolicy, Func1<Object, Object> enterTransform,
Func1<Object, Object> leaveTransform) {
this.list = new NodeList<Object>();
this.tail = list.tail;
this.evictionPolicy = evictionPolicy;
this.enterTransform = enterTransform;
this.leaveTransform = leaveTransform;
}
@Override
public void next(T value) {
if (!terminated) {
list.addLast(enterTransform.call(nl.next(value)));
evictionPolicy.evict(list);
tail = list.tail;
}
}
@Override
public void complete() {
if (!terminated) {
terminated = true;
// don't evict the terminal value
evictionPolicy.evict(list);
// so add it later
list.addLast(enterTransform.call(nl.completed()));
tail = list.tail;
}
}
@Override
public void error(Throwable e) {
if (!terminated) {
terminated = true;
// don't evict the terminal value
evictionPolicy.evict(list);
// so add it later
list.addLast(enterTransform.call(nl.error(e)));
tail = list.tail;
}
}
public void accept(Observer<? super T> o, NodeList.Node<Object> node) {
nl.accept(o, leaveTransform.call(node.value));
}
/**
* Accept only non-stale nodes.
* @param o the target observer
* @param node the node to accept or reject
* @param now the current time
*/
public void acceptTest(Observer<? super T> o, NodeList.Node<Object> node, long now) {
Object v = node.value;
if (!evictionPolicy.test(v, now)) {
nl.accept(o, leaveTransform.call(v));
}
}
public Node<Object> head() {
return list.head;
}
public Node<Object> tail() {
return tail;
}
@Override
public void replayObserver(SubjectObserver<? super T> observer) {
NodeList.Node<Object> lastEmittedLink = observer.index();
NodeList.Node<Object> l = replayObserverFromIndex(lastEmittedLink, observer);
observer.index(l);
}
@Override
public NodeList.Node<Object> replayObserverFromIndex(
NodeList.Node<Object> l, SubjectObserver<? super T> observer) {
while (l != tail()) {
accept(observer, l.next);
l = l.next;
}
return l;
}
@Override
public NodeList.Node<Object> replayObserverFromIndexTest(
NodeList.Node<Object> l, SubjectObserver<? super T> observer, long now) {
while (l != tail()) {
acceptTest(observer, l.next, now);
l = l.next;
}
return l;
}
@Override
public boolean terminated() {
return terminated;
}
}
// **************
// API interfaces
// **************
/**
* General API for replay state management.
* @param <T> the input and output type
* @param <I> the index type
*/
interface ReplayState<T, I> {
/** @return true if the subject has reached a terminal state. */
boolean terminated();
/**
* Replay contents to the given observer.
* @param observer the receiver of events
*/
void replayObserver(SubjectObserver<? super T> observer);
/**
* Replay the buffered values from an index position and return a new index
* @param idx the current index position
* @param observer the receiver of events
* @return the new index position
*/
I replayObserverFromIndex(
I idx, SubjectObserver<? super T> observer);
/**
* Replay the buffered values from an index position while testing for stale entries and return a new index
* @param idx the current index position
* @param observer the receiver of events
* @return the new index position
*/
I replayObserverFromIndexTest(
I idx, SubjectObserver<? super T> observer, long now);
/**
* Add an OnNext value to the buffer
* @param value the value to add
*/
void next(T value);
/**
* Add an OnError exception and terminate the subject
* @param e the exception to add
*/
void error(Throwable e);
/**
* Add an OnCompleted exception and terminate the subject
*/
void complete();
}
/** Interface to manage eviction checking. */
interface EvictionPolicy {
/**
* Subscribe-time checking for stale entries.
* @param value the value to test
* @param now the current time
* @return true if the value may be evicted
*/
boolean test(Object value, long now);
/**
* Evict values from the list
* @param list
*/
void evict(NodeList<Object> list);
}
// ************************
// Callback implementations
// ************************
/**
* Remove elements from the beginning of the list if the size exceeds some threshold.
*/
static final class SizeEvictionPolicy implements EvictionPolicy {
final int maxSize;
public SizeEvictionPolicy(int maxSize) {
this.maxSize = maxSize;
}
@Override
public void evict(NodeList<Object> t1) {
while (t1.size() > maxSize) {
t1.removeFirst();
}
}
@Override
public boolean test(Object value, long now) {
return true; // size gets never stale
}
}
/**
* Remove elements from the beginning of the list if the Timestamped value is older than
* a threshold.
*/
static final class TimeEvictionPolicy implements EvictionPolicy {
final long maxAgeMillis;
final Scheduler scheduler;
public TimeEvictionPolicy(long maxAgeMillis, Scheduler scheduler) {
this.maxAgeMillis = maxAgeMillis;
this.scheduler = scheduler;
}
@Override
public void evict(NodeList<Object> t1) {
long now = scheduler.now();
while (!t1.isEmpty()) {
NodeList.Node<Object> n = t1.head.next;
if (test(n.value, now)) {
t1.removeFirst();
} else {
break;
}
}
}
@Override
public boolean test(Object value, long now) {
Timestamped<?> ts = (Timestamped<?>)value;
return ts.getTimestampMillis() <= now - maxAgeMillis;
}
}
/**
* Pairs up two eviction policy callbacks.
*/
static final class PairEvictionPolicy implements EvictionPolicy {
final EvictionPolicy first;
final EvictionPolicy second;
public PairEvictionPolicy(EvictionPolicy first, EvictionPolicy second) {
this.first = first;
this.second = second;
}
@Override
public void evict(NodeList<Object> t1) {
first.evict(t1);
second.evict(t1);
}
@Override
public boolean test(Object value, long now) {
return first.test(value, now) || second.test(value, now);
}
};
/** Maps the values to Timestamped. */
static final class AddTimestamped implements Func1<Object, Object> {
final Scheduler scheduler;
public AddTimestamped(Scheduler scheduler) {
this.scheduler = scheduler;
}
@Override
public Object call(Object t1) {
return new Timestamped<Object>(scheduler.now(), t1);
}
}
/** Maps timestamped values back to raw objects. */
static final class RemoveTimestamped implements Func1<Object, Object> {
@Override
@SuppressWarnings("unchecked")
public Object call(Object t1) {
return ((Timestamped<Object>)t1).getValue();
}
}
/**
* Default action of simply replaying the buffer on subscribe.
* @param <T> the input and output value type
*/
static final class DefaultOnAdd<T> implements Action1<SubjectObserver<T>> {
final BoundedState<T> state;
public DefaultOnAdd(BoundedState<T> state) {
this.state = state;
}
@Override
public void call(SubjectObserver<T> t1) {
NodeList.Node<Object> l = state.replayObserverFromIndex(state.head(), t1);
t1.index(l);
}
}
/**
* Action of replaying non-stale entries of the buffer on subscribe
* @param <T> the input and output value
*/
static final class TimedOnAdd<T> implements Action1<SubjectObserver<T>> {
final BoundedState<T> state;
final Scheduler scheduler;
public TimedOnAdd(BoundedState<T> state, Scheduler scheduler) {
this.state = state;
this.scheduler = scheduler;
}
@Override
public void call(SubjectObserver<T> t1) {
NodeList.Node<Object> l;
if (!state.terminated) {
// ignore stale entries if still active
l = state.replayObserverFromIndexTest(state.head(), t1, scheduler.now());
} else {
// accept all if terminated
l = state.replayObserverFromIndex(state.head(), t1);
}
t1.index(l);
}
}
/**
* A singly-linked list with volatile next node pointer.
* @param <T> the value type
*/
static final class NodeList<T> {
/**
* The node containing the value and references to neighbours.
* @param <T> the value type
*/
static final class Node<T> {
/** The managed value. */
final T value;
/** The hard reference to the next node. */
volatile Node<T> next;
Node(T value) {
this.value = value;
}
}
/** The head of the list. */
final Node<T> head = new Node<T>(null);
/** The tail of the list. */
Node<T> tail = head;
/** The number of elements in the list. */
int size;
public void addLast(T value) {
Node<T> t = tail;
Node<T> t2 = new Node<T>(value);
t.next = t2;
tail = t2;
size++;
}
public T removeFirst() {
if (head.next == null) {
throw new IllegalStateException("Empty!");
}
Node<T> t = head.next;
head.next = t.next;
if (head.next == null) {
tail = head;
}
size--;
return t.value;
}
public boolean isEmpty() {
return size == 0;
}
public int size() {
return size;
}
public void clear() {
tail = head;
size = 0;
}
}
/** Empty eviction policy. */
static final class EmptyEvictionPolicy implements EvictionPolicy {
@Override
public boolean test(Object value, long now) {
return true;
}
@Override
public void evict(NodeList<Object> list) {
}
}
}