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

io.reactivex.observable.subjects.ReplaySubject Maven / Gradle / Ivy

The newest version!
/**
 * 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.observable.subjects;

import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.*;

import io.reactivex.common.*;
import io.reactivex.common.annotations.CheckReturnValue;
import io.reactivex.common.internal.functions.ObjectHelper;
import io.reactivex.observable.Observer;
import io.reactivex.observable.internal.utils.NotificationLite;

/**
 * Replays events to Observers.
 * 

* *

* Example usage: *

*

 {@code

  ReplaySubject subject = new ReplaySubject<>();
  subject.onNext("one");
  subject.onNext("two");
  subject.onNext("three");
  subject.onComplete();

  // both of the following will get the onNext/onComplete calls from above
  subject.subscribe(observer1);
  subject.subscribe(observer2);

  } 
 *
 * @param  the value type
 */
public final class ReplaySubject extends Subject {
    final ReplayBuffer buffer;

    final AtomicReference[]> observers;

    @SuppressWarnings("rawtypes")
    static final ReplayDisposable[] EMPTY = new ReplayDisposable[0];

    @SuppressWarnings("rawtypes")
    static final ReplayDisposable[] TERMINATED = new ReplayDisposable[0];

    boolean done;

    /**
     * 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 */ @CheckReturnValue public static ReplaySubject create() { return new ReplaySubject(new UnboundedReplayBuffer(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 accommodate 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 capacityHint * the initial buffer capacity * @return the created subject */ @CheckReturnValue public static ReplaySubject create(int capacityHint) { return new ReplaySubject(new UnboundedReplayBuffer(capacityHint)); } /** * 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 maxSize * the maximum number of buffered items * @return the created subject */ @CheckReturnValue public static ReplaySubject createWithSize(int maxSize) { return new ReplaySubject(new SizeBoundReplayBuffer(maxSize)); } /** * 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 */ /* test */ static ReplaySubject createUnbounded() { return new ReplaySubject(new SizeBoundReplayBuffer(Integer.MAX_VALUE)); } /** * 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 onComplete}) trigger eviction as well. For * example, with a max age of 5, the first item is observed at T=0, then an {@code onComplete} notification * arrives at T=10. If an observer subscribes at T=11, it will find an empty {@code ReplaySubject} with just * an {@code onComplete} notification. * * @param * the type of items observed and emitted by the Subject * @param maxAge * 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 */ @CheckReturnValue public static ReplaySubject createWithTime(long maxAge, TimeUnit unit, Scheduler scheduler) { return new ReplaySubject(new SizeAndTimeBoundReplayBuffer(Integer.MAX_VALUE, maxAge, unit, 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 onComplete}) trigger eviction as well. For * example, with a max age of 5, the first item is observed at T=0, then an {@code onComplete} notification * arrives at T=10. If an observer subscribes at T=11, it will find an empty {@code ReplaySubject} with just * an {@code onComplete} notification. * * @param * the type of items observed and emitted by the Subject * @param maxAge * the maximum age of the contained items * @param unit * the time unit of {@code time} * @param maxSize * the maximum number of buffered items * @param scheduler * the {@link Scheduler} that provides the current time * @return the created subject */ @CheckReturnValue public static ReplaySubject createWithTimeAndSize(long maxAge, TimeUnit unit, Scheduler scheduler, int maxSize) { return new ReplaySubject(new SizeAndTimeBoundReplayBuffer(maxSize, maxAge, unit, scheduler)); } /** * Constructs a ReplayProcessor with the given custom ReplayBuffer instance. * @param buffer the ReplayBuffer instance, not null (not verified) */ @SuppressWarnings("unchecked") ReplaySubject(ReplayBuffer buffer) { this.buffer = buffer; this.observers = new AtomicReference[]>(EMPTY); } @Override protected void subscribeActual(Observer observer) { ReplayDisposable rs = new ReplayDisposable(observer, this); observer.onSubscribe(rs); if (!rs.cancelled) { if (add(rs)) { if (rs.cancelled) { remove(rs); return; } } buffer.replay(rs); } } @Override public void onSubscribe(Disposable s) { if (done) { s.dispose(); } } @Override public void onNext(T t) { if (t == null) { onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources.")); return; } if (done) { return; } ReplayBuffer b = buffer; b.add(t); for (ReplayDisposable rs : observers.get()) { b.replay(rs); } } @Override public void onError(Throwable t) { if (t == null) { t = new NullPointerException("onError called with null. Null values are generally not allowed in 2.x operators and sources."); } if (done) { RxJavaCommonPlugins.onError(t); return; } done = true; Object o = NotificationLite.error(t); ReplayBuffer b = buffer; b.addFinal(o); for (ReplayDisposable rs : terminate(o)) { b.replay(rs); } } @Override public void onComplete() { if (done) { return; } done = true; Object o = NotificationLite.complete(); ReplayBuffer b = buffer; b.addFinal(o); for (ReplayDisposable rs : terminate(o)) { b.replay(rs); } } @Override public boolean hasObservers() { return observers.get().length != 0; } /* test */ int observerCount() { return observers.get().length; } @Override public Throwable getThrowable() { Object o = buffer.get(); if (NotificationLite.isError(o)) { return NotificationLite.getError(o); } return null; } /** * Returns a single value the Subject currently has or null if no such value exists. *

The method is thread-safe. * @return a single value the Subject currently has or null if no such value exists */ public T getValue() { return buffer.getValue(); } /** An empty array to avoid allocation in getValues(). */ private static final Object[] EMPTY_ARRAY = new Object[0]; /** * Returns an Object array containing snapshot all values of the Subject. *

The method is thread-safe. * @return the array containing the snapshot of all values of the Subject */ public Object[] getValues() { @SuppressWarnings("unchecked") T[] a = (T[])EMPTY_ARRAY; T[] b = getValues(a); if (b == EMPTY_ARRAY) { return new Object[0]; } return b; } /** * Returns a typed array containing a snapshot of all values of the Subject. *

The method follows the conventions of Collection.toArray by setting the array element * after the last value to null (if the capacity permits). *

The method is thread-safe. * @param array the target array to copy values into if it fits * @return the given array if the values fit into it or a new array containing all values */ public T[] getValues(T[] array) { return buffer.getValues(array); } @Override public boolean hasComplete() { Object o = buffer.get(); return NotificationLite.isComplete(o); } @Override public boolean hasThrowable() { Object o = buffer.get(); return NotificationLite.isError(o); } /** * Returns true if the subject has any value. *

The method is thread-safe. * @return true if the subject has any value */ public boolean hasValue() { return buffer.size() != 0; // NOPMD } /* test*/ int size() { return buffer.size(); } boolean add(ReplayDisposable rs) { for (;;) { ReplayDisposable[] a = observers.get(); if (a == TERMINATED) { return false; } int len = a.length; @SuppressWarnings("unchecked") ReplayDisposable[] b = new ReplayDisposable[len + 1]; System.arraycopy(a, 0, b, 0, len); b[len] = rs; if (observers.compareAndSet(a, b)) { return true; } } } @SuppressWarnings("unchecked") void remove(ReplayDisposable rs) { for (;;) { ReplayDisposable[] a = observers.get(); if (a == TERMINATED || a == EMPTY) { return; } int len = a.length; int j = -1; for (int i = 0; i < len; i++) { if (a[i] == rs) { j = i; break; } } if (j < 0) { return; } ReplayDisposable[] b; if (len == 1) { b = EMPTY; } else { b = new ReplayDisposable[len - 1]; System.arraycopy(a, 0, b, 0, j); System.arraycopy(a, j + 1, b, j, len - j - 1); } if (observers.compareAndSet(a, b)) { return; } } } @SuppressWarnings("unchecked") ReplayDisposable[] terminate(Object terminalValue) { if (buffer.compareAndSet(null, terminalValue)) { return observers.getAndSet(TERMINATED); } return TERMINATED; } /** * Abstraction over a buffer that receives events and replays them to * individual Observers. * * @param the value type */ interface ReplayBuffer { void add(T value); void addFinal(Object notificationLite); void replay(ReplayDisposable rs); int size(); T getValue(); T[] getValues(T[] array); /** * Returns the terminal NotificationLite object or null if not yet terminated. * @return the terminal NotificationLite object or null if not yet terminated */ Object get(); /** * Atomically compares and sets the next terminal NotificationLite object if the * current equals to the expected NotificationLite object. * @param expected the expected NotificationLite object * @param next the next NotificationLite object * @return true if successful */ boolean compareAndSet(Object expected, Object next); } static final class ReplayDisposable extends AtomicInteger implements Disposable { private static final long serialVersionUID = 466549804534799122L; final Observer actual; final ReplaySubject state; Object index; volatile boolean cancelled; ReplayDisposable(Observer actual, ReplaySubject state) { this.actual = actual; this.state = state; } @Override public void dispose() { if (!cancelled) { cancelled = true; state.remove(this); } } @Override public boolean isDisposed() { return cancelled; } } static final class UnboundedReplayBuffer extends AtomicReference implements ReplayBuffer { private static final long serialVersionUID = -733876083048047795L; final List buffer; volatile boolean done; volatile int size; UnboundedReplayBuffer(int capacityHint) { this.buffer = new ArrayList(ObjectHelper.verifyPositive(capacityHint, "capacityHint")); } @Override public void add(T value) { buffer.add(value); size++; } @Override public void addFinal(Object notificationLite) { buffer.add(notificationLite); size++; done = true; } @Override @SuppressWarnings("unchecked") public T getValue() { int s = size; if (s != 0) { List b = buffer; Object o = b.get(s - 1); if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { if (s == 1) { return null; } return (T)b.get(s - 2); } return (T)o; } return null; } @Override @SuppressWarnings("unchecked") public T[] getValues(T[] array) { int s = size; if (s == 0) { if (array.length != 0) { array[0] = null; } return array; } List b = buffer; Object o = b.get(s - 1); if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { s--; if (s == 0) { if (array.length != 0) { array[0] = null; } return array; } } if (array.length < s) { array = (T[])Array.newInstance(array.getClass().getComponentType(), s); } for (int i = 0; i < s; i++) { array[i] = (T)b.get(i); } if (array.length > s) { array[s] = null; } return array; } @Override @SuppressWarnings("unchecked") public void replay(ReplayDisposable rs) { if (rs.getAndIncrement() != 0) { return; } int missed = 1; final List b = buffer; final Observer a = rs.actual; Integer indexObject = (Integer)rs.index; int index; if (indexObject != null) { index = indexObject; } else { index = 0; rs.index = 0; } for (;;) { if (rs.cancelled) { rs.index = null; return; } int s = size; while (s != index) { if (rs.cancelled) { rs.index = null; return; } Object o = b.get(index); if (done) { if (index + 1 == s) { s = size; if (index + 1 == s) { if (NotificationLite.isComplete(o)) { a.onComplete(); } else { a.onError(NotificationLite.getError(o)); } rs.index = null; rs.cancelled = true; return; } } } a.onNext((T)o); index++; } if (index != size) { continue; } rs.index = index; missed = rs.addAndGet(-missed); if (missed == 0) { break; } } } @Override public int size() { int s = size; if (s != 0) { Object o = buffer.get(s - 1); if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { return s - 1; } return s; } return 0; } } static final class Node extends AtomicReference> { private static final long serialVersionUID = 6404226426336033100L; final T value; Node(T value) { this.value = value; } } static final class TimedNode extends AtomicReference> { private static final long serialVersionUID = 6404226426336033100L; final T value; final long time; TimedNode(T value, long time) { this.value = value; this.time = time; } } static final class SizeBoundReplayBuffer extends AtomicReference implements ReplayBuffer { private static final long serialVersionUID = 1107649250281456395L; final int maxSize; int size; volatile Node head; Node tail; volatile boolean done; SizeBoundReplayBuffer(int maxSize) { this.maxSize = ObjectHelper.verifyPositive(maxSize, "maxSize"); Node h = new Node(null); this.tail = h; this.head = h; } void trim() { if (size > maxSize) { size--; Node h = head; head = h.get(); } } @Override public void add(T value) { Node n = new Node(value); Node t = tail; tail = n; size++; t.set(n); // releases both the tail and size trim(); } @Override public void addFinal(Object notificationLite) { Node n = new Node(notificationLite); Node t = tail; tail = n; size++; t.lazySet(n); // releases both the tail and size done = true; } @Override @SuppressWarnings("unchecked") public T getValue() { Node prev = null; Node h = head; for (;;) { Node next = h.get(); if (next == null) { break; } prev = h; h = next; } Object v = h.value; if (v == null) { return null; } if (NotificationLite.isComplete(v) || NotificationLite.isError(v)) { return (T)prev.value; } return (T)v; } @Override @SuppressWarnings("unchecked") public T[] getValues(T[] array) { Node h = head; int s = size(); if (s == 0) { if (array.length != 0) { array[0] = null; } } else { if (array.length < s) { array = (T[])Array.newInstance(array.getClass().getComponentType(), s); } int i = 0; while (i != s) { Node next = h.get(); array[i] = (T)next.value; i++; h = next; } if (array.length > s) { array[s] = null; } } return array; } @Override @SuppressWarnings("unchecked") public void replay(ReplayDisposable rs) { if (rs.getAndIncrement() != 0) { return; } int missed = 1; final Observer a = rs.actual; Node index = (Node)rs.index; if (index == null) { index = head; } for (;;) { for (;;) { if (rs.cancelled) { rs.index = null; return; } Node n = index.get(); if (n == null) { break; } Object o = n.value; if (done) { if (n.get() == null) { if (NotificationLite.isComplete(o)) { a.onComplete(); } else { a.onError(NotificationLite.getError(o)); } rs.index = null; rs.cancelled = true; return; } } a.onNext((T)o); index = n; } if (index.get() != null) { continue; } rs.index = index; missed = rs.addAndGet(-missed); if (missed == 0) { break; } } } @Override public int size() { int s = 0; Node h = head; while (s != Integer.MAX_VALUE) { Node next = h.get(); if (next == null) { Object o = h.value; if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { s--; } break; } s++; h = next; } return s; } } static final class SizeAndTimeBoundReplayBuffer extends AtomicReference implements ReplayBuffer { private static final long serialVersionUID = -8056260896137901749L; final int maxSize; final long maxAge; final TimeUnit unit; final Scheduler scheduler; int size; volatile TimedNode head; TimedNode tail; volatile boolean done; SizeAndTimeBoundReplayBuffer(int maxSize, long maxAge, TimeUnit unit, Scheduler scheduler) { this.maxSize = ObjectHelper.verifyPositive(maxSize, "maxSize"); this.maxAge = ObjectHelper.verifyPositive(maxAge, "maxAge"); this.unit = ObjectHelper.requireNonNull(unit, "unit is null"); this.scheduler = ObjectHelper.requireNonNull(scheduler, "scheduler is null"); TimedNode h = new TimedNode(null, 0L); this.tail = h; this.head = h; } void trim() { if (size > maxSize) { size--; TimedNode h = head; head = h.get(); } long limit = scheduler.now(unit) - maxAge; TimedNode h = head; for (;;) { TimedNode next = h.get(); if (next == null) { head = h; break; } if (next.time > limit) { head = h; break; } h = next; } } void trimFinal() { long limit = scheduler.now(unit) - maxAge; TimedNode h = head; for (;;) { TimedNode next = h.get(); if (next.get() == null) { head = h; break; } if (next.time > limit) { head = h; break; } h = next; } } @Override public void add(T value) { TimedNode n = new TimedNode(value, scheduler.now(unit)); TimedNode t = tail; tail = n; size++; t.set(n); // releases both the tail and size trim(); } @Override public void addFinal(Object notificationLite) { TimedNode n = new TimedNode(notificationLite, Long.MAX_VALUE); TimedNode t = tail; tail = n; size++; t.lazySet(n); // releases both the tail and size trimFinal(); done = true; } @Override @SuppressWarnings("unchecked") public T getValue() { TimedNode prev = null; TimedNode h = head; for (;;) { TimedNode next = h.get(); if (next == null) { break; } prev = h; h = next; } Object v = h.value; if (v == null) { return null; } if (NotificationLite.isComplete(v) || NotificationLite.isError(v)) { return (T)prev.value; } return (T)v; } TimedNode getHead() { TimedNode index = head; // skip old entries long limit = scheduler.now(unit) - maxAge; TimedNode next = index.get(); while (next != null) { long ts = next.time; if (ts > limit) { break; } index = next; next = index.get(); } return index; } @Override @SuppressWarnings("unchecked") public T[] getValues(T[] array) { TimedNode h = getHead(); int s = size(h); if (s == 0) { if (array.length != 0) { array[0] = null; } } else { if (array.length < s) { array = (T[])Array.newInstance(array.getClass().getComponentType(), s); } int i = 0; while (i != s) { TimedNode next = h.get(); array[i] = (T)next.value; i++; h = next; } if (array.length > s) { array[s] = null; } } return array; } @Override @SuppressWarnings("unchecked") public void replay(ReplayDisposable rs) { if (rs.getAndIncrement() != 0) { return; } int missed = 1; final Observer a = rs.actual; TimedNode index = (TimedNode)rs.index; if (index == null) { index = getHead(); } for (;;) { if (rs.cancelled) { rs.index = null; return; } for (;;) { if (rs.cancelled) { rs.index = null; return; } TimedNode n = index.get(); if (n == null) { break; } Object o = n.value; if (done) { if (n.get() == null) { if (NotificationLite.isComplete(o)) { a.onComplete(); } else { a.onError(NotificationLite.getError(o)); } rs.index = null; rs.cancelled = true; return; } } a.onNext((T)o); index = n; } if (index.get() != null) { continue; } rs.index = index; missed = rs.addAndGet(-missed); if (missed == 0) { break; } } } @Override public int size() { return size(getHead()); } int size(TimedNode h) { int s = 0; while (s != Integer.MAX_VALUE) { TimedNode next = h.get(); if (next == null) { Object o = h.value; if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { s--; } break; } s++; h = next; } return s; } } }