From 085b18a341e569808543f7d612755cf99b39e88e Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 21 Jun 2019 13:37:06 +0200 Subject: [PATCH 1/2] 3.x: Add eager truncation to bounded replay() to avoid item retention --- src/main/java/io/reactivex/Flowable.java | 310 ++- src/main/java/io/reactivex/Observable.java | 272 +- .../flowable/FlowableInternalHelper.java | 52 +- .../operators/flowable/FlowableReplay.java | 56 +- .../observable/ObservableInternalHelper.java | 55 +- .../observable/ObservableReplay.java | 53 +- .../FlowableReplayEagerTruncateTest.java | 2311 +++++++++++++++++ .../flowable/FlowableReplayTest.java | 147 +- .../ObservableReplayEagerTruncateTest.java | 2050 +++++++++++++++ .../observable/ObservableReplayTest.java | 8 +- .../ParamValidationCheckerTest.java | 8 + 11 files changed, 5220 insertions(+), 102 deletions(-) create mode 100644 src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayEagerTruncateTest.java create mode 100644 src/test/java/io/reactivex/internal/operators/observable/ObservableReplayEagerTruncateTest.java diff --git a/src/main/java/io/reactivex/Flowable.java b/src/main/java/io/reactivex/Flowable.java index fdc421fc4d..a8b6a2796a 100644 --- a/src/main/java/io/reactivex/Flowable.java +++ b/src/main/java/io/reactivex/Flowable.java @@ -12912,6 +12912,7 @@ public final Flowable replay(Function, ? extends Publ * a {@link ConnectableFlowable} that shares a single subscription to the source Publisher * replaying no more than {@code bufferSize} items * @see ReactiveX operators documentation: Replay + * @see #replay(Function, int, boolean) */ @CheckReturnValue @NonNull @@ -12920,7 +12921,50 @@ public final Flowable replay(Function, ? extends Publ public final Flowable replay(Function, ? extends Publisher> selector, final int bufferSize) { ObjectHelper.requireNonNull(selector, "selector is null"); ObjectHelper.verifyPositive(bufferSize, "bufferSize"); - return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, bufferSize), selector); + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, bufferSize, false), selector); + } + + /** + * Returns a Flowable that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the source Publisher, + * replaying {@code bufferSize} notifications. + *

+ * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + *

+ * + *

+ *
Backpressure:
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Publisher sequence.
+ *
Scheduler:
+ *
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the type of items emitted by the resulting Publisher + * @param selector + * the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Publisher + * @param bufferSize + * the buffer size that limits the number of items the connectable Publisher can replay + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return a Flowable that emits items that are the results of invoking the selector on items emitted by + * a {@link ConnectableFlowable} that shares a single subscription to the source Publisher + * replaying no more than {@code bufferSize} items + * @see ReactiveX operators documentation: Replay + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable replay(Function, ? extends Publisher> selector, final int bufferSize, boolean eagerTruncate) { + ObjectHelper.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, bufferSize, eagerTruncate), selector); } /** @@ -13003,6 +13047,7 @@ public final Flowable replay(Function, ? extends Publ * @throws IllegalArgumentException * if {@code bufferSize} is less than zero * @see ReactiveX operators documentation: Replay + * @see #replay(Function, int, long, TimeUnit, Scheduler, boolean) */ @CheckReturnValue @NonNull @@ -13014,7 +13059,62 @@ public final Flowable replay(Function, ? extends Publ ObjectHelper.verifyPositive(bufferSize, "bufferSize"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); return FlowableReplay.multicastSelector( - FlowableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler), selector); + FlowableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler, false), selector); + } + + /** + * Returns a Flowable that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the source Publisher, + * replaying no more than {@code bufferSize} items that were emitted within a specified time window. + *

+ * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + *

+ * + *

+ *
Backpressure:
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Publisher sequence.
+ *
Scheduler:
+ *
You specify which {@link Scheduler} this operator will use.
+ *
+ * + * @param + * the type of items emitted by the resulting Publisher + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Publisher + * @param bufferSize + * the buffer size that limits the number of items the connectable Publisher can replay + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the Scheduler that is the time source for the window + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return a Flowable that emits items that are the results of invoking the selector on items emitted by + * a {@link ConnectableFlowable} that shares a single subscription to the source Publisher, and + * replays no more than {@code bufferSize} items that were emitted within the window defined by + * {@code time} + * @throws IllegalArgumentException + * if {@code bufferSize} is less than zero + * @see ReactiveX operators documentation: Replay + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable replay(Function, ? extends Publisher> selector, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + ObjectHelper.requireNonNull(selector, "selector is null"); + ObjectHelper.requireNonNull(unit, "unit is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + ObjectHelper.requireNonNull(scheduler, "scheduler is null"); + return FlowableReplay.multicastSelector( + FlowableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler, eagerTruncate), selector); } /** @@ -13057,7 +13157,7 @@ public final Flowable replay(final Function, ? extend ObjectHelper.requireNonNull(selector, "selector is null"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); ObjectHelper.verifyPositive(bufferSize, "bufferSize"); - return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, bufferSize), + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, bufferSize, false), FlowableInternalHelper.replayFunction(selector, scheduler) ); } @@ -13128,6 +13228,7 @@ public final Flowable replay(Function, ? extends Publ * a {@link ConnectableFlowable} that shares a single subscription to the source Publisher, * replaying all items that were emitted within the window defined by {@code time} * @see ReactiveX operators documentation: Replay + * @see #replay(Function, long, TimeUnit, Scheduler, boolean) */ @CheckReturnValue @NonNull @@ -13137,7 +13238,52 @@ public final Flowable replay(Function, ? extends Publ ObjectHelper.requireNonNull(selector, "selector is null"); ObjectHelper.requireNonNull(unit, "unit is null"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); - return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, time, unit, scheduler), selector); + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, time, unit, scheduler, false), selector); + } + + /** + * Returns a Flowable that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the source Publisher, + * replaying all items that were emitted within a specified time window. + *

+ * + *

+ *
Backpressure:
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Publisher sequence.
+ *
Scheduler:
+ *
You specify which {@link Scheduler} this operator will use.
+ *
+ * + * @param + * the type of items emitted by the resulting Publisher + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Publisher + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is the time source for the window + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return a Flowable that emits items that are the results of invoking the selector on items emitted by + * a {@link ConnectableFlowable} that shares a single subscription to the source Publisher, + * replaying all items that were emitted within the window defined by {@code time} + * @see ReactiveX operators documentation: Replay + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable replay(Function, ? extends Publisher> selector, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + ObjectHelper.requireNonNull(selector, "selector is null"); + ObjectHelper.requireNonNull(unit, "unit is null"); + ObjectHelper.requireNonNull(scheduler, "scheduler is null"); + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, time, unit, scheduler, eagerTruncate), selector); } /** @@ -13185,6 +13331,8 @@ public final Flowable replay(final Function, ? extend *

* Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than * {@code bufferSize} source emissions. + * To ensure no beyond-bufferSize items are referenced, + * use the {@link #replay(int, boolean)} overload with {@code eagerTruncate = true}. *

* *

@@ -13201,13 +13349,51 @@ public final Flowable replay(final Function, ? extend * @return a {@link ConnectableFlowable} that shares a single subscription to the source Publisher and * replays at most {@code bufferSize} items emitted by that Publisher * @see ReactiveX operators documentation: Replay + * @see #replay(int, boolean) */ @CheckReturnValue @BackpressureSupport(BackpressureKind.FULL) @SchedulerSupport(SchedulerSupport.NONE) public final ConnectableFlowable replay(final int bufferSize) { ObjectHelper.verifyPositive(bufferSize, "bufferSize"); - return FlowableReplay.create(this, bufferSize); + return FlowableReplay.create(this, bufferSize, false); + } + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the source Publisher that + * replays at most {@code bufferSize} items emitted by that Publisher. A Connectable Publisher resembles + * an ordinary Publisher, except that it does not begin emitting items when it is subscribed to, but only + * when its {@code connect} method is called. + *

+ * + *

+ * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no beyond-bufferSize items are referenced, set {@code eagerTruncate = true}. + *

+ *
Backpressure:
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Publisher sequence.
+ *
Scheduler:
+ *
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return a {@link ConnectableFlowable} that shares a single subscription to the source Publisher and + * replays at most {@code bufferSize} items emitted by that Publisher + * @see ReactiveX operators documentation: Replay + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final ConnectableFlowable replay(final int bufferSize, boolean eagerTruncate) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return FlowableReplay.create(this, bufferSize, eagerTruncate); } /** @@ -13216,10 +13402,12 @@ public final ConnectableFlowable replay(final int bufferSize) { * Publisher resembles an ordinary Publisher, except that it does not begin emitting items when it is * subscribed to, but only when its {@code connect} method is called. *

+ * + *

* Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than * {@code bufferSize} source emissions. - *

- * + * To ensure no out-of-date or beyond-bufferSize items are referenced, + * use the {@link #replay(int, long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. *

*
Backpressure:
*
This operator supports backpressure. Note that the upstream requests are determined by the child @@ -13239,6 +13427,7 @@ public final ConnectableFlowable replay(final int bufferSize) { * replays at most {@code bufferSize} items that were emitted during the window defined by * {@code time} * @see ReactiveX operators documentation: Replay + * @see #replay(int, long, TimeUnit, Scheduler, boolean) */ @CheckReturnValue @BackpressureSupport(BackpressureKind.FULL) @@ -13253,8 +13442,57 @@ public final ConnectableFlowable replay(int bufferSize, long time, TimeUnit u * Connectable Publisher resembles an ordinary Publisher, except that it does not begin emitting items * when it is subscribed to, but only when its {@code connect} method is called. *

+ * + *

* Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than * {@code bufferSize} source emissions. + * To ensure no out-of-date or beyond-bufferSize items are referenced, + * use the {@link #replay(int, long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. + *

+ *
Backpressure:
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Publisher sequence.
+ *
Scheduler:
+ *
You specify which {@link Scheduler} this operator will use.
+ *
+ * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is used as a time source for the window + * @return a {@link ConnectableFlowable} that shares a single subscription to the source Publisher and + * replays at most {@code bufferSize} items that were emitted during the window defined by + * {@code time} + * @throws IllegalArgumentException + * if {@code bufferSize} is less than zero + * @see ReactiveX operators documentation: Replay + * @see #replay(int, long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final ConnectableFlowable replay(final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + ObjectHelper.requireNonNull(unit, "unit is null"); + ObjectHelper.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return FlowableReplay.create(this, time, unit, scheduler, bufferSize, false); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the source Publisher and + * that replays a maximum of {@code bufferSize} items that are emitted within a specified time window. A + * Connectable Publisher resembles an ordinary Publisher, except that it does not begin emitting items + * when it is subscribed to, but only when its {@code connect} method is called. + *

+ * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. To ensure no out-of-date or beyond-bufferSize items + * are referenced, set {@code eagerTruncate = true}. *

* *

@@ -13274,22 +13512,26 @@ public final ConnectableFlowable replay(int bufferSize, long time, TimeUnit u * the time unit of {@code time} * @param scheduler * the scheduler that is used as a time source for the window + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention * @return a {@link ConnectableFlowable} that shares a single subscription to the source Publisher and * replays at most {@code bufferSize} items that were emitted during the window defined by * {@code time} * @throws IllegalArgumentException * if {@code bufferSize} is less than zero * @see ReactiveX operators documentation: Replay + * @since 3.0.0 */ @CheckReturnValue @BackpressureSupport(BackpressureKind.FULL) @SchedulerSupport(SchedulerSupport.CUSTOM) - public final ConnectableFlowable replay(final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler) { + public final ConnectableFlowable replay(final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { ObjectHelper.verifyPositive(bufferSize, "bufferSize"); ObjectHelper.requireNonNull(unit, "unit is null"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); ObjectHelper.verifyPositive(bufferSize, "bufferSize"); - return FlowableReplay.create(this, time, unit, scheduler, bufferSize); + return FlowableReplay.create(this, time, unit, scheduler, bufferSize, eagerTruncate); } /** @@ -13334,6 +13576,9 @@ public final ConnectableFlowable replay(final int bufferSize, final Scheduler * but only when its {@code connect} method is called. *

* + *

+ * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, use the {@link #replay(long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. *

*
Backpressure:
*
This operator supports backpressure. Note that the upstream requests are determined by the child @@ -13365,6 +13610,9 @@ public final ConnectableFlowable replay(long time, TimeUnit unit) { * but only when its {@code connect} method is called. *

* + *

+ * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, use the {@link #replay(long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. *

*
Backpressure:
*
This operator supports backpressure. Note that the upstream requests are determined by the child @@ -13383,6 +13631,7 @@ public final ConnectableFlowable replay(long time, TimeUnit unit) { * @return a {@link ConnectableFlowable} that shares a single subscription to the source Publisher and * replays the items that were emitted during the window defined by {@code time} * @see ReactiveX operators documentation: Replay + * @see #replay(long, TimeUnit, Scheduler, boolean) */ @CheckReturnValue @BackpressureSupport(BackpressureKind.FULL) @@ -13390,7 +13639,48 @@ public final ConnectableFlowable replay(long time, TimeUnit unit) { public final ConnectableFlowable replay(final long time, final TimeUnit unit, final Scheduler scheduler) { ObjectHelper.requireNonNull(unit, "unit is null"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); - return FlowableReplay.create(this, time, unit, scheduler); + return FlowableReplay.create(this, time, unit, scheduler, false); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the source Publisher and + * replays all items emitted by that Publisher within a specified time window. A Connectable Publisher + * resembles an ordinary Publisher, except that it does not begin emitting items when it is subscribed to, + * but only when its {@code connect} method is called. + *

+ * + *

+ * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, set {@code eagerTruncate = true}. + *

+ *
Backpressure:
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Publisher sequence.
+ *
Scheduler:
+ *
You specify which {@link Scheduler} this operator will use.
+ *
+ * + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the Scheduler that is the time source for the window + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return a {@link ConnectableFlowable} that shares a single subscription to the source Publisher and + * replays the items that were emitted during the window defined by {@code time} + * @see ReactiveX operators documentation: Replay + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final ConnectableFlowable replay(final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + ObjectHelper.requireNonNull(unit, "unit is null"); + ObjectHelper.requireNonNull(scheduler, "scheduler is null"); + return FlowableReplay.create(this, time, unit, scheduler, eagerTruncate); } /** diff --git a/src/main/java/io/reactivex/Observable.java b/src/main/java/io/reactivex/Observable.java index a090a511b8..03a8400cae 100644 --- a/src/main/java/io/reactivex/Observable.java +++ b/src/main/java/io/reactivex/Observable.java @@ -10581,13 +10581,51 @@ public final Observable replay(Function, ? extends * a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource * replaying no more than {@code bufferSize} items * @see ReactiveX operators documentation: Replay + * @see #replay(Function, int, boolean) */ @CheckReturnValue @SchedulerSupport(SchedulerSupport.NONE) public final Observable replay(Function, ? extends ObservableSource> selector, final int bufferSize) { ObjectHelper.requireNonNull(selector, "selector is null"); ObjectHelper.verifyPositive(bufferSize, "bufferSize"); - return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, bufferSize), selector); + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, bufferSize, false), selector); + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource, + * replaying {@code bufferSize} notifications. + *

+ * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + *

+ * + *

+ *
Scheduler:
+ *
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the type of items emitted by the resulting ObservableSource + * @param selector + * the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the ObservableSource + * @param bufferSize + * the buffer size that limits the number of items the connectable ObservableSource can replay + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource + * replaying no more than {@code bufferSize} items + * @see ReactiveX operators documentation: Replay + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public final Observable replay(Function, ? extends ObservableSource> selector, final int bufferSize, boolean eagerTruncate) { + ObjectHelper.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, bufferSize, eagerTruncate), selector); } /** @@ -10661,6 +10699,7 @@ public final Observable replay(Function, ? extends * @throws IllegalArgumentException * if {@code bufferSize} is less than zero * @see ReactiveX operators documentation: Replay + * @see #replay(Function, int, long, TimeUnit, Scheduler, boolean) */ @CheckReturnValue @SchedulerSupport(SchedulerSupport.CUSTOM) @@ -10670,7 +10709,55 @@ public final Observable replay(Function, ? extends ObjectHelper.requireNonNull(unit, "unit is null"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); return ObservableReplay.multicastSelector( - ObservableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler), selector); + ObservableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler, false), selector); + } + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource, + * replaying no more than {@code bufferSize} items that were emitted within a specified time window. + *

+ * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + *

+ * + *

+ *
Scheduler:
+ *
You specify which {@link Scheduler} this operator will use.
+ *
+ * + * @param + * the type of items emitted by the resulting ObservableSource + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the ObservableSource + * @param bufferSize + * the buffer size that limits the number of items the connectable ObservableSource can replay + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the Scheduler that is the time source for the window + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource, and + * replays no more than {@code bufferSize} items that were emitted within the window defined by + * {@code time} + * @throws IllegalArgumentException + * if {@code bufferSize} is less than zero + * @see ReactiveX operators documentation: Replay + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Observable replay(Function, ? extends ObservableSource> selector, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + ObjectHelper.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + ObjectHelper.requireNonNull(unit, "unit is null"); + ObjectHelper.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.multicastSelector( + ObservableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler, eagerTruncate), selector); } /** @@ -10707,7 +10794,7 @@ public final Observable replay(final Function, ? ex ObjectHelper.requireNonNull(selector, "selector is null"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); ObjectHelper.verifyPositive(bufferSize, "bufferSize"); - return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, bufferSize), + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, bufferSize, false), ObservableInternalHelper.replayFunction(selector, scheduler)); } @@ -10768,6 +10855,7 @@ public final Observable replay(Function, ? extends * a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource, * replaying all items that were emitted within the window defined by {@code time} * @see ReactiveX operators documentation: Replay + * @see #replay(Function, long, TimeUnit, Scheduler, boolean) */ @CheckReturnValue @SchedulerSupport(SchedulerSupport.CUSTOM) @@ -10775,7 +10863,46 @@ public final Observable replay(Function, ? extends ObjectHelper.requireNonNull(selector, "selector is null"); ObjectHelper.requireNonNull(unit, "unit is null"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); - return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, time, unit, scheduler), selector); + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, time, unit, scheduler, false), selector); + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource, + * replaying all items that were emitted within a specified time window. + *

+ * + *

+ *
Scheduler:
+ *
You specify which {@link Scheduler} this operator will use.
+ *
+ * + * @param + * the type of items emitted by the resulting ObservableSource + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the ObservableSource + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is the time source for the window + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource, + * replaying all items that were emitted within the window defined by {@code time} + * @see ReactiveX operators documentation: Replay + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Observable replay(Function, ? extends ObservableSource> selector, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + ObjectHelper.requireNonNull(selector, "selector is null"); + ObjectHelper.requireNonNull(unit, "unit is null"); + ObjectHelper.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, time, unit, scheduler, eagerTruncate), selector); } /** @@ -10815,10 +10942,42 @@ public final Observable replay(final Function, ? ex * an ordinary ObservableSource, except that it does not begin emitting items when it is subscribed to, but only * when its {@code connect} method is called. *

+ * + *

* Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than * {@code bufferSize} source emissions. + * To ensure no beyond-bufferSize items are referenced, + * use the {@link #replay(int, boolean)} overload with {@code eagerTruncate = true}. + *

+ *
Scheduler:
+ *
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @return a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource and + * replays at most {@code bufferSize} items emitted by that ObservableSource + * @see ReactiveX operators documentation: Replay + * @see #replay(int, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public final ConnectableObservable replay(final int bufferSize) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return ObservableReplay.create(this, bufferSize, false); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource that + * replays at most {@code bufferSize} items emitted by that ObservableSource. A Connectable ObservableSource resembles + * an ordinary ObservableSource, except that it does not begin emitting items when it is subscribed to, but only + * when its {@code connect} method is called. *

* + *

+ * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no beyond-bufferSize items are referenced, set {@code eagerTruncate = true}. *

*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
@@ -10826,15 +10985,18 @@ public final Observable replay(final Function, ? ex * * @param bufferSize * the buffer size that limits the number of items that can be replayed + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention * @return a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource and * replays at most {@code bufferSize} items emitted by that ObservableSource * @see ReactiveX operators documentation: Replay */ @CheckReturnValue @SchedulerSupport(SchedulerSupport.NONE) - public final ConnectableObservable replay(final int bufferSize) { + public final ConnectableObservable replay(final int bufferSize, boolean eagerTruncate) { ObjectHelper.verifyPositive(bufferSize, "bufferSize"); - return ObservableReplay.create(this, bufferSize); + return ObservableReplay.create(this, bufferSize, eagerTruncate); } /** @@ -10843,10 +11005,12 @@ public final ConnectableObservable replay(final int bufferSize) { * ObservableSource resembles an ordinary ObservableSource, except that it does not begin emitting items when it is * subscribed to, but only when its {@code connect} method is called. *

+ * + *

* Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than * {@code bufferSize} source emissions. - *

- * + * To ensure no out-of-date or beyond-bufferSize items are referenced, + * use the {@link #replay(int, long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. *

*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
@@ -10862,6 +11026,7 @@ public final ConnectableObservable replay(final int bufferSize) { * replays at most {@code bufferSize} items that were emitted during the window defined by * {@code time} * @see ReactiveX operators documentation: Replay + * @see #replay(int, long, TimeUnit, Scheduler, boolean) */ @CheckReturnValue @SchedulerSupport(SchedulerSupport.COMPUTATION) @@ -10877,6 +11042,8 @@ public final ConnectableObservable replay(int bufferSize, long time, TimeUnit *

* Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than * {@code bufferSize} source emissions. + * To ensure no out-of-date or beyond-bufferSize items are referenced, + * use the {@link #replay(int, long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. *

* *

@@ -10898,6 +11065,7 @@ public final ConnectableObservable replay(int bufferSize, long time, TimeUnit * @throws IllegalArgumentException * if {@code bufferSize} is less than zero * @see ReactiveX operators documentation: Replay + * @see #replay(int, long, TimeUnit, Scheduler, boolean) */ @CheckReturnValue @SchedulerSupport(SchedulerSupport.CUSTOM) @@ -10905,7 +11073,51 @@ public final ConnectableObservable replay(final int bufferSize, final long ti ObjectHelper.verifyPositive(bufferSize, "bufferSize"); ObjectHelper.requireNonNull(unit, "unit is null"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); - return ObservableReplay.create(this, time, unit, scheduler, bufferSize); + return ObservableReplay.create(this, time, unit, scheduler, bufferSize, false); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource and + * that replays a maximum of {@code bufferSize} items that are emitted within a specified time window. A + * Connectable ObservableSource resembles an ordinary ObservableSource, except that it does not begin emitting items + * when it is subscribed to, but only when its {@code connect} method is called. + *

+ * + *

+ * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no out-of-date or beyond-bufferSize items + * are referenced, set {@code eagerTruncate = true}. + *

+ *
Scheduler:
+ *
You specify which {@link Scheduler} this operator will use.
+ *
+ * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is used as a time source for the window + * @return a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource and + * replays at most {@code bufferSize} items that were emitted during the window defined by + * {@code time} + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @throws IllegalArgumentException + * if {@code bufferSize} is less than zero + * @see ReactiveX operators documentation: Replay + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final ConnectableObservable replay(final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + ObjectHelper.requireNonNull(unit, "unit is null"); + ObjectHelper.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.create(this, time, unit, scheduler, bufferSize, eagerTruncate); } /** @@ -10971,6 +11183,9 @@ public final ConnectableObservable replay(long time, TimeUnit unit) { * but only when its {@code connect} method is called. *

* + *

+ * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, use the {@link #replay(long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. *

*
Scheduler:
*
You specify which {@link Scheduler} this operator will use.
@@ -10985,13 +11200,50 @@ public final ConnectableObservable replay(long time, TimeUnit unit) { * @return a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource and * replays the items that were emitted during the window defined by {@code time} * @see ReactiveX operators documentation: Replay + * @see #replay(long, TimeUnit, Scheduler, boolean) */ @CheckReturnValue @SchedulerSupport(SchedulerSupport.CUSTOM) public final ConnectableObservable replay(final long time, final TimeUnit unit, final Scheduler scheduler) { ObjectHelper.requireNonNull(unit, "unit is null"); ObjectHelper.requireNonNull(scheduler, "scheduler is null"); - return ObservableReplay.create(this, time, unit, scheduler); + return ObservableReplay.create(this, time, unit, scheduler, false); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource and + * replays all items emitted by that ObservableSource within a specified time window. A Connectable ObservableSource + * resembles an ordinary ObservableSource, except that it does not begin emitting items when it is subscribed to, + * but only when its {@code connect} method is called. + *

+ * + *

+ * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, set {@code eagerTruncate = true}. + *

+ *
Scheduler:
+ *
You specify which {@link Scheduler} this operator will use.
+ *
+ * + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the Scheduler that is the time source for the window + * @param eagerTruncate + * if true, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return a {@link ConnectableObservable} that shares a single subscription to the source ObservableSource and + * replays the items that were emitted during the window defined by {@code time} + * @see ReactiveX operators documentation: Replay + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final ConnectableObservable replay(final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + ObjectHelper.requireNonNull(unit, "unit is null"); + ObjectHelper.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.create(this, time, unit, scheduler, eagerTruncate); } /** diff --git a/src/main/java/io/reactivex/internal/operators/flowable/FlowableInternalHelper.java b/src/main/java/io/reactivex/internal/operators/flowable/FlowableInternalHelper.java index 936bfbb0a0..8cb186da20 100644 --- a/src/main/java/io/reactivex/internal/operators/flowable/FlowableInternalHelper.java +++ b/src/main/java/io/reactivex/internal/operators/flowable/FlowableInternalHelper.java @@ -197,16 +197,16 @@ public static Supplier> replaySupplier(final Flowable return new ReplaySupplier(parent); } - public static Supplier> replaySupplier(final Flowable parent, final int bufferSize) { - return new BufferedReplaySupplier(parent, bufferSize); + public static Supplier> replaySupplier(final Flowable parent, final int bufferSize, boolean eagerTruncate) { + return new BufferedReplaySupplier(parent, bufferSize, eagerTruncate); } - public static Supplier> replaySupplier(final Flowable parent, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler) { - return new BufferedTimedReplay(parent, bufferSize, time, unit, scheduler); + public static Supplier> replaySupplier(final Flowable parent, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + return new BufferedTimedReplay(parent, bufferSize, time, unit, scheduler, eagerTruncate); } - public static Supplier> replaySupplier(final Flowable parent, final long time, final TimeUnit unit, final Scheduler scheduler) { - return new TimedReplay(parent, time, unit, scheduler); + public static Supplier> replaySupplier(final Flowable parent, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + return new TimedReplay(parent, time, unit, scheduler, eagerTruncate); } public static Function, Publisher> replayFunction(final Function, ? extends Publisher> selector, final Scheduler scheduler) { @@ -240,7 +240,8 @@ public static Function>, Publisher implements Supplier> { - private final Flowable parent; + + final Flowable parent; ReplaySupplier(Flowable parent) { this.parent = parent; @@ -253,38 +254,46 @@ public ConnectableFlowable get() { } static final class BufferedReplaySupplier implements Supplier> { - private final Flowable parent; - private final int bufferSize; - BufferedReplaySupplier(Flowable parent, int bufferSize) { + final Flowable parent; + + final int bufferSize; + + final boolean eagerTruncate; + + BufferedReplaySupplier(Flowable parent, int bufferSize, boolean eagerTruncate) { this.parent = parent; this.bufferSize = bufferSize; + this.eagerTruncate = eagerTruncate; } @Override public ConnectableFlowable get() { - return parent.replay(bufferSize); + return parent.replay(bufferSize, eagerTruncate); } } static final class BufferedTimedReplay implements Supplier> { - private final Flowable parent; - private final int bufferSize; - private final long time; - private final TimeUnit unit; - private final Scheduler scheduler; + final Flowable parent; + final int bufferSize; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + + final boolean eagerTruncate; - BufferedTimedReplay(Flowable parent, int bufferSize, long time, TimeUnit unit, Scheduler scheduler) { + BufferedTimedReplay(Flowable parent, int bufferSize, long time, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { this.parent = parent; this.bufferSize = bufferSize; this.time = time; this.unit = unit; this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; } @Override public ConnectableFlowable get() { - return parent.replay(bufferSize, time, unit, scheduler); + return parent.replay(bufferSize, time, unit, scheduler, eagerTruncate); } } @@ -294,16 +303,19 @@ static final class TimedReplay implements Supplier> { private final TimeUnit unit; private final Scheduler scheduler; - TimedReplay(Flowable parent, long time, TimeUnit unit, Scheduler scheduler) { + final boolean eagerTruncate; + + TimedReplay(Flowable parent, long time, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { this.parent = parent; this.time = time; this.unit = unit; this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; } @Override public ConnectableFlowable get() { - return parent.replay(time, unit, scheduler); + return parent.replay(time, unit, scheduler, eagerTruncate); } } diff --git a/src/main/java/io/reactivex/internal/operators/flowable/FlowableReplay.java b/src/main/java/io/reactivex/internal/operators/flowable/FlowableReplay.java index f1c10ae44b..a482f1fa01 100644 --- a/src/main/java/io/reactivex/internal/operators/flowable/FlowableReplay.java +++ b/src/main/java/io/reactivex/internal/operators/flowable/FlowableReplay.java @@ -89,14 +89,15 @@ public static ConnectableFlowable createFrom(Flowable source * @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) { + final int bufferSize, boolean eagerTruncate) { if (bufferSize == Integer.MAX_VALUE) { return createFrom(source); } - return create(source, new ReplayBufferTask(bufferSize)); + return create(source, new ReplayBufferSupplier(bufferSize, eagerTruncate)); } /** @@ -106,11 +107,12 @@ public static ConnectableFlowable create(Flowable source, * @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) { - return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE); + long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE, eagerTruncate); } /** @@ -121,11 +123,12 @@ public static ConnectableFlowable create(Flowable source, * @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) { - return create(source, new ScheduledReplayBufferTask(bufferSize, maxAge, unit, scheduler)); + final long maxAge, final TimeUnit unit, final Scheduler scheduler, final int bufferSize, boolean eagerTruncate) { + return create(source, new ScheduledReplayBufferSupplier(bufferSize, maxAge, unit, scheduler, eagerTruncate)); } /** @@ -731,12 +734,15 @@ static class BoundedReplayBuffer extends AtomicReference implements Rep private static final long serialVersionUID = 2346567790059478686L; + final boolean eagerTruncate; + Node tail; int size; long index; - BoundedReplayBuffer() { + BoundedReplayBuffer(boolean eagerTruncate) { + this.eagerTruncate = eagerTruncate; Node n = new Node(null, 0); tail = n; set(n); @@ -780,6 +786,15 @@ final void removeFirst() { * @param n the Node instance to set as first */ final void setFirst(Node n) { + if (eagerTruncate) { + Node m = new Node(null, n.index); + Node nextNode = n.get(); + if (nextNode == null) { + tail = m; + } + m.lazySet(nextNode); + n = m; + } set(n); } @@ -962,7 +977,8 @@ static final class SizeBoundReplayBuffer extends BoundedReplayBuffer { private static final long serialVersionUID = -5898283885385201806L; final int limit; - SizeBoundReplayBuffer(int limit) { + SizeBoundReplayBuffer(int limit, boolean eagerTruncate) { + super(eagerTruncate); this.limit = limit; } @@ -989,7 +1005,8 @@ static final class SizeAndTimeBoundReplayBuffer extends BoundedReplayBuffer s) { } } - static final class ReplayBufferTask implements Supplier> { - private final int bufferSize; + static final class ReplayBufferSupplier implements Supplier> { - ReplayBufferTask(int bufferSize) { + final int bufferSize; + + final boolean eagerTruncate; + + ReplayBufferSupplier(int bufferSize, boolean eagerTruncate) { this.bufferSize = bufferSize; + this.eagerTruncate = eagerTruncate; } @Override public ReplayBuffer get() { - return new SizeBoundReplayBuffer(bufferSize); + return new SizeBoundReplayBuffer(bufferSize, eagerTruncate); } } - static final class ScheduledReplayBufferTask implements Supplier> { + static final class ScheduledReplayBufferSupplier implements Supplier> { private final int bufferSize; private final long maxAge; private final TimeUnit unit; private final Scheduler scheduler; - ScheduledReplayBufferTask(int bufferSize, long maxAge, TimeUnit unit, Scheduler scheduler) { + final boolean eagerTruncate; + + ScheduledReplayBufferSupplier(int bufferSize, long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { this.bufferSize = bufferSize; this.maxAge = maxAge; this.unit = unit; this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; } @Override public ReplayBuffer get() { - return new SizeAndTimeBoundReplayBuffer(bufferSize, maxAge, unit, scheduler); + return new SizeAndTimeBoundReplayBuffer(bufferSize, maxAge, unit, scheduler, eagerTruncate); } } diff --git a/src/main/java/io/reactivex/internal/operators/observable/ObservableInternalHelper.java b/src/main/java/io/reactivex/internal/operators/observable/ObservableInternalHelper.java index d6d584f361..af9d5e4628 100644 --- a/src/main/java/io/reactivex/internal/operators/observable/ObservableInternalHelper.java +++ b/src/main/java/io/reactivex/internal/operators/observable/ObservableInternalHelper.java @@ -202,16 +202,16 @@ public static Supplier> replaySupplier(final Observ return new ReplaySupplier(parent); } - public static Supplier> replaySupplier(final Observable parent, final int bufferSize) { - return new BufferedReplaySupplier(parent, bufferSize); + public static Supplier> replaySupplier(final Observable parent, final int bufferSize, boolean eagerTruncate) { + return new BufferedReplaySupplier(parent, bufferSize, eagerTruncate); } - public static Supplier> replaySupplier(final Observable parent, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler) { - return new BufferedTimedReplaySupplier(parent, bufferSize, time, unit, scheduler); + public static Supplier> replaySupplier(final Observable parent, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + return new BufferedTimedReplaySupplier(parent, bufferSize, time, unit, scheduler, eagerTruncate); } - public static Supplier> replaySupplier(final Observable parent, final long time, final TimeUnit unit, final Scheduler scheduler) { - return new TimedReplayCallable(parent, time, unit, scheduler); + public static Supplier> replaySupplier(final Observable parent, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + return new TimedReplayCallable(parent, time, unit, scheduler, eagerTruncate); } public static Function, ObservableSource> replayFunction(final Function, ? extends ObservableSource> selector, final Scheduler scheduler) { @@ -250,57 +250,66 @@ public ConnectableObservable get() { } static final class BufferedReplaySupplier implements Supplier> { - private final Observable parent; - private final int bufferSize; + final Observable parent; + final int bufferSize; + + final boolean eagerTruncate; - BufferedReplaySupplier(Observable parent, int bufferSize) { + BufferedReplaySupplier(Observable parent, int bufferSize, boolean eagerTruncate) { this.parent = parent; this.bufferSize = bufferSize; + this.eagerTruncate = eagerTruncate; } @Override public ConnectableObservable get() { - return parent.replay(bufferSize); + return parent.replay(bufferSize, eagerTruncate); } } static final class BufferedTimedReplaySupplier implements Supplier> { - private final Observable parent; - private final int bufferSize; - private final long time; - private final TimeUnit unit; - private final Scheduler scheduler; + final Observable parent; + final int bufferSize; + final long time; + final TimeUnit unit; + final Scheduler scheduler; - BufferedTimedReplaySupplier(Observable parent, int bufferSize, long time, TimeUnit unit, Scheduler scheduler) { + final boolean eagerTruncate; + + BufferedTimedReplaySupplier(Observable parent, int bufferSize, long time, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { this.parent = parent; this.bufferSize = bufferSize; this.time = time; this.unit = unit; this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; } @Override public ConnectableObservable get() { - return parent.replay(bufferSize, time, unit, scheduler); + return parent.replay(bufferSize, time, unit, scheduler, eagerTruncate); } } static final class TimedReplayCallable implements Supplier> { - private final Observable parent; - private final long time; - private final TimeUnit unit; - private final Scheduler scheduler; + final Observable parent; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + + final boolean eagerTruncate; - TimedReplayCallable(Observable parent, long time, TimeUnit unit, Scheduler scheduler) { + TimedReplayCallable(Observable parent, long time, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { this.parent = parent; this.time = time; this.unit = unit; this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; } @Override public ConnectableObservable get() { - return parent.replay(time, unit, scheduler); + return parent.replay(time, unit, scheduler, eagerTruncate); } } diff --git a/src/main/java/io/reactivex/internal/operators/observable/ObservableReplay.java b/src/main/java/io/reactivex/internal/operators/observable/ObservableReplay.java index cffbeb26bf..c7a7bcfba8 100644 --- a/src/main/java/io/reactivex/internal/operators/observable/ObservableReplay.java +++ b/src/main/java/io/reactivex/internal/operators/observable/ObservableReplay.java @@ -92,14 +92,15 @@ public static ConnectableObservable createFrom(ObservableSource the value type * @param source the source ObservableSource 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 ConnectableObservable create(ObservableSource source, - final int bufferSize) { + final int bufferSize, boolean eagerTruncate) { if (bufferSize == Integer.MAX_VALUE) { return createFrom(source); } - return create(source, new ReplayBufferSupplier(bufferSize)); + return create(source, new ReplayBufferSupplier(bufferSize, eagerTruncate)); } /** @@ -109,11 +110,12 @@ public static ConnectableObservable create(ObservableSource source, * @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 ConnectableObservable create(ObservableSource source, - long maxAge, TimeUnit unit, Scheduler scheduler) { - return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE); + long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE, eagerTruncate); } /** @@ -124,11 +126,12 @@ public static ConnectableObservable create(ObservableSource source, * @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 ConnectableObservable instance */ public static ConnectableObservable create(ObservableSource source, - final long maxAge, final TimeUnit unit, final Scheduler scheduler, final int bufferSize) { - return create(source, new ScheduledReplaySupplier(bufferSize, maxAge, unit, scheduler)); + final long maxAge, final TimeUnit unit, final Scheduler scheduler, final int bufferSize, boolean eagerTruncate) { + return create(source, new ScheduledReplaySupplier(bufferSize, maxAge, unit, scheduler, eagerTruncate)); } /** @@ -595,7 +598,10 @@ abstract static class BoundedReplayBuffer extends AtomicReference imple Node tail; int size; - BoundedReplayBuffer() { + final boolean eagerTruncate; + + BoundedReplayBuffer(boolean eagerTruncate) { + this.eagerTruncate = eagerTruncate; Node n = new Node(null); tail = n; set(n); @@ -646,6 +652,15 @@ final void trimHead() { * @param n the Node instance to set as first */ final void setFirst(Node n) { + if (eagerTruncate) { + Node m = new Node(null); + Node nextNode = n.get(); + if (nextNode == null) { + tail = m; + } + m.lazySet(nextNode); + n = m; + } set(n); } @@ -787,7 +802,9 @@ static final class SizeBoundReplayBuffer extends BoundedReplayBuffer { private static final long serialVersionUID = -5898283885385201806L; final int limit; - SizeBoundReplayBuffer(int limit) { + + SizeBoundReplayBuffer(int limit, boolean eagerTruncate) { + super(eagerTruncate); this.limit = limit; } @@ -814,7 +831,8 @@ static final class SizeAndTimeBoundReplayBuffer extends BoundedReplayBuffer implements BufferSupplier { - private final int bufferSize; - ReplayBufferSupplier(int bufferSize) { + final int bufferSize; + + final boolean eagerTruncate; + + ReplayBufferSupplier(int bufferSize, boolean eagerTruncate) { this.bufferSize = bufferSize; + this.eagerTruncate = eagerTruncate; } @Override public ReplayBuffer call() { - return new SizeBoundReplayBuffer(bufferSize); + return new SizeBoundReplayBuffer(bufferSize, eagerTruncate); } } @@ -957,16 +979,19 @@ static final class ScheduledReplaySupplier implements BufferSupplier { private final TimeUnit unit; private final Scheduler scheduler; - ScheduledReplaySupplier(int bufferSize, long maxAge, TimeUnit unit, Scheduler scheduler) { + final boolean eagerTruncate; + + ScheduledReplaySupplier(int bufferSize, long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { this.bufferSize = bufferSize; this.maxAge = maxAge; this.unit = unit; this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; } @Override public ReplayBuffer call() { - return new SizeAndTimeBoundReplayBuffer(bufferSize, maxAge, unit, scheduler); + return new SizeAndTimeBoundReplayBuffer(bufferSize, maxAge, unit, scheduler, eagerTruncate); } } diff --git a/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayEagerTruncateTest.java b/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayEagerTruncateTest.java new file mode 100644 index 0000000000..ac8049387e --- /dev/null +++ b/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayEagerTruncateTest.java @@ -0,0 +1,2311 @@ +/** + * 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 static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.management.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.*; +import io.reactivex.Scheduler.Worker; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; +import io.reactivex.exceptions.TestException; +import io.reactivex.flowables.ConnectableFlowable; +import io.reactivex.functions.*; +import io.reactivex.internal.functions.Functions; +import io.reactivex.internal.fuseable.HasUpstreamPublisher; +import io.reactivex.internal.operators.flowable.FlowableReplay.*; +import io.reactivex.internal.subscriptions.BooleanSubscription; +import io.reactivex.plugins.RxJavaPlugins; +import io.reactivex.processors.PublishProcessor; +import io.reactivex.schedulers.*; +import io.reactivex.subscribers.TestSubscriber; +import io.reactivex.testsupport.*; + +public class FlowableReplayEagerTruncateTest { + @Test + public void bufferedReplay() { + PublishProcessor source = PublishProcessor.create(); + + ConnectableFlowable cf = source.replay(3, true); + cf.connect(); + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + source.onNext(4); + source.onComplete(); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedWindowReplay() { + PublishProcessor source = PublishProcessor.create(); + TestScheduler scheduler = new TestScheduler(); + ConnectableFlowable cf = source.replay(3, 100, TimeUnit.MILLISECONDS, scheduler, true); + cf.connect(); + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + source.onNext(4); + source.onNext(5); + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(4); + + inOrder.verify(subscriber1, times(1)).onNext(5); + + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(5); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplay() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor source = PublishProcessor.create(); + + ConnectableFlowable cf = source.replay(100, TimeUnit.MILLISECONDS, scheduler, true); + cf.connect(); + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + inOrder.verify(subscriber1, never()).onNext(3); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void replaySelector() { + final Function dbl = new Function() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function, Flowable> selector = new Function, Flowable>() { + + @Override + public Flowable apply(Flowable t1) { + return t1.map(dbl); + } + + }; + + PublishProcessor source = PublishProcessor.create(); + + Flowable co = source.replay(selector); + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(subscriber1, times(1)).onNext(8); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + } + + @Test + public void bufferedReplaySelector() { + + final Function dbl = new Function() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function, Flowable> selector = new Function, Flowable>() { + + @Override + public Flowable apply(Flowable t1) { + return t1.map(dbl); + } + + }; + + PublishProcessor source = PublishProcessor.create(); + + Flowable co = source.replay(selector, 3, true); + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(subscriber1, times(1)).onNext(8); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplaySelector() { + + final Function dbl = new Function() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function, Flowable> selector = new Function, Flowable>() { + + @Override + public Flowable apply(Flowable t1) { + return t1.map(dbl); + } + + }; + + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor source = PublishProcessor.create(); + + Flowable co = source.replay(selector, 100, TimeUnit.MILLISECONDS, scheduler, true); + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(6); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedReplayError() { + PublishProcessor source = PublishProcessor.create(); + + ConnectableFlowable cf = source.replay(3, true); + cf.connect(); + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + source.onNext(4); + source.onError(new RuntimeException("Forced failure")); + + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + + } + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + } + } + + @Test + public void windowedReplayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor source = PublishProcessor.create(); + + ConnectableFlowable cf = source.replay(100, TimeUnit.MILLISECONDS, scheduler, true); + cf.connect(); + + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onError(new RuntimeException("Forced failure")); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + + } + { + Subscriber subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + inOrder.verify(subscriber1, never()).onNext(3); + + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + } + } + + @Test + public void synchronousDisconnect() { + final AtomicInteger effectCounter = new AtomicInteger(); + Flowable source = Flowable.just(1, 2, 3, 4) + .doOnNext(new Consumer() { + @Override + public void accept(Integer v) { + effectCounter.incrementAndGet(); + System.out.println("Sideeffect #" + v); + } + }); + + Flowable result = source.replay( + new Function, Flowable>() { + @Override + public Flowable apply(Flowable f) { + return f.take(2); + } + }); + + for (int i = 1; i < 3; i++) { + effectCounter.set(0); + System.out.printf("- %d -%n", i); + result.subscribe(new Consumer() { + + @Override + public void accept(Integer t1) { + System.out.println(t1); + } + + }, new Consumer() { + + @Override + public void accept(Throwable t1) { + t1.printStackTrace(); + } + }, + new Action() { + @Override + public void run() { + System.out.println("Done"); + } + }); + assertEquals(2, effectCounter.get()); + } + } + + /* + * test the basic expectation of OperatorMulticast via replay + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_UnsubscribeSource() throws Throwable { + // setup mocks + Consumer sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + Subscriber spiedSubscriberBeforeConnect = TestHelper.mockSubscriber(); + Subscriber spiedSubscriberAfterConnect = TestHelper.mockSubscriber(); + + // Flowable under test + Flowable source = Flowable.just(1, 2); + + ConnectableFlowable replay = source + .doOnNext(sourceNext) + .doOnCancel(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + verify(spiedSubscriberBeforeConnect, times(2)).onSubscribe((Subscription)any()); + verify(spiedSubscriberAfterConnect, times(2)).onSubscribe((Subscription)any()); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceCompleted, times(1)).run(); + verifyObserverMock(spiedSubscriberBeforeConnect, 2, 4); + verifyObserverMock(spiedSubscriberAfterConnect, 2, 4); + + verify(sourceUnsubscribed, never()).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces declare throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribe() throws Throwable { + // setup mocks + Consumer sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Disposable mockSubscription = mock(Disposable.class); + Worker spiedWorker = workerSpy(mockSubscription); + Subscriber mockObserverBeforeConnect = TestHelper.mockSubscriber(); + Subscriber mockObserverAfterConnect = TestHelper.mockSubscriber(); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Flowable under test + ConnectableFlowable replay = Flowable.just(1, 2, 3) + .doOnNext(sourceNext) + .doOnCancel(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect, times(2)).onSubscribe((Subscription)any()); + verify(mockObserverAfterConnect, times(2)).onSubscribe((Subscription)any()); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceNext, times(1)).accept(3); + verify(sourceCompleted, times(1)).run(); + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Runnable)notNull()); + verifyObserverMock(mockObserverBeforeConnect, 2, 6); + verifyObserverMock(mockObserverAfterConnect, 2, 6); + + // FIXME publish calls cancel too + verify(spiedWorker, times(1)).dispose(); + verify(sourceUnsubscribed, never()).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces declare throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribeOnError() throws Throwable { + // setup mocks + Consumer sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Consumer sourceError = mock(Consumer.class); + Action sourceUnsubscribed = mock(Action.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Disposable mockSubscription = mock(Disposable.class); + Worker spiedWorker = workerSpy(mockSubscription); + Subscriber mockObserverBeforeConnect = TestHelper.mockSubscriber(); + Subscriber mockObserverAfterConnect = TestHelper.mockSubscriber(); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Flowable under test + Function mockFunc = mock(Function.class); + IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); + when(mockFunc.apply(1)).thenReturn(1); + when(mockFunc.apply(2)).thenThrow(illegalArgumentException); + ConnectableFlowable replay = Flowable.just(1, 2, 3).map(mockFunc) + .doOnNext(sourceNext) + .doOnCancel(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .doOnError(sourceError) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect, times(2)).onSubscribe((Subscription)any()); + verify(mockObserverAfterConnect, times(2)).onSubscribe((Subscription)any()); + + // verify interactions + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Runnable)notNull()); + verify(sourceNext, times(1)).accept(1); + verify(sourceError, times(1)).accept(illegalArgumentException); + verifyObserver(mockObserverBeforeConnect, 2, 2, illegalArgumentException); + verifyObserver(mockObserverAfterConnect, 2, 2, illegalArgumentException); + + // FIXME publish also calls cancel + verify(spiedWorker, times(1)).dispose(); + verify(sourceUnsubscribed, never()).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceError); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + private static void verifyObserverMock(Subscriber mock, int numSubscriptions, int numItemsExpected) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onComplete(); + verifyNoMoreInteractions(mock); + } + + private static void verifyObserver(Subscriber mock, int numSubscriptions, int numItemsExpected, Throwable error) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onError(error); + verifyNoMoreInteractions(mock); + } + + public static Worker workerSpy(final Disposable mockDisposable) { + return spy(new InprocessWorker(mockDisposable)); + } + + private static class InprocessWorker extends Worker { + private final Disposable mockDisposable; + public boolean unsubscribed; + + InprocessWorker(Disposable mockDisposable) { + this.mockDisposable = mockDisposable; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action) { + action.run(); + return mockDisposable; // this subscription is returned but discarded + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + action.run(); + return mockDisposable; + } + + @Override + public void dispose() { + unsubscribed = true; + } + + @Override + public boolean isDisposed() { + return unsubscribed; + } + } + + @Test + public void boundedReplayBuffer() { + BoundedReplayBuffer buf = new BoundedReplayBuffer(true); + buf.addLast(new Node(1, 0)); + buf.addLast(new Node(2, 1)); + buf.addLast(new Node(3, 2)); + buf.addLast(new Node(4, 3)); + buf.addLast(new Node(5, 4)); + + List values = new ArrayList(); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), values); + + buf.removeSome(2); + buf.removeFirst(); + buf.removeSome(2); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + buf.addLast(new Node(5, 5)); + buf.addLast(new Node(6, 6)); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(5, 6), values); + } + + @Test + public void timedAndSizedTruncation() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test, true); + List values = new ArrayList(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertTrue(buf.hasCompleted()); + } + + @Test + public void backpressure() { + final AtomicLong requested = new AtomicLong(); + Flowable source = Flowable.range(1, 1000) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requested.addAndGet(t); + } + }); + ConnectableFlowable cf = source.replay(); + + TestSubscriberEx ts1 = new TestSubscriberEx(10L); + TestSubscriberEx ts2 = new TestSubscriberEx(90L); + + cf.subscribe(ts1); + cf.subscribe(ts2); + + ts2.request(10); + + cf.connect(); + + ts1.assertValueCount(10); + ts1.assertNotTerminated(); + + ts2.assertValueCount(100); + ts2.assertNotTerminated(); + + Assert.assertEquals(100, requested.get()); + } + + @Test + public void backpressureBounded() { + final AtomicLong requested = new AtomicLong(); + Flowable source = Flowable.range(1, 1000) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requested.addAndGet(t); + } + }); + ConnectableFlowable cf = source.replay(50, true); + + TestSubscriberEx ts1 = new TestSubscriberEx(10L); + TestSubscriberEx ts2 = new TestSubscriberEx(90L); + + cf.subscribe(ts1); + cf.subscribe(ts2); + + ts2.request(10); + + cf.connect(); + + ts1.assertValueCount(10); + ts1.assertNotTerminated(); + + ts2.assertValueCount(100); + ts2.assertNotTerminated(); + + Assert.assertEquals(100, requested.get()); + } + + @Test + public void coldReplayNoBackpressure() { + Flowable source = Flowable.range(0, 1000).replay().autoConnect(); + + TestSubscriberEx ts = new TestSubscriberEx(); + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + List onNextEvents = ts.values(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + + @Test + public void coldReplayBackpressure() { + Flowable source = Flowable.range(0, 1000).replay().autoConnect(); + + TestSubscriber ts = new TestSubscriber(0L); + ts.request(10); + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertNotComplete(); + List onNextEvents = ts.values(); + assertEquals(10, onNextEvents.size()); + + for (int i = 0; i < 10; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + + ts.cancel(); + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Flowable f = Flowable.unsafeCreate(new Publisher() { + + @Override + public void subscribe(final Subscriber subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published observable being executed"); + subscriber.onNext("one"); + subscriber.onComplete(); + } + }).start(); + } + }).replay().autoConnect(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + f.subscribe(new Consumer() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + f.subscribe(new Consumer() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void unsubscribeSource() throws Throwable { + Action unsubscribe = mock(Action.class); + Flowable f = Flowable.just(1).doOnCancel(unsubscribe).replay().autoConnect(); + f.subscribe(); + f.subscribe(); + f.subscribe(); + verify(unsubscribe, never()).run(); + } + + @Test + public void take() { + TestSubscriberEx ts = new TestSubscriberEx(); + + Flowable cached = Flowable.range(1, 100).replay().autoConnect(); + cached.take(10).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void async() { + Flowable source = Flowable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestSubscriberEx ts1 = new TestSubscriberEx(); + + Flowable cached = source.replay().autoConnect(); + + cached.observeOn(Schedulers.computation()).subscribe(ts1); + + ts1.awaitDone(2, TimeUnit.SECONDS); + ts1.assertNoErrors(); + ts1.assertTerminated(); + assertEquals(10000, ts1.values().size()); + + TestSubscriberEx ts2 = new TestSubscriberEx(); + cached.observeOn(Schedulers.computation()).subscribe(ts2); + + ts2.awaitDone(2, TimeUnit.SECONDS); + ts2.assertNoErrors(); + ts2.assertTerminated(); + assertEquals(10000, ts2.values().size()); + } + } + + @Test + public void asyncComeAndGo() { + Flowable source = Flowable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + Flowable cached = source.replay().autoConnect(); + + Flowable output = cached.observeOn(Schedulers.computation(), false, 1024); + + List> list = new ArrayList>(100); + for (int i = 0; i < 100; i++) { + TestSubscriberEx ts = new TestSubscriberEx(); + list.add(ts); + output.skip(i * 10).take(10).subscribe(ts); + } + + List expected = new ArrayList(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestSubscriberEx ts : list) { + ts.awaitDone(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminated(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + ts.assertValueSequence(expected); + + j++; + } + } + + @Test + public void noMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Flowable firehose = Flowable.unsafeCreate(new Publisher() { + @Override + public void subscribe(Subscriber t) { + t.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onComplete(); + } + }); + + TestSubscriberEx ts = new TestSubscriberEx(); + firehose.replay().autoConnect().observeOn(Schedulers.computation()).takeLast(100).subscribe(ts); + + ts.awaitDone(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminated(); + + assertEquals(100, ts.values().size()); + } + + @Test + public void valuesAndThenError() { + Flowable source = Flowable.range(1, 10) + .concatWith(Flowable.error(new TestException())) + .replay().autoConnect(); + + TestSubscriberEx ts = new TestSubscriberEx(); + source.subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts.assertNotComplete(); + Assert.assertEquals(1, ts.errors().size()); + + TestSubscriberEx ts2 = new TestSubscriberEx(); + source.subscribe(ts2); + + ts2.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts2.assertNotComplete(); + Assert.assertEquals(1, ts2.errors().size()); + } + + @Test + public void unsafeChildThrows() { + final AtomicInteger count = new AtomicInteger(); + + Flowable source = Flowable.range(1, 100) + .doOnNext(new Consumer() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }) + .replay().autoConnect(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.subscribe(ts); + + Assert.assertEquals(100, count.get()); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test + public void unboundedLeavesEarly() { + PublishProcessor source = PublishProcessor.create(); + + final List requests = new ArrayList(); + + Flowable out = source + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requests.add(t); + } + }).replay().autoConnect(); + + TestSubscriber ts1 = new TestSubscriber(5L); + TestSubscriber ts2 = new TestSubscriber(10L); + + out.subscribe(ts1); + out.subscribe(ts2); + ts2.cancel(); + + Assert.assertEquals(Arrays.asList(5L, 5L), requests); + } + + @Test + public void subscribersComeAndGoAtRequestBoundaries() { + ConnectableFlowable source = Flowable.range(1, 10).replay(1, true); + source.connect(); + + TestSubscriber ts1 = new TestSubscriber(2L); + + source.subscribe(ts1); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.cancel(); + + TestSubscriber ts2 = new TestSubscriber(2L); + + source.subscribe(ts2); + + ts2.assertValues(2, 3); + ts2.assertNoErrors(); + ts2.cancel(); + + TestSubscriber ts21 = new TestSubscriber(1L); + + source.subscribe(ts21); + + ts21.assertValues(3); + ts21.assertNoErrors(); + ts21.cancel(); + + TestSubscriber ts22 = new TestSubscriber(1L); + + source.subscribe(ts22); + + ts22.assertValues(3); + ts22.assertNoErrors(); + ts22.cancel(); + + TestSubscriber ts3 = new TestSubscriber(); + + source.subscribe(ts3); + + ts3.assertNoErrors(); + System.out.println(ts3.values()); + ts3.assertValues(3, 4, 5, 6, 7, 8, 9, 10); + ts3.assertComplete(); + } + + @Test + public void subscribersComeAndGoAtRequestBoundaries2() { + ConnectableFlowable source = Flowable.range(1, 10).replay(2, true); + source.connect(); + + TestSubscriber ts1 = new TestSubscriber(2L); + + source.subscribe(ts1); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.cancel(); + + TestSubscriber ts11 = new TestSubscriber(2L); + + source.subscribe(ts11); + + ts11.assertValues(1, 2); + ts11.assertNoErrors(); + ts11.cancel(); + + TestSubscriber ts2 = new TestSubscriber(3L); + + source.subscribe(ts2); + + ts2.assertValues(1, 2, 3); + ts2.assertNoErrors(); + ts2.cancel(); + + TestSubscriber ts21 = new TestSubscriber(1L); + + source.subscribe(ts21); + + ts21.assertValues(2); + ts21.assertNoErrors(); + ts21.cancel(); + + TestSubscriber ts22 = new TestSubscriber(1L); + + source.subscribe(ts22); + + ts22.assertValues(2); + ts22.assertNoErrors(); + ts22.cancel(); + + TestSubscriber ts3 = new TestSubscriber(); + + source.subscribe(ts3); + + ts3.assertNoErrors(); + System.out.println(ts3.values()); + ts3.assertValues(2, 3, 4, 5, 6, 7, 8, 9, 10); + ts3.assertComplete(); + } + + @Test + public void replayScheduler() { + + Flowable.just(1).replay(Schedulers.computation()) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replayTime() { + Flowable.just(1).replay(1, TimeUnit.MINUTES, Schedulers.computation(), true) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySizeScheduler() { + + Flowable.just(1).replay(1, Schedulers.computation()) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySizeAndTime() { + Flowable.just(1).replay(1, 1, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorSizeScheduler() { + Flowable.just(1).replay(Functions.>identity(), 1, Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorScheduler() { + Flowable.just(1).replay(Functions.>identity(), Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorTime() { + Flowable.just(1).replay(Functions.>identity(), 1, TimeUnit.MINUTES, Schedulers.computation(), true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void source() { + Flowable source = Flowable.range(1, 3); + + assertSame(source, (((HasUpstreamPublisher)source.replay())).source()); + } + + @Test + public void connectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable cf = Flowable.range(1, 3).replay(); + + Runnable r = new Runnable() { + @Override + public void run() { + cf.connect(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable cf = Flowable.range(1, 3).replay(); + + final TestSubscriber ts1 = new TestSubscriber(); + final TestSubscriber ts2 = new TestSubscriber(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable cf = Flowable.range(1, 3).replay(); + + final TestSubscriber ts1 = new TestSubscriber(); + final TestSubscriber ts2 = new TestSubscriber(); + + cf.subscribe(ts1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelOnArrival() { + Flowable.range(1, 2) + .replay(Integer.MAX_VALUE, true) + .autoConnect() + .test(Long.MAX_VALUE, true) + .assertEmpty(); + } + + @Test + public void cancelOnArrival2() { + ConnectableFlowable cf = PublishProcessor.create() + .replay(Integer.MAX_VALUE, true); + + cf.test(); + + cf + .autoConnect() + .test(Long.MAX_VALUE, true) + .assertEmpty(); + } + + @Test + public void connectConsumerThrows() { + ConnectableFlowable cf = Flowable.range(1, 2) + .replay(); + + try { + cf.connect(new Consumer() { + @Override + public void accept(Disposable t) throws Exception { + throw new TestException(); + } + }); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + cf.test().assertEmpty().cancel(); + + cf.connect(); + + cf.test().assertResult(1, 2); + } + + @Test + public void badSource() { + List errors = TestHelper.trackPluginErrors(); + try { + new Flowable() { + @Override + protected void subscribeActual(Subscriber subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + subscriber.onNext(1); + subscriber.onError(new TestException("Second")); + subscriber.onComplete(); + } + }.replay() + .autoConnect() + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor pp = PublishProcessor.create(); + + final ConnectableFlowable cf = pp.replay(); + + final TestSubscriber ts1 = new TestSubscriber(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + pp.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor pp = PublishProcessor.create(); + + final ConnectableFlowable cf = pp.replay(); + + final TestSubscriber ts1 = new TestSubscriber(); + + cf.subscribe(ts1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + pp.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeReplayRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable cf = Flowable.range(1, 1000).replay(); + + final TestSubscriber ts1 = new TestSubscriber(); + + cf.connect(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void reentrantOnNext() { + final PublishProcessor pp = PublishProcessor.create(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + pp.onComplete(); + } + super.onNext(t); + } + }; + + pp.replay().autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertResult(1, 2); + } + + @Test + public void reentrantOnNextBound() { + final PublishProcessor pp = PublishProcessor.create(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + pp.onComplete(); + } + super.onNext(t); + } + }; + + pp.replay(10, true).autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertResult(1, 2); + } + + @Test + public void reentrantOnNextCancel() { + final PublishProcessor pp = PublishProcessor.create(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + cancel(); + } + super.onNext(t); + } + }; + + pp.replay().autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertValues(1); + } + + @Test + public void reentrantOnNextCancelBounded() { + final PublishProcessor pp = PublishProcessor.create(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + cancel(); + } + super.onNext(t); + } + }; + + pp.replay(10, true).autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertValues(1); + } + + @Test + public void replayMaxInt() { + Flowable.range(1, 2) + .replay(Integer.MAX_VALUE, true) + .autoConnect() + .test() + .assertResult(1, 2); + } + + @Test + public void timedAndSizedTruncationError() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test, true); + + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + List values = new ArrayList(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.error(new TestException()); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertTrue(buf.hasError()); + } + + @Test + public void sizedTruncation() { + SizeBoundReplayBuffer buf = new SizeBoundReplayBuffer(2, true); + List values = new ArrayList(); + + buf.next(1); + buf.next(2); + buf.collect(values); + Assert.assertEquals(Arrays.asList(1, 2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + Assert.assertFalse(buf.hasCompleted()); + + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + + Assert.assertEquals(3, buf.size); + Assert.assertTrue(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + } + + @Test + public void delayedUpstreamOnSubscribe() { + final Subscriber[] sub = { null }; + + new Flowable() { + @Override + protected void subscribeActual(Subscriber s) { + sub[0] = s; + } + } + .replay() + .connect() + .dispose(); + + BooleanSubscription bs = new BooleanSubscription(); + + sub[0].onSubscribe(bs); + + assertTrue(bs.isCancelled()); + } + + @Test + public void timedNoOutdatedData() { + TestScheduler scheduler = new TestScheduler(); + + Flowable source = Flowable.just(1) + .replay(2, TimeUnit.SECONDS, scheduler, true) + .autoConnect(); + + source.test().assertResult(1); + + source.test().assertResult(1); + + scheduler.advanceTimeBy(3, TimeUnit.SECONDS); + + source.test().assertResult(); + } + + @Test + public void replaySelectorReturnsNull() { + Flowable.just(1) + .replay(new Function, Publisher>() { + @Override + public Publisher apply(Flowable v) throws Exception { + return null; + } + }, Schedulers.trampoline()) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The selector returned a null Publisher"); + } + + @Test + public void multicastSelectorCallableConnectableCrash() { + FlowableReplay.multicastSelector(new Supplier>() { + @Override + public ConnectableFlowable get() throws Exception { + throw new TestException(); + } + }, Functions.>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported( + Flowable.never() + .replay() + ); + } + + @Test + public void noHeadRetentionCompleteSize() { + PublishProcessor source = PublishProcessor.create(); + + FlowableReplay co = (FlowableReplay)source + .replay(1, true); + + // the backpressure coordination would not accept items from source otherwise + co.test(); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorSize() { + PublishProcessor source = PublishProcessor.create(); + + FlowableReplay co = (FlowableReplay)source + .replay(1, true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionSize() { + PublishProcessor source = PublishProcessor.create(); + + FlowableReplay co = (FlowableReplay)source + .replay(1, true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + + assertNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionCompleteTime() { + PublishProcessor source = PublishProcessor.create(); + + FlowableReplay co = (FlowableReplay)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation(), true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorTime() { + PublishProcessor source = PublishProcessor.create(); + + FlowableReplay co = (FlowableReplay)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation(), true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionTime() { + TestScheduler sch = new TestScheduler(); + + PublishProcessor source = PublishProcessor.create(); + + FlowableReplay co = (FlowableReplay)source + .replay(1, TimeUnit.MILLISECONDS, sch, true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + + sch.advanceTimeBy(2, TimeUnit.MILLISECONDS); + + source.onNext(2); + + assertNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test(expected = TestException.class) + public void createBufferFactoryCrash() { + FlowableReplay.create(Flowable.just(1), new Supplier>() { + @Override + public ReplayBuffer get() throws Exception { + throw new TestException(); + } + }) + .connect(); + } + + @Test + public void createBufferFactoryCrashOnSubscribe() { + FlowableReplay.create(Flowable.just(1), new Supplier>() { + @Override + public ReplayBuffer get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void currentDisposedWhenConnecting() { + FlowableReplay fr = (FlowableReplay)FlowableReplay.create(Flowable.never(), 16, true); + fr.connect(); + + fr.current.get().dispose(); + assertTrue(fr.current.get().isDisposed()); + + fr.connect(); + + assertFalse(fr.current.get().isDisposed()); + } + + @Test + public void noBoundedRetentionViaThreadLocal() throws Exception { + Flowable source = Flowable.range(1, 200) + .map(new Function() { + @Override + public byte[] apply(Integer v) throws Exception { + return new byte[1024 * 1024]; + } + }) + .replay(new Function, Publisher>() { + @Override + public Publisher apply(final Flowable f) throws Exception { + return f.take(1) + .concatMap(new Function>() { + @Override + public Publisher apply(byte[] v) throws Exception { + return f; + } + }); + } + }, 1, true) + .takeLast(1) + ; + + System.out.println("Bounded Replay Leak check: Wait before GC"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC"); + System.gc(); + + Thread.sleep(500); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); + long initial = memHeap.getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + final AtomicLong after = new AtomicLong(); + + source.subscribe(new Consumer() { + @Override + public void accept(byte[] v) throws Exception { + System.out.println("Bounded Replay Leak check: Wait before GC 2"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC 2"); + System.gc(); + + Thread.sleep(500); + + after.set(memoryMXBean.getHeapMemoryUsage().getUsed()); + } + }); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after.get() / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after.get()) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after.get() / 1024.0 / 1024.0); + } + } + + @Test + public void sizeBoundEagerTruncate() throws Exception { + + PublishProcessor pp = PublishProcessor.create(); + + ConnectableFlowable cf = pp.replay(1, true); + + TestSubscriber ts = cf.test(); + + cf.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeBoundEagerTruncate() throws Exception { + + PublishProcessor pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableFlowable cf = pp.replay(1, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber ts = cf.test(); + + cf.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeBoundEagerTruncate() throws Exception { + + PublishProcessor pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableFlowable cf = pp.replay(1, 5, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber ts = cf.test(); + + cf.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void sizeBoundSelectorEagerTruncate() throws Exception { + + PublishProcessor pp = PublishProcessor.create(); + + Flowable cf = pp.replay(Functions.>identity(), 1, true); + + TestSubscriber ts = cf.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeBoundSelectorEagerTruncate() throws Exception { + + PublishProcessor pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + Flowable cf = pp.replay(Functions.>identity(), 1, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber ts = cf.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeBoundSelectorEagerTruncate() throws Exception { + + PublishProcessor pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + Flowable cf = pp.replay(Functions.>identity(), 1, 5, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber ts = cf.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } +} diff --git a/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayTest.java b/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayTest.java index 62c43639da..71e60eac44 100644 --- a/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayTest.java +++ b/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayTest.java @@ -732,7 +732,7 @@ public boolean isDisposed() { @Test public void boundedReplayBuffer() { - BoundedReplayBuffer buf = new BoundedReplayBuffer(); + BoundedReplayBuffer buf = new BoundedReplayBuffer(false); buf.addLast(new Node(1, 0)); buf.addLast(new Node(2, 1)); buf.addLast(new Node(3, 2)); @@ -763,7 +763,7 @@ public void boundedReplayBuffer() { @Test public void timedAndSizedTruncation() { TestScheduler test = new TestScheduler(); - SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test, false); List values = new ArrayList(); buf.next(1); @@ -1629,7 +1629,7 @@ public void replayMaxInt() { @Test public void timedAndSizedTruncationError() { TestScheduler test = new TestScheduler(); - SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test, false); Assert.assertFalse(buf.hasCompleted()); Assert.assertFalse(buf.hasError()); @@ -1672,7 +1672,7 @@ public void timedAndSizedTruncationError() { @Test public void sizedTruncation() { - SizeBoundReplayBuffer buf = new SizeBoundReplayBuffer(2); + SizeBoundReplayBuffer buf = new SizeBoundReplayBuffer(2, false); List values = new ArrayList(); buf.next(1); @@ -1968,7 +1968,7 @@ public ReplayBuffer get() throws Exception { @Test public void currentDisposedWhenConnecting() { - FlowableReplay fr = (FlowableReplay)FlowableReplay.create(Flowable.never(), 16); + FlowableReplay fr = (FlowableReplay)FlowableReplay.create(Flowable.never(), 16, false); fr.connect(); fr.current.get().dispose(); @@ -2041,4 +2041,141 @@ public void accept(byte[] v) throws Exception { + " -> " + after.get() / 1024.0 / 1024.0); } } + + @Test + public void sizeBoundEagerTruncate() throws Exception { + + PublishProcessor pp = PublishProcessor.create(); + + ConnectableFlowable cf = pp.replay(1, true); + + TestSubscriber ts = cf.test(); + + cf.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeBoundEagerTruncate() throws Exception { + + PublishProcessor pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableFlowable cf = pp.replay(1, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber ts = cf.test(); + + cf.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeBoundEagerTruncate() throws Exception { + + PublishProcessor pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableFlowable cf = pp.replay(1, 5, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber ts = cf.test(); + + cf.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } } diff --git a/src/test/java/io/reactivex/internal/operators/observable/ObservableReplayEagerTruncateTest.java b/src/test/java/io/reactivex/internal/operators/observable/ObservableReplayEagerTruncateTest.java new file mode 100644 index 0000000000..2922bf1d07 --- /dev/null +++ b/src/test/java/io/reactivex/internal/operators/observable/ObservableReplayEagerTruncateTest.java @@ -0,0 +1,2050 @@ +/** + * 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.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.management.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.*; +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.Scheduler.Worker; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.*; +import io.reactivex.exceptions.TestException; +import io.reactivex.functions.*; +import io.reactivex.internal.functions.Functions; +import io.reactivex.internal.fuseable.HasUpstreamObservableSource; +import io.reactivex.internal.operators.observable.ObservableReplay.*; +import io.reactivex.observables.ConnectableObservable; +import io.reactivex.observers.TestObserver; +import io.reactivex.plugins.RxJavaPlugins; +import io.reactivex.schedulers.*; +import io.reactivex.subjects.PublishSubject; +import io.reactivex.testsupport.*; + +public class ObservableReplayEagerTruncateTest { + @Test + public void bufferedReplay() { + PublishSubject source = PublishSubject.create(); + + ConnectableObservable co = source.replay(3, true); + co.connect(); + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onComplete(); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedWindowReplay() { + PublishSubject source = PublishSubject.create(); + TestScheduler scheduler = new TestScheduler(); + ConnectableObservable co = source.replay(3, 100, TimeUnit.MILLISECONDS, scheduler, true); + co.connect(); + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onNext(5); + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(4); + + inOrder.verify(observer1, times(1)).onNext(5); + + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(5); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplay() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject source = PublishSubject.create(); + + ConnectableObservable co = source.replay(100, TimeUnit.MILLISECONDS, scheduler, true); + co.connect(); + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + inOrder.verify(observer1, never()).onNext(3); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void replaySelector() { + final Function dbl = new Function() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function, Observable> selector = new Function, Observable>() { + + @Override + public Observable apply(Observable t1) { + return t1.map(dbl); + } + + }; + + PublishSubject source = PublishSubject.create(); + + Observable co = source.replay(selector); + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(observer1, times(1)).onNext(8); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + } + + @Test + public void bufferedReplaySelector() { + + final Function dbl = new Function() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function, Observable> selector = new Function, Observable>() { + + @Override + public Observable apply(Observable t1) { + return t1.map(dbl); + } + + }; + + PublishSubject source = PublishSubject.create(); + + Observable co = source.replay(selector, 3); + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(observer1, times(1)).onNext(8); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplaySelector() { + + final Function dbl = new Function() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function, Observable> selector = new Function, Observable>() { + + @Override + public Observable apply(Observable t1) { + return t1.map(dbl); + } + + }; + + TestScheduler scheduler = new TestScheduler(); + + PublishSubject source = PublishSubject.create(); + + Observable co = source.replay(selector, 100, TimeUnit.MILLISECONDS, scheduler); + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(6); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedReplayError() { + PublishSubject source = PublishSubject.create(); + + ConnectableObservable co = source.replay(3, true); + co.connect(); + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onError(new RuntimeException("Forced failure")); + + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + + } + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + } + } + + @Test + public void windowedReplayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject source = PublishSubject.create(); + + ConnectableObservable co = source.replay(100, TimeUnit.MILLISECONDS, scheduler, true); + co.connect(); + + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onError(new RuntimeException("Forced failure")); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + + } + { + Observer observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + inOrder.verify(observer1, never()).onNext(3); + + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + } + } + + @Test + public void synchronousDisconnect() { + final AtomicInteger effectCounter = new AtomicInteger(); + Observable source = Observable.just(1, 2, 3, 4) + .doOnNext(new Consumer() { + @Override + public void accept(Integer v) { + effectCounter.incrementAndGet(); + System.out.println("Sideeffect #" + v); + } + }); + + Observable result = source.replay( + new Function, Observable>() { + @Override + public Observable apply(Observable o) { + return o.take(2); + } + }); + + for (int i = 1; i < 3; i++) { + effectCounter.set(0); + System.out.printf("- %d -%n", i); + result.subscribe(new Consumer() { + + @Override + public void accept(Integer t1) { + System.out.println(t1); + } + + }, new Consumer() { + + @Override + public void accept(Throwable t1) { + t1.printStackTrace(); + } + }, + new Action() { + @Override + public void run() { + System.out.println("Done"); + } + }); + assertEquals(2, effectCounter.get()); + } + } + + /* + * test the basic expectation of OperatorMulticast via replay + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_UnsubscribeSource() throws Throwable { + // setup mocks + Consumer sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + Observer spiedSubscriberBeforeConnect = TestHelper.mockObserver(); + Observer spiedSubscriberAfterConnect = TestHelper.mockObserver(); + + // Observable under test + Observable source = Observable.just(1, 2); + + ConnectableObservable replay = source + .doOnNext(sourceNext) + .doOnDispose(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + verify(spiedSubscriberBeforeConnect, times(2)).onSubscribe((Disposable)any()); + verify(spiedSubscriberAfterConnect, times(2)).onSubscribe((Disposable)any()); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceCompleted, times(1)).run(); + verifyObserverMock(spiedSubscriberBeforeConnect, 2, 4); + verifyObserverMock(spiedSubscriberAfterConnect, 2, 4); + +// verify(sourceUnsubscribed, times(1)).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces are declared with throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribe() throws Throwable { + // setup mocks + Consumer sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + final TestScheduler mockScheduler = new TestScheduler(); + + Observer mockObserverBeforeConnect = TestHelper.mockObserver(); + Observer mockObserverAfterConnect = TestHelper.mockObserver(); + + // Observable under test + ConnectableObservable replay = Observable.just(1, 2, 3) + .doOnNext(sourceNext) + .doOnDispose(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect).onSubscribe((Disposable)any()); + verify(mockObserverAfterConnect).onSubscribe((Disposable)any()); + + mockScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceNext, times(1)).accept(3); + verify(sourceCompleted, times(1)).run(); + verifyObserverMock(mockObserverBeforeConnect, 1, 3); + verifyObserverMock(mockObserverAfterConnect, 1, 3); + + // FIXME not supported +// verify(spiedWorker, times(1)).isUnsubscribed(); + // FIXME publish calls cancel too +// verify(sourceUnsubscribed, times(1)).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces are declared with throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribeOnError() throws Throwable { + // setup mocks + Consumer sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Consumer sourceError = mock(Consumer.class); + Action sourceUnsubscribed = mock(Action.class); + final TestScheduler mockScheduler = new TestScheduler(); + Observer mockObserverBeforeConnect = TestHelper.mockObserver(); + Observer mockObserverAfterConnect = TestHelper.mockObserver(); + + // Observable under test + Function mockFunc = mock(Function.class); + IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); + when(mockFunc.apply(1)).thenReturn(1); + when(mockFunc.apply(2)).thenThrow(illegalArgumentException); + ConnectableObservable replay = Observable.just(1, 2, 3).map(mockFunc) + .doOnNext(sourceNext) + .doOnDispose(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .doOnError(sourceError) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect).onSubscribe((Disposable)any()); + verify(mockObserverAfterConnect).onSubscribe((Disposable)any()); + + mockScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceError, times(1)).accept(illegalArgumentException); + verifyObserver(mockObserverBeforeConnect, 1, 1, illegalArgumentException); + verifyObserver(mockObserverAfterConnect, 1, 1, illegalArgumentException); + + // FIXME no longer supported +// verify(spiedWorker, times(1)).isUnsubscribed(); + // FIXME publish also calls cancel +// verify(sourceUnsubscribed, times(1)).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceError); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + private static void verifyObserverMock(Observer mock, int numSubscriptions, int numItemsExpected) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onComplete(); + verifyNoMoreInteractions(mock); + } + + private static void verifyObserver(Observer mock, int numSubscriptions, int numItemsExpected, Throwable error) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onError(error); + verifyNoMoreInteractions(mock); + } + + public static Worker workerSpy(final Disposable mockDisposable) { + return spy(new InprocessWorker(mockDisposable)); + } + + static class InprocessWorker extends Worker { + private final Disposable mockDisposable; + public boolean unsubscribed; + + InprocessWorker(Disposable mockDisposable) { + this.mockDisposable = mockDisposable; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action) { + action.run(); + return mockDisposable; // this subscription is returned but discarded + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + action.run(); + return mockDisposable; + } + + @Override + public void dispose() { + unsubscribed = true; + } + + @Override + public boolean isDisposed() { + return unsubscribed; + } + } + + @Test + public void boundedReplayBuffer() { + BoundedReplayBuffer buf = new BoundedReplayBuffer(false) { + private static final long serialVersionUID = -5182053207244406872L; + + @Override + void truncate() { + } + }; + buf.addLast(new Node(1)); + buf.addLast(new Node(2)); + buf.addLast(new Node(3)); + buf.addLast(new Node(4)); + buf.addLast(new Node(5)); + + List values = new ArrayList(); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), values); + + buf.removeSome(2); + buf.removeFirst(); + buf.removeSome(2); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + buf.addLast(new Node(5)); + buf.addLast(new Node(6)); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(5, 6), values); + + } + + @Test + public void timedAndSizedTruncation() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test, false); + List values = new ArrayList(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + Assert.assertFalse(buf.hasCompleted()); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertTrue(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + } + + @Test + public void timedAndSizedTruncationError() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test, false); + + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + List values = new ArrayList(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.error(new TestException()); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertTrue(buf.hasError()); + } + + @Test + public void sizedTruncation() { + SizeBoundReplayBuffer buf = new SizeBoundReplayBuffer(2, false); + List values = new ArrayList(); + + buf.next(1); + buf.next(2); + buf.collect(values); + Assert.assertEquals(Arrays.asList(1, 2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + Assert.assertFalse(buf.hasCompleted()); + + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + + Assert.assertEquals(3, buf.size); + Assert.assertTrue(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + } + + @Test + public void coldReplayNoBackpressure() { + Observable source = Observable.range(0, 1000).replay().autoConnect(); + + TestObserverEx to = new TestObserverEx(); + + source.subscribe(to); + + to.assertNoErrors(); + to.assertTerminated(); + List onNextEvents = to.values(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable o = Observable.unsafeCreate(new ObservableSource() { + + @Override + public void subscribe(final Observer observer) { + observer.onSubscribe(Disposables.empty()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published Observable being executed"); + observer.onNext("one"); + observer.onComplete(); + } + }).start(); + } + }).replay().autoConnect(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Consumer() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Consumer() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void unsubscribeSource() throws Throwable { + Action unsubscribe = mock(Action.class); + Observable o = Observable.just(1).doOnDispose(unsubscribe).replay().autoConnect(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + verify(unsubscribe, never()).run(); + } + + @Test + public void take() { + TestObserverEx to = new TestObserverEx(); + + Observable cached = Observable.range(1, 100).replay().autoConnect(); + cached.take(10).subscribe(to); + + to.assertNoErrors(); + to.assertTerminated(); + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + // FIXME no longer assertable +// ts.assertUnsubscribed(); + } + + @Test + public void async() { + Observable source = Observable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestObserverEx to1 = new TestObserverEx(); + + Observable cached = source.replay().autoConnect(); + + cached.observeOn(Schedulers.computation()).subscribe(to1); + + to1.awaitDone(2, TimeUnit.SECONDS); + to1.assertNoErrors(); + to1.assertTerminated(); + assertEquals(10000, to1.values().size()); + + TestObserverEx to2 = new TestObserverEx(); + cached.observeOn(Schedulers.computation()).subscribe(to2); + + to2.awaitDone(2, TimeUnit.SECONDS); + to2.assertNoErrors(); + to2.assertTerminated(); + assertEquals(10000, to2.values().size()); + } + } + + @Test + public void asyncComeAndGo() { + Observable source = Observable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + Observable cached = source.replay().autoConnect(); + + Observable output = cached.observeOn(Schedulers.computation()); + + List> list = new ArrayList>(100); + for (int i = 0; i < 100; i++) { + TestObserverEx to = new TestObserverEx(); + list.add(to); + output.skip(i * 10).take(10).subscribe(to); + } + + List expected = new ArrayList(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestObserverEx to : list) { + to.awaitDone(3, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertTerminated(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + to.assertValueSequence(expected); + + j++; + } + } + + @Test + public void noMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Observable firehose = Observable.unsafeCreate(new ObservableSource() { + @Override + public void subscribe(Observer t) { + t.onSubscribe(Disposables.empty()); + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onComplete(); + } + }); + + TestObserverEx to = new TestObserverEx(); + firehose.replay().autoConnect().observeOn(Schedulers.computation()).takeLast(100).subscribe(to); + + to.awaitDone(3, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertTerminated(); + + assertEquals(100, to.values().size()); + } + + @Test + public void valuesAndThenError() { + Observable source = Observable.range(1, 10) + .concatWith(Observable.error(new TestException())) + .replay().autoConnect(); + + TestObserverEx to = new TestObserverEx(); + source.subscribe(to); + + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + to.assertNotComplete(); + Assert.assertEquals(1, to.errors().size()); + + TestObserverEx to2 = new TestObserverEx(); + source.subscribe(to2); + + to2.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + to2.assertNotComplete(); + Assert.assertEquals(1, to2.errors().size()); + } + + @Test + @Ignore("onNext should not throw") + public void unsafeChildThrows() { + final AtomicInteger count = new AtomicInteger(); + + Observable source = Observable.range(1, 100) + .doOnNext(new Consumer() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }) + .replay().autoConnect(); + + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.subscribe(to); + + Assert.assertEquals(100, count.get()); + + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(TestException.class); + } + + @Test + public void replayScheduler() { + + Observable.just(1).replay(Schedulers.computation()) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replayTime() { + Observable.just(1).replay(1, TimeUnit.MINUTES, Schedulers.computation(), true) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySizeScheduler() { + + Observable.just(1).replay(1, Schedulers.computation()) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySizeAndTime() { + Observable.just(1).replay(1, 1, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorSizeScheduler() { + Observable.just(1).replay(Functions.>identity(), 1, Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorScheduler() { + Observable.just(1).replay(Functions.>identity(), Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorTime() { + Observable.just(1).replay(Functions.>identity(), 1, TimeUnit.MINUTES) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replayMaxInt() { + Observable.range(1, 2) + .replay(Integer.MAX_VALUE, true) + .autoConnect() + .test() + .assertResult(1, 2); + } + + @Test + public void source() { + Observable source = Observable.range(1, 3); + + assertSame(source, (((HasUpstreamObservableSource)source.replay())).source()); + } + + @Test + public void connectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable co = Observable.range(1, 3).replay(); + + Runnable r = new Runnable() { + @Override + public void run() { + co.connect(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable co = Observable.range(1, 3).replay(); + + final TestObserver to1 = new TestObserver(); + final TestObserver to2 = new TestObserver(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + co.subscribe(to2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable co = Observable.range(1, 3).replay(); + + final TestObserver to1 = new TestObserver(); + final TestObserver to2 = new TestObserver(); + + co.subscribe(to1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + co.subscribe(to2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelOnArrival() { + Observable.range(1, 2) + .replay(Integer.MAX_VALUE, true) + .autoConnect() + .test(true) + .assertEmpty(); + } + + @Test + public void cancelOnArrival2() { + ConnectableObservable co = PublishSubject.create() + .replay(Integer.MAX_VALUE, true); + + co.test(); + + co + .autoConnect() + .test(true) + .assertEmpty(); + } + + @Test + public void connectConsumerThrows() { + ConnectableObservable co = Observable.range(1, 2) + .replay(); + + try { + co.connect(new Consumer() { + @Override + public void accept(Disposable t) throws Exception { + throw new TestException(); + } + }); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + co.test().assertEmpty().dispose(); + + co.connect(); + + co.test().assertResult(1, 2); + } + + @Test + public void badSource() { + List errors = TestHelper.trackPluginErrors(); + try { + new Observable() { + @Override + protected void subscribeActual(Observer observer) { + observer.onSubscribe(Disposables.empty()); + observer.onError(new TestException("First")); + observer.onNext(1); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + }.replay() + .autoConnect() + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject ps = PublishSubject.create(); + + final ConnectableObservable co = ps.replay(); + + final TestObserver to1 = new TestObserver(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + ps.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject ps = PublishSubject.create(); + + final ConnectableObservable co = ps.replay(); + + final TestObserver to1 = new TestObserver(); + + co.subscribe(to1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + ps.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeReplayRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable co = Observable.range(1, 1000).replay(); + + final TestObserver to1 = new TestObserver(); + + co.connect(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void reentrantOnNext() { + final PublishSubject ps = PublishSubject.create(); + + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + super.onNext(t); + } + }; + + ps.replay().autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertResult(1, 2); + } + + @Test + public void reentrantOnNextBound() { + final PublishSubject ps = PublishSubject.create(); + + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + super.onNext(t); + } + }; + + ps.replay(10, true).autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertResult(1, 2); + } + + @Test + public void reentrantOnNextCancel() { + final PublishSubject ps = PublishSubject.create(); + + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + dispose(); + } + super.onNext(t); + } + }; + + ps.replay().autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertValues(1); + } + + @Test + public void reentrantOnNextCancelBounded() { + final PublishSubject ps = PublishSubject.create(); + + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + dispose(); + } + super.onNext(t); + } + }; + + ps.replay(10, true).autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertValues(1); + } + + @Test + public void delayedUpstreamOnSubscribe() { + final Observer[] sub = { null }; + + new Observable() { + @Override + protected void subscribeActual(Observer observer) { + sub[0] = observer; + } + } + .replay() + .connect() + .dispose(); + + Disposable bs = Disposables.empty(); + + sub[0].onSubscribe(bs); + + assertTrue(bs.isDisposed()); + } + + @Test + public void timedNoOutdatedData() { + TestScheduler scheduler = new TestScheduler(); + + Observable source = Observable.just(1) + .replay(2, TimeUnit.SECONDS, scheduler, true) + .autoConnect(); + + source.test().assertResult(1); + + source.test().assertResult(1); + + scheduler.advanceTimeBy(3, TimeUnit.SECONDS); + + source.test().assertResult(); + } + + @Test + public void replaySelectorReturnsNullScheduled() { + Observable.just(1) + .replay(new Function, Observable>() { + @Override + public Observable apply(Observable v) throws Exception { + return null; + } + }, Schedulers.trampoline()) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The selector returned a null ObservableSource"); + } + + @Test + public void replaySelectorReturnsNull() { + Observable.just(1) + .replay(new Function, Observable>() { + @Override + public Observable apply(Observable v) throws Exception { + return null; + } + }) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The selector returned a null ObservableSource"); + } + + @Test + public void replaySelectorConnectableReturnsNull() { + ObservableReplay.multicastSelector(Functions.justSupplier((ConnectableObservable)null), Functions.justFunction(Observable.just(1))) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The connectableFactory returned a null ConnectableObservable"); + } + + @Test + public void noHeadRetentionCompleteSize() { + PublishSubject source = PublishSubject.create(); + + ObservableReplay co = (ObservableReplay)source + .replay(1, true); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorSize() { + PublishSubject source = PublishSubject.create(); + + ObservableReplay co = (ObservableReplay)source + .replay(1, true); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionSize() { + PublishSubject source = PublishSubject.create(); + + ObservableReplay co = (ObservableReplay)source + .replay(1, true); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + + assertNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionCompleteTime() { + PublishSubject source = PublishSubject.create(); + + ObservableReplay co = (ObservableReplay)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation(), true); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorTime() { + PublishSubject source = PublishSubject.create(); + + ObservableReplay co = (ObservableReplay)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation(), true); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionTime() { + TestScheduler sch = new TestScheduler(); + + PublishSubject source = PublishSubject.create(); + + ObservableReplay co = (ObservableReplay)source + .replay(1, TimeUnit.MILLISECONDS, sch, true); + + co.connect(); + + BoundedReplayBuffer buf = (BoundedReplayBuffer)(co.current.get().buffer); + + source.onNext(1); + + sch.advanceTimeBy(2, TimeUnit.MILLISECONDS); + + source.onNext(2); + + assertNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noBoundedRetentionViaThreadLocal() throws Exception { + Observable source = Observable.range(1, 200) + .map(new Function() { + @Override + public byte[] apply(Integer v) throws Exception { + return new byte[1024 * 1024]; + } + }) + .replay(new Function, Observable>() { + @Override + public Observable apply(final Observable o) throws Exception { + return o.take(1) + .concatMap(new Function>() { + @Override + public Observable apply(byte[] v) throws Exception { + return o; + } + }); + } + }, 1) + .takeLast(1) + ; + + System.out.println("Bounded Replay Leak check: Wait before GC"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC"); + System.gc(); + + Thread.sleep(500); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); + long initial = memHeap.getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + final AtomicLong after = new AtomicLong(); + + source.subscribe(new Consumer() { + @Override + public void accept(byte[] v) throws Exception { + System.out.println("Bounded Replay Leak check: Wait before GC 2"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC 2"); + System.gc(); + + Thread.sleep(500); + + after.set(memoryMXBean.getHeapMemoryUsage().getUsed()); + } + }); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after.get() / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after.get()) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after.get() / 1024.0 / 1024.0); + } + } + + @Test + public void sizeBoundEagerTruncate() throws Exception { + + PublishSubject ps = PublishSubject.create(); + + ConnectableObservable co = ps.replay(1, true); + + TestObserver to = co.test(); + + co.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeBoundEagerTruncate() throws Exception { + + PublishSubject ps = PublishSubject.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableObservable co = ps.replay(1, TimeUnit.SECONDS, scheduler, true); + + TestObserver to = co.test(); + + co.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeBoundEagerTruncate() throws Exception { + + PublishSubject ps = PublishSubject.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableObservable co = ps.replay(1, 5, TimeUnit.SECONDS, scheduler, true); + + TestObserver to = co.test(); + + co.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void sizeBoundSelectorEagerTruncate() throws Exception { + + PublishSubject ps = PublishSubject.create(); + + Observable co = ps.replay(Functions.>identity(), 1, true); + + TestObserver to = co.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeBoundSelectorEagerTruncate() throws Exception { + + PublishSubject ps = PublishSubject.create(); + + TestScheduler scheduler = new TestScheduler(); + + Observable co = ps.replay(Functions.>identity(), 1, TimeUnit.SECONDS, scheduler, true); + + TestObserver to = co.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeSelectorBoundEagerTruncate() throws Exception { + + PublishSubject ps = PublishSubject.create(); + + TestScheduler scheduler = new TestScheduler(); + + Observable co = ps.replay(Functions.>identity(), 1, 5, TimeUnit.SECONDS, scheduler, true); + + TestObserver to = co.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + +} diff --git a/src/test/java/io/reactivex/internal/operators/observable/ObservableReplayTest.java b/src/test/java/io/reactivex/internal/operators/observable/ObservableReplayTest.java index 9124dbb4f7..bd4bdb92ff 100644 --- a/src/test/java/io/reactivex/internal/operators/observable/ObservableReplayTest.java +++ b/src/test/java/io/reactivex/internal/operators/observable/ObservableReplayTest.java @@ -716,7 +716,7 @@ public boolean isDisposed() { @Test public void boundedReplayBuffer() { - BoundedReplayBuffer buf = new BoundedReplayBuffer() { + BoundedReplayBuffer buf = new BoundedReplayBuffer(false) { private static final long serialVersionUID = -5182053207244406872L; @Override @@ -753,7 +753,7 @@ void truncate() { @Test public void timedAndSizedTruncation() { TestScheduler test = new TestScheduler(); - SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test, false); List values = new ArrayList(); buf.next(1); @@ -792,7 +792,7 @@ public void timedAndSizedTruncation() { @Test public void timedAndSizedTruncationError() { TestScheduler test = new TestScheduler(); - SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, TimeUnit.MILLISECONDS, test, false); Assert.assertFalse(buf.hasCompleted()); Assert.assertFalse(buf.hasError()); @@ -835,7 +835,7 @@ public void timedAndSizedTruncationError() { @Test public void sizedTruncation() { - SizeBoundReplayBuffer buf = new SizeBoundReplayBuffer(2); + SizeBoundReplayBuffer buf = new SizeBoundReplayBuffer(2, false); List values = new ArrayList(); buf.next(1); diff --git a/src/test/java/io/reactivex/validators/ParamValidationCheckerTest.java b/src/test/java/io/reactivex/validators/ParamValidationCheckerTest.java index e8ab6373be..47b5a2f88e 100644 --- a/src/test/java/io/reactivex/validators/ParamValidationCheckerTest.java +++ b/src/test/java/io/reactivex/validators/ParamValidationCheckerTest.java @@ -167,12 +167,16 @@ public void checkParallelFlowable() { // negative time is considered as zero time addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class)); addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class)); addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class)); addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); addOverride(new ParamOverride(Flowable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class)); addOverride(new ParamOverride(Flowable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); // zero retry is allowed addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE)); @@ -417,12 +421,16 @@ public void checkParallelFlowable() { // negative time is considered as zero time addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class)); addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class)); addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class)); addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); addOverride(new ParamOverride(Observable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class)); addOverride(new ParamOverride(Observable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); // zero retry is allowed addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE)); From ef4f6b25e7b550107633a7443656d5746cddc4ba Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 21 Jun 2019 14:28:30 +0200 Subject: [PATCH 2/2] Those eager tests are in their separate files already --- .../flowable/FlowableReplayTest.java | 137 ------------------ 1 file changed, 137 deletions(-) diff --git a/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayTest.java b/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayTest.java index 71e60eac44..2c4ec557cc 100644 --- a/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayTest.java +++ b/src/test/java/io/reactivex/internal/operators/flowable/FlowableReplayTest.java @@ -2041,141 +2041,4 @@ public void accept(byte[] v) throws Exception { + " -> " + after.get() / 1024.0 / 1024.0); } } - - @Test - public void sizeBoundEagerTruncate() throws Exception { - - PublishProcessor pp = PublishProcessor.create(); - - ConnectableFlowable cf = pp.replay(1, true); - - TestSubscriber ts = cf.test(); - - cf.connect(); - - Thread.sleep(200); - System.gc(); - Thread.sleep(200); - - final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); - long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); - - System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); - - pp.onNext(new int[100 * 1024 * 1024]); - - ts.assertValueCount(1); - ts.values().clear(); - - pp.onNext(new int[0]); - - Thread.sleep(200); - System.gc(); - Thread.sleep(200); - - long after = memoryMXBean.getHeapMemoryUsage().getUsed(); - - ts.cancel(); - - System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); - - if (initial + 100 * 1024 * 1024 < after) { - Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) - + " -> " + after / 1024.0 / 1024.0); - } - } - - @Test - public void timeBoundEagerTruncate() throws Exception { - - PublishProcessor pp = PublishProcessor.create(); - - TestScheduler scheduler = new TestScheduler(); - - ConnectableFlowable cf = pp.replay(1, TimeUnit.SECONDS, scheduler, true); - - TestSubscriber ts = cf.test(); - - cf.connect(); - - Thread.sleep(200); - System.gc(); - Thread.sleep(200); - - final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); - long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); - - System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); - - pp.onNext(new int[100 * 1024 * 1024]); - - ts.assertValueCount(1); - ts.values().clear(); - - scheduler.advanceTimeBy(2, TimeUnit.SECONDS); - - pp.onNext(new int[0]); - - Thread.sleep(200); - System.gc(); - Thread.sleep(200); - - long after = memoryMXBean.getHeapMemoryUsage().getUsed(); - - ts.cancel(); - - System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); - - if (initial + 100 * 1024 * 1024 < after) { - Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) - + " -> " + after / 1024.0 / 1024.0); - } - } - - @Test - public void timeAndSizeBoundEagerTruncate() throws Exception { - - PublishProcessor pp = PublishProcessor.create(); - - TestScheduler scheduler = new TestScheduler(); - - ConnectableFlowable cf = pp.replay(1, 5, TimeUnit.SECONDS, scheduler, true); - - TestSubscriber ts = cf.test(); - - cf.connect(); - - Thread.sleep(200); - System.gc(); - Thread.sleep(200); - - final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); - long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); - - System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); - - pp.onNext(new int[100 * 1024 * 1024]); - - ts.assertValueCount(1); - ts.values().clear(); - - scheduler.advanceTimeBy(2, TimeUnit.SECONDS); - - pp.onNext(new int[0]); - - Thread.sleep(200); - System.gc(); - Thread.sleep(200); - - long after = memoryMXBean.getHeapMemoryUsage().getUsed(); - - ts.cancel(); - - System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); - - if (initial + 100 * 1024 * 1024 < after) { - Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) - + " -> " + after / 1024.0 / 1024.0); - } - } }