All Downloads are FREE. Search and download functionalities are using the official Maven repository.

rx.subjects.ReplaySubject Maven / Gradle / Ivy

There is a newer version: 0.20.7
Show newest version
/**
 * 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.
 * 

* *

* Example usage: *

*

 {@code

  ReplaySubject 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);

  } 
 * 
 * @param 
 *          the type of items observed and emitted by the Subject
 */
public final class ReplaySubject extends Subject {
    /**
     * Creates an unbounded replay subject.
     * 

* 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 * the type of items observed and emitted by the Subject * @return the created subject */ public static ReplaySubject create() { return create(16); } /** * Creates an unbounded replay subject with the specified initial buffer capacity. *

* 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 * the type of items observed and emitted by the Subject * @param capacity * the initial buffer capacity * @return the created subject */ public static ReplaySubject create(int capacity) { final UnboundedReplayState state = new UnboundedReplayState(capacity); SubjectSubscriptionManager ssm = new SubjectSubscriptionManager(); ssm.onStart = new Action1>() { @Override public void call(SubjectObserver 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>() { @Override public void call(SubjectObserver 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(ssm, ssm, state); } /** * Creates an unbounded replay subject with the bounded-implementation for testing purposes. *

* 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 * the type of items observed and emitted by the Subject * @return the created subject */ /* public */ static ReplaySubject createUnbounded() { final BoundedState state = new BoundedState( new EmptyEvictionPolicy(), Functions.identity(), Functions.identity() ); return createWithState(state, new DefaultOnAdd(state)); } /** * Creates a size-bounded replay subject. *

* In this setting, the {@code ReplaySubject} holds at most {@code size} items in its internal buffer and * discards the oldest item. *

* 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. *

* 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 * the type of items observed and emitted by the Subject * @param size * the maximum number of buffered items * @return the created subject */ public static ReplaySubject createWithSize(int size) { final BoundedState state = new BoundedState( new SizeEvictionPolicy(size), Functions.identity(), Functions.identity() ); return createWithState(state, new DefaultOnAdd(state)); } /** * Creates a time-bounded replay subject. *

* 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. *

* 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. *

* 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. *

* 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 * 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 ReplaySubject createWithTime(long time, TimeUnit unit, final Scheduler scheduler) { final BoundedState state = new BoundedState( new TimeEvictionPolicy(unit.toMillis(time), scheduler), new AddTimestamped(scheduler), new RemoveTimestamped() ); return createWithState(state, new TimedOnAdd(state, scheduler)); } /** * Creates a time- and size-bounded replay subject. *

* 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. *

* 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. *

* 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. *

* 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 * 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 ReplaySubject createWithTimeAndSize(long time, TimeUnit unit, int size, final Scheduler scheduler) { final BoundedState state = new BoundedState( new PairEvictionPolicy( new SizeEvictionPolicy(size), new TimeEvictionPolicy(unit.toMillis(time), scheduler) ), new AddTimestamped(scheduler), new RemoveTimestamped() ); return createWithState(state, new TimedOnAdd(state, scheduler)); } /** * Creates a bounded replay subject with the given state shared between the subject and the * {@link OnSubscribe} functions. * * @param * the type of items observed and emitted by the Subject * @param state * the shared state * @return the created subject */ static final ReplaySubject createWithState(final BoundedState state, Action1> onStart) { SubjectSubscriptionManager ssm = new SubjectSubscriptionManager(); ssm.onStart = onStart; ssm.onTerminated = new Action1>() { @Override public void call(SubjectObserver t1) { NodeList.Node l = t1.index(); if (l == null) { l = state.head(); } state.replayObserverFromIndex(l, t1); } }; return new ReplaySubject(ssm, ssm, state); } /** The state storing the history and the references. */ final ReplayState state; /** The manager of subscribers. */ final SubjectSubscriptionManager ssm; ReplaySubject(OnSubscribe onSubscribe, SubjectSubscriptionManager ssm, ReplayState state) { super(onSubscribe); this.ssm = ssm; this.state = state; } @Override public void onNext(T t) { if (ssm.active) { state.next(t); for (SubjectSubscriptionManager.SubjectObserver o : ssm.observers()) { if (caughtUp(o)) { o.onNext(t); } } } } @Override public void onError(final Throwable e) { if (ssm.active) { state.error(e); for (SubjectObserver o : ssm.terminate(NotificationLite.instance().error(e))) { if (caughtUp(o)) { o.onError(e); } } } } @Override public void onCompleted() { if (ssm.active) { state.complete(); for (SubjectObserver 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 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 the input and output type */ static final class UnboundedReplayState implements ReplayState { private final NotificationLite nl = NotificationLite.instance(); /** The buffer. */ private final ArrayList list; /** The termination flag. */ private volatile boolean terminated; /** The size of the buffer. */ volatile int index; @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater INDEX_UPDATER = AtomicIntegerFieldUpdater.newUpdater(UnboundedReplayState.class, "index"); public UnboundedReplayState(int initialCapacity) { list = new ArrayList(initialCapacity); } @Override public void next(T n) { if (!terminated) { list.add(nl.next(n)); INDEX_UPDATER.getAndIncrement(this); } } public void accept(Observer 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 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 observer) { int i = idx; while (i < index) { accept(observer, i); i++; } return i; } @Override public Integer replayObserverFromIndexTest(Integer idx, SubjectObserver observer, long now) { return replayObserverFromIndex(idx, observer); } } /** * The bounded replay state. * @param the input and output type */ static final class BoundedState implements ReplayState> { final NodeList list; final EvictionPolicy evictionPolicy; final Func1 enterTransform; final Func1 leaveTransform; final NotificationLite nl = NotificationLite.instance(); volatile boolean terminated; volatile NodeList.Node tail; public BoundedState(EvictionPolicy evictionPolicy, Func1 enterTransform, Func1 leaveTransform) { this.list = new NodeList(); 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 o, NodeList.Node 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 o, NodeList.Node node, long now) { Object v = node.value; if (!evictionPolicy.test(v, now)) { nl.accept(o, leaveTransform.call(v)); } } public Node head() { return list.head; } public Node tail() { return tail; } @Override public void replayObserver(SubjectObserver observer) { NodeList.Node lastEmittedLink = observer.index(); NodeList.Node l = replayObserverFromIndex(lastEmittedLink, observer); observer.index(l); } @Override public NodeList.Node replayObserverFromIndex( NodeList.Node l, SubjectObserver observer) { while (l != tail()) { accept(observer, l.next); l = l.next; } return l; } @Override public NodeList.Node replayObserverFromIndexTest( NodeList.Node l, SubjectObserver 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 the input and output type * @param the index type */ interface ReplayState { /** @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 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 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 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 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 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 t1) { long now = scheduler.now(); while (!t1.isEmpty()) { NodeList.Node 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 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 { final Scheduler scheduler; public AddTimestamped(Scheduler scheduler) { this.scheduler = scheduler; } @Override public Object call(Object t1) { return new Timestamped(scheduler.now(), t1); } } /** Maps timestamped values back to raw objects. */ static final class RemoveTimestamped implements Func1 { @Override @SuppressWarnings("unchecked") public Object call(Object t1) { return ((Timestamped)t1).getValue(); } } /** * Default action of simply replaying the buffer on subscribe. * @param the input and output value type */ static final class DefaultOnAdd implements Action1> { final BoundedState state; public DefaultOnAdd(BoundedState state) { this.state = state; } @Override public void call(SubjectObserver t1) { NodeList.Node l = state.replayObserverFromIndex(state.head(), t1); t1.index(l); } } /** * Action of replaying non-stale entries of the buffer on subscribe * @param the input and output value */ static final class TimedOnAdd implements Action1> { final BoundedState state; final Scheduler scheduler; public TimedOnAdd(BoundedState state, Scheduler scheduler) { this.state = state; this.scheduler = scheduler; } @Override public void call(SubjectObserver t1) { NodeList.Node 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 the value type */ static final class NodeList { /** * The node containing the value and references to neighbours. * @param the value type */ static final class Node { /** The managed value. */ final T value; /** The hard reference to the next node. */ volatile Node next; Node(T value) { this.value = value; } } /** The head of the list. */ final Node head = new Node(null); /** The tail of the list. */ Node tail = head; /** The number of elements in the list. */ int size; public void addLast(T value) { Node t = tail; Node t2 = new Node(value); t.next = t2; tail = t2; size++; } public T removeFirst() { if (head.next == null) { throw new IllegalStateException("Empty!"); } Node 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 list) { } } }