From 1f8c2b3182217a8c0180f1f9e975c1d0385d67d3 Mon Sep 17 00:00:00 2001 From: Luka Cindro Date: Fri, 19 Feb 2016 14:42:02 +0100 Subject: [PATCH 001/322] Add maxConcurrent overload to flatMapIterable --- src/main/java/rx/Observable.java | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 5e42dc830c..e8b139213c 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5558,6 +5558,36 @@ public final Observable flatMapIterable(Func1 + * + *
+ *
Scheduler:
+ *
{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the type of item emitted by the resulting Observable + * @param collectionSelector + * a function that returns an Iterable sequence of values for when given an item emitted by the + * source Observable + * @param maxConcurrent + * the maximum number of Observables that may be subscribed to concurrently + * @return an Observable that emits the results of merging the items emitted by the source Observable with + * the values in the Iterables corresponding to those items, as generated by {@code collectionSelector} + * @throws IllegalArgumentException + * if {@code maxConcurrent} is less than or equal to 0 + * @see ReactiveX operators documentation: FlatMap + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Beta + public final Observable flatMapIterable(Func1> collectionSelector, int maxConcurrent) { + return merge(map(OperatorMapPair.convertSelector(collectionSelector)), maxConcurrent); + } + /** * Returns an Observable that emits the results of applying a function to the pair of values from the source * Observable and an Iterable corresponding to that item that is generated by a selector. @@ -5587,6 +5617,42 @@ public final Observable flatMapIterable(Func1 + * + *
+ *
Scheduler:
+ *
{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the collection element type + * @param + * the type of item emited by the resulting Observable + * @param collectionSelector + * a function that returns an Iterable sequence of values for each item emitted by the source + * Observable + * @param resultSelector + * a function that returns an item based on the item emitted by the source Observable and the + * Iterable returned for that item by the {@code collectionSelector} + * @param maxConcurrent + * the maximum number of Observables that may be subscribed to concurrently + * @return an Observable that emits the items returned by {@code resultSelector} for each item in the source + * Observable + * @throws IllegalArgumentException + * if {@code maxConcurrent} is less than or equal to 0 + * @see ReactiveX operators documentation: FlatMap + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Beta + public final Observable flatMapIterable(Func1> collectionSelector, + Func2 resultSelector, int maxConcurrent) { + return flatMap(OperatorMapPair.convertSelector(collectionSelector), resultSelector, maxConcurrent); + } + /** * Subscribes to the {@link Observable} and receives notifications for each element. *

From 8b553035c737676efc61b0fc77e33f71ab84ea7f Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Wed, 24 Feb 2016 16:11:09 -0500 Subject: [PATCH 002/322] Avoid swallowing errors in Completable Instead, deliver them up to the thread's uncaught exception handler. Fixes reactivex/rxjava#3726 --- src/main/java/rx/Completable.java | 17 +++- .../rx/CapturingUncaughtExceptionHandler.java | 16 ++++ src/test/java/rx/CompletableTest.java | 79 ++++++++++++++++++- .../java/rx/schedulers/SchedulerTests.java | 14 +--- 4 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 src/test/java/rx/CapturingUncaughtExceptionHandler.java diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index b71ee03e20..f2e752c2b2 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -1835,6 +1835,7 @@ public void onCompleted() { public void onError(Throwable e) { ERROR_HANDLER.handleError(e); mad.unsubscribe(); + deliverUncaughtException(e); } @Override @@ -1864,14 +1865,17 @@ public void onCompleted() { onComplete.call(); } catch (Throwable e) { ERROR_HANDLER.handleError(e); + deliverUncaughtException(e); + } finally { + mad.unsubscribe(); } - mad.unsubscribe(); } @Override public void onError(Throwable e) { ERROR_HANDLER.handleError(e); mad.unsubscribe(); + deliverUncaughtException(e); } @Override @@ -1915,8 +1919,10 @@ public void onError(Throwable e) { } catch (Throwable ex) { e = new CompositeException(Arrays.asList(e, ex)); ERROR_HANDLER.handleError(e); + deliverUncaughtException(e); + } finally { + mad.unsubscribe(); } - mad.unsubscribe(); } @Override @@ -1927,7 +1933,12 @@ public void onSubscribe(Subscription d) { return mad; } - + + private static void deliverUncaughtException(Throwable e) { + Thread thread = Thread.currentThread(); + thread.getUncaughtExceptionHandler().uncaughtException(thread, e); + } + /** * Subscribes the given CompletableSubscriber to this Completable instance. * @param s the CompletableSubscriber, not null diff --git a/src/test/java/rx/CapturingUncaughtExceptionHandler.java b/src/test/java/rx/CapturingUncaughtExceptionHandler.java new file mode 100644 index 0000000000..52b809a3c1 --- /dev/null +++ b/src/test/java/rx/CapturingUncaughtExceptionHandler.java @@ -0,0 +1,16 @@ +package rx; + +import java.util.concurrent.CountDownLatch; + +public final class CapturingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { + public int count = 0; + public Throwable caught; + public CountDownLatch completed = new CountDownLatch(1); + + @Override + public void uncaughtException(Thread t, Throwable e) { + count++; + caught = e; + completed.countDown(); + } +} diff --git a/src/test/java/rx/CompletableTest.java b/src/test/java/rx/CompletableTest.java index 97c169c4f5..894c72109f 100644 --- a/src/test/java/rx/CompletableTest.java +++ b/src/test/java/rx/CompletableTest.java @@ -2700,7 +2700,64 @@ public void call(CompletableSubscriber s) { Assert.assertTrue(name.get().startsWith("RxComputation")); } - + + @Test + public void subscribeEmptyOnError() { + expectUncaughtTestException(new Action0() { + @Override public void call() { + error.completable.subscribe(); + } + }); + } + + @Test + public void subscribeOneActionOnError() { + expectUncaughtTestException(new Action0() { + @Override + public void call() { + error.completable.subscribe(new Action0() { + @Override + public void call() { + } + }); + } + }); + } + + @Test + public void subscribeOneActionThrowFromOnCompleted() { + expectUncaughtTestException(new Action0() { + @Override + public void call() { + normal.completable.subscribe(new Action0() { + @Override + public void call() { + throw new TestException(); + } + }); + } + }); + } + + @Test + public void subscribeTwoActionsThrowFromOnError() { + expectUncaughtTestException(new Action0() { + @Override + public void call() { + error.completable.subscribe(new Action1() { + @Override + public void call(Throwable throwable) { + throw new TestException(); + } + }, new Action0() { + @Override + public void call() { + } + }); + } + }); + } + @Test(timeout = 1000) public void timeoutEmitError() { Throwable e = Completable.never().timeout(100, TimeUnit.MILLISECONDS).get(); @@ -3742,4 +3799,24 @@ public void call(Throwable e) { assertNotNull("Unsubscribed before the call to onError", subscriptionRef.get()); } + private static void expectUncaughtTestException(Action0 action) { + Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler(); + CapturingUncaughtExceptionHandler handler = new CapturingUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(handler); + try { + action.call(); + assertEquals("Should have received exactly 1 exception", 1, handler.count); + Throwable caught = handler.caught; + while (caught != null) { + if (caught instanceof TestException) break; + if (caught == caught.getCause()) break; + caught = caught.getCause(); + } + assertTrue("A TestException should have been delivered to the handler", + caught instanceof TestException); + } finally { + Thread.setDefaultUncaughtExceptionHandler(originalHandler); + } + } + } \ No newline at end of file diff --git a/src/test/java/rx/schedulers/SchedulerTests.java b/src/test/java/rx/schedulers/SchedulerTests.java index 3b25c7be91..a9146fafde 100644 --- a/src/test/java/rx/schedulers/SchedulerTests.java +++ b/src/test/java/rx/schedulers/SchedulerTests.java @@ -1,5 +1,6 @@ package rx.schedulers; +import rx.CapturingUncaughtExceptionHandler; import rx.Observable; import rx.Observer; import rx.Scheduler; @@ -87,19 +88,6 @@ static void testHandledErrorIsNotDeliveredToThreadHandler(Scheduler scheduler) t } } - private static final class CapturingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { - int count = 0; - Throwable caught; - CountDownLatch completed = new CountDownLatch(1); - - @Override - public void uncaughtException(Thread t, Throwable e) { - count++; - caught = e; - completed.countDown(); - } - } - private static final class CapturingObserver implements Observer { CountDownLatch completed = new CountDownLatch(1); int errorCount = 0; From ccd24b5c3ce471428531e12737591b94c91db8bf Mon Sep 17 00:00:00 2001 From: Aaron He Date: Wed, 2 Mar 2016 15:38:01 -0800 Subject: [PATCH 003/322] Add doOnSubscribe for Single --- src/main/java/rx/Single.java | 22 +++++++++++++++++++ src/test/java/rx/SingleTest.java | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 20b983c063..a8a10bafb9 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -2250,6 +2250,28 @@ public void onNext(T t) { return lift(new OperatorDoOnEach(observer)); } + /** + * Modifies the source {@code Single} so that it invokes the given action when it is subscribed from + * its subscribers. Each subscription will result in an invocation of the given action except when the + * source {@code Single} is reference counted, in which case the source {@code Single} will invoke + * the given action for the first subscription. + *

+ * + *

+ *
Scheduler:
+ *
{@code doOnSubscribe} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param subscribe + * the action that gets called when an observer subscribes to this {@code Single} + * @return the source {@code Single} modified so as to call this Action when appropriate + * @see ReactiveX operators documentation: Do + */ + @Experimental + public final Single doOnSubscribe(final Action0 subscribe) { + return lift(new OperatorDoOnSubscribe(subscribe)); + } + /** * Returns an Single that emits the items emitted by the source Single shifted forward in time by a * specified delay. Error notifications from the source Single are not delayed. diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 3ce86e9772..17794e4dbb 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -878,6 +878,43 @@ public void doOnSuccessShouldNotSwallowExceptionThrownByAction() { verify(action).call(eq("value")); } + @Test + public void doOnSubscribeShouldInvokeAction() { + Action0 action = mock(Action0.class); + Single single = Single.just(1).doOnSubscribe(action); + + verifyZeroInteractions(action); + + single.subscribe(); + single.subscribe(); + + verify(action, times(2)).call(); + } + + @Test + public void doOnSubscribeShouldInvokeActionBeforeSubscriberSubscribes() { + final List callSequence = new ArrayList(2); + + Single single = Single.create(new OnSubscribe() { + @Override + public void call(SingleSubscriber singleSubscriber) { + callSequence.add("onSubscribe"); + singleSubscriber.onSuccess(1); + } + }).doOnSubscribe(new Action0() { + @Override + public void call() { + callSequence.add("doOnSubscribe"); + } + }); + + single.subscribe(); + + assertEquals(2, callSequence.size()); + assertEquals("doOnSubscribe", callSequence.get(0)); + assertEquals("onSubscribe", callSequence.get(1)); + } + @Test public void delayWithSchedulerShouldDelayCompletion() { TestScheduler scheduler = new TestScheduler(); From 479d1e0bf6c80d2a52bea6472858295c22e610b6 Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Tue, 1 Mar 2016 17:22:31 -0800 Subject: [PATCH 004/322] Creating Observable#create overloads for SyncOnSubscribe and AsyncOnSubscribe --- src/main/java/rx/Observable.java | 71 +++++++++++++++++++ .../java/rx/observables/AsyncOnSubscribe.java | 24 +++---- .../java/rx/observables/SyncOnSubscribe.java | 24 +++---- 3 files changed, 95 insertions(+), 24 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 78780ab31b..ab0c8d3746 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -95,6 +95,77 @@ public static Observable create(OnSubscribe f) { return new Observable(hook.onCreate(f)); } + /** + * Returns an Observable that respects the back-pressure semantics. When the returned Observable is + * subscribed to it will initiate the given {@link SyncOnSubscribe}'s life cycle for + * generating events. + * + *

Note: the {@code SyncOnSubscribe} provides a generic way to fulfill data by iterating + * over a (potentially stateful) function (e.g. reading data off of a channel, a parser, ). If your + * data comes directly from an asyrchronous/potentially concurrent source then consider using the + * {@link Observable#create(AsyncOnSubscribe) asynchronous overload}. + * + *

+ * + *

+ * See Rx Design Guidelines (PDF) for detailed + * information. + *

+ *
Scheduler:
+ *
{@code create} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the type of the items that this Observable emits + * @param syncOnSubscribe + * an implementation of {@link SyncOnSubscribe}. There are many static creation methods + * on the class for convenience. + * @return an Observable that, when a {@link Subscriber} subscribes to it, will execute the specified + * function + * @see {@link SyncOnSubscribe} {@code static create*} methods + * @see ReactiveX operators documentation: Create + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public static Observable create(SyncOnSubscribe syncOnSubscribe) { + return new Observable(hook.onCreate(syncOnSubscribe)); + } + + /** + * Returns an Observable that respects the back-pressure semantics. When the returned Observable is + * subscribed to it will initiate the given {@link AsyncOnSubscribe}'s life cycle for + * generating events. + * + *

Note: the {@code AsyncOnSubscribe} is useful for observable sources of data that are + * necessarily asynchronous (RPC, external services, etc). Typically most use cases can be solved + * with the {@link Observable#create(SyncOnSubscribe) synchronous overload}. + * + *

+ * + *

+ * See Rx Design Guidelines (PDF) for detailed + * information. + *

+ *
Scheduler:
+ *
{@code create} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the type of the items that this Observable emits + * @param asyncOnSubscribe + * an implementation of {@link AsyncOnSubscribe}. There are many static creation methods + * on the class for convenience. + * @return an Observable that, when a {@link Subscriber} subscribes to it, will execute the specified + * function + * @see {@link AsyncOnSubscribe AsyncOnSubscribe} {@code static create*} methods + * @see ReactiveX operators documentation: Create + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public static Observable create(AsyncOnSubscribe asyncOnSubscribe) { + return new Observable(hook.onCreate(asyncOnSubscribe)); + } + /** * Invoked when Observable.subscribe is called. */ diff --git a/src/main/java/rx/observables/AsyncOnSubscribe.java b/src/main/java/rx/observables/AsyncOnSubscribe.java index d95dc82b9d..24de19c149 100644 --- a/src/main/java/rx/observables/AsyncOnSubscribe.java +++ b/src/main/java/rx/observables/AsyncOnSubscribe.java @@ -106,10 +106,10 @@ protected void onUnsubscribe(S state) { * @param next * produces data to the downstream subscriber (see * {@link #next(Object, long, Observer) next(S, long, Observer)}) - * @return an OnSubscribe that emits data in a protocol compatible with back-pressure. + * @return an AsyncOnSubscribe that emits data in a protocol compatible with back-pressure. */ @Experimental - public static OnSubscribe createSingleState(Func0 generator, + public static AsyncOnSubscribe createSingleState(Func0 generator, final Action3>> next) { Func3>, S> nextFunc = new Func3>, S>() { @@ -134,11 +134,11 @@ public S call(S state, Long requested, Observer> subscri * {@link #next(Object, long, Observer) next(S, long, Observer)}) * @param onUnsubscribe * clean up behavior (see {@link #onUnsubscribe(Object) onUnsubscribe(S)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return an AsyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createSingleState(Func0 generator, + public static AsyncOnSubscribe createSingleState(Func0 generator, final Action3>> next, final Action1 onUnsubscribe) { Func3>, S> nextFunc = @@ -162,11 +162,11 @@ public S call(S state, Long requested, Observer> subscri * {@link #next(Object, long, Observer) next(S, long, Observer)}) * @param onUnsubscribe * clean up behavior (see {@link #onUnsubscribe(Object) onUnsubscribe(S)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return an AsyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createStateful(Func0 generator, + public static AsyncOnSubscribe createStateful(Func0 generator, Func3>, ? extends S> next, Action1 onUnsubscribe) { return new AsyncOnSubscribeImpl(generator, next, onUnsubscribe); @@ -181,11 +181,11 @@ public static OnSubscribe createStateful(Func0 generator, * @param next * produces data to the downstream subscriber (see * {@link #next(Object, long, Observer) next(S, long, Observer)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return an AsyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createStateful(Func0 generator, + public static AsyncOnSubscribe createStateful(Func0 generator, Func3>, ? extends S> next) { return new AsyncOnSubscribeImpl(generator, next); } @@ -200,11 +200,11 @@ public static OnSubscribe createStateful(Func0 generator, * @param next * produces data to the downstream subscriber (see * {@link #next(Object, long, Observer) next(S, long, Observer)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return an AsyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createStateless(final Action2>> next) { + public static AsyncOnSubscribe createStateless(final Action2>> next) { Func3>, Void> nextFunc = new Func3>, Void>() { @Override @@ -227,11 +227,11 @@ public Void call(Void state, Long requested, Observer> s * {@link #next(Object, long, Observer) next(S, long, Observer)}) * @param onUnsubscribe * clean up behavior (see {@link #onUnsubscribe(Object) onUnsubscribe(S)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return an AsyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createStateless(final Action2>> next, + public static AsyncOnSubscribe createStateless(final Action2>> next, final Action0 onUnsubscribe) { Func3>, Void> nextFunc = new Func3>, Void>() { diff --git a/src/main/java/rx/observables/SyncOnSubscribe.java b/src/main/java/rx/observables/SyncOnSubscribe.java index f8cda8dde0..910a5acddb 100644 --- a/src/main/java/rx/observables/SyncOnSubscribe.java +++ b/src/main/java/rx/observables/SyncOnSubscribe.java @@ -124,10 +124,10 @@ protected void onUnsubscribe(S state) { * @param next * produces data to the downstream subscriber (see {@link #next(Object, Subscriber) * next(S, Subscriber)}) - * @return an OnSubscribe that emits data in a protocol compatible with back-pressure. + * @return a SyncOnSubscribe that emits data in a protocol compatible with back-pressure. */ @Experimental - public static OnSubscribe createSingleState(Func0 generator, + public static SyncOnSubscribe createSingleState(Func0 generator, final Action2> next) { Func2, S> nextFunc = new Func2, S>() { @Override @@ -152,11 +152,11 @@ public S call(S state, Observer subscriber) { * next(S, Subscriber)}) * @param onUnsubscribe * clean up behavior (see {@link #onUnsubscribe(Object) onUnsubscribe(S)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createSingleState(Func0 generator, + public static SyncOnSubscribe createSingleState(Func0 generator, final Action2> next, final Action1 onUnsubscribe) { Func2, S> nextFunc = new Func2, S>() { @@ -180,11 +180,11 @@ public S call(S state, Observer subscriber) { * next(S, Subscriber)}) * @param onUnsubscribe * clean up behavior (see {@link #onUnsubscribe(Object) onUnsubscribe(S)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createStateful(Func0 generator, + public static SyncOnSubscribe createStateful(Func0 generator, Func2, ? extends S> next, Action1 onUnsubscribe) { return new SyncOnSubscribeImpl(generator, next, onUnsubscribe); @@ -199,11 +199,11 @@ public static OnSubscribe createStateful(Func0 generator, * @param next * produces data to the downstream subscriber (see {@link #next(Object, Subscriber) * next(S, Subscriber)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createStateful(Func0 generator, + public static SyncOnSubscribe createStateful(Func0 generator, Func2, ? extends S> next) { return new SyncOnSubscribeImpl(generator, next); } @@ -218,11 +218,11 @@ public static OnSubscribe createStateful(Func0 generator, * @param next * produces data to the downstream subscriber (see {@link #next(Object, Subscriber) * next(S, Subscriber)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createStateless(final Action1> next) { + public static SyncOnSubscribe createStateless(final Action1> next) { Func2, Void> nextFunc = new Func2, Void>() { @Override public Void call(Void state, Observer subscriber) { @@ -245,11 +245,11 @@ public Void call(Void state, Observer subscriber) { * next(S, Subscriber)}) * @param onUnsubscribe * clean up behavior (see {@link #onUnsubscribe(Object) onUnsubscribe(S)}) - * @return an OnSubscribe that emits data downstream in a protocol compatible with + * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ @Experimental - public static OnSubscribe createStateless(final Action1> next, + public static SyncOnSubscribe createStateless(final Action1> next, final Action0 onUnsubscribe) { Func2, Void> nextFunc = new Func2, Void>() { @Override From f7f5db4ddc3ee94967f8f086453a5533f1ae7746 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 10 Mar 2016 13:22:15 +0100 Subject: [PATCH 005/322] 1.x: Fix the test Issue1685 not waiting long enough. --- src/test/java/rx/ObservableTests.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index 214eb7aff4..4b0d2f2330 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -1108,7 +1108,9 @@ public void uncaughtException(Thread t, Throwable e) { subject.subscribe(); subject.materialize().toBlocking().first(); - Thread.sleep(1000); // the uncaught exception comes after the terminal event reaches toBlocking + for (int i = 0; i < 20 && err.get() == null; i++) { + Thread.sleep(100); // the uncaught exception comes after the terminal event reaches toBlocking + } assertNotNull("UncaughtExceptionHandler didn't get anything.", err.get()); From 1be11d694dba04ac8a7a9edf3c99c36d5d92c1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Sun, 13 Mar 2016 21:12:16 +0100 Subject: [PATCH 006/322] 1.x: Single.using() --- src/main/java/rx/Single.java | 95 ++++ .../operators/SingleOnSubscribeUsing.java | 116 +++++ src/test/java/rx/SingleTest.java | 53 +- .../operators/SingleOnSubscribeUsingTest.java | 492 ++++++++++++++++++ 4 files changed, 746 insertions(+), 10 deletions(-) create mode 100644 src/main/java/rx/internal/operators/SingleOnSubscribeUsing.java create mode 100644 src/test/java/rx/internal/operators/SingleOnSubscribeUsingTest.java diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index a8a10bafb9..bacdd90667 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1597,6 +1597,30 @@ public final void unsafeSubscribe(Subscriber subscriber) { } } + /** + * Subscribes an Observer to this single and returns a Subscription that allows + * unsubscription. + * + * @param observer the Observer to subscribe + * @return the Subscription that allows unsubscription + */ + public final Subscription subscribe(final Observer observer) { + if (observer == null) { + throw new NullPointerException("observer is null"); + } + return subscribe(new SingleSubscriber() { + @Override + public void onSuccess(T value) { + observer.onNext(value); + observer.onCompleted(); + } + @Override + public void onError(Throwable error) { + observer.onError(error); + } + }); + } + /** * Subscribes to a Single and provides a Subscriber that implements functions to handle the item the Single * emits or any error notification it issues. @@ -2541,4 +2565,75 @@ public final Single retryWhen(final Func1, ? return toObservable().retryWhen(notificationHandler).toSingle(); } + /** + * Constructs an Single that creates a dependent resource object which is disposed of on unsubscription. + *

+ * + *

+ *
Scheduler:
+ *
{@code using} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param resourceFactory + * the factory function to create a resource object that depends on the Single + * @param singleFactory + * the factory function to create a Single + * @param disposeAction + * the function that will dispose of the resource + * @return the Single whose lifetime controls the lifetime of the dependent resource object + * @see ReactiveX operators documentation: Using + */ + @Experimental + public static Single using( + final Func0 resourceFactory, + final Func1> observableFactory, + final Action1 disposeAction) { + return using(resourceFactory, observableFactory, disposeAction, false); + } + + /** + * Constructs an Single that creates a dependent resource object which is disposed of just before + * termination if you have set {@code disposeEagerly} to {@code true} and unsubscription does not occur + * before termination. Otherwise resource disposal will occur on unsubscription. Eager disposal is + * particularly appropriate for a synchronous Single that resuses resources. {@code disposeAction} will + * only be called once per subscription. + *

+ * + *

+ *
Scheduler:
+ *
{@code using} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @warn "Backpressure Support" section missing from javadoc + * @param resourceFactory + * the factory function to create a resource object that depends on the Single + * @param singleFactory + * the factory function to create a Single + * @param disposeAction + * the function that will dispose of the resource + * @param disposeEagerly + * if {@code true} then disposal will happen either on unsubscription or just before emission of + * a terminal event ({@code onComplete} or {@code onError}). + * @return the Single whose lifetime controls the lifetime of the dependent resource object + * @see ReactiveX operators documentation: Using + * @Experimental The behavior of this can change at any time. + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public static Single using( + final Func0 resourceFactory, + final Func1> singleFactory, + final Action1 disposeAction, boolean disposeEagerly) { + if (resourceFactory == null) { + throw new NullPointerException("resourceFactory is null"); + } + if (singleFactory == null) { + throw new NullPointerException("singleFactory is null"); + } + if (disposeAction == null) { + throw new NullPointerException("disposeAction is null"); + } + return create(new SingleOnSubscribeUsing(resourceFactory, singleFactory, disposeAction, disposeEagerly)); + } + } diff --git a/src/main/java/rx/internal/operators/SingleOnSubscribeUsing.java b/src/main/java/rx/internal/operators/SingleOnSubscribeUsing.java new file mode 100644 index 0000000000..8bd73a29b0 --- /dev/null +++ b/src/main/java/rx/internal/operators/SingleOnSubscribeUsing.java @@ -0,0 +1,116 @@ +package rx.internal.operators; + +import java.util.Arrays; + +import rx.*; +import rx.exceptions.*; +import rx.functions.*; +import rx.plugins.RxJavaPlugins; + +/** + * Generates a resource, derives a Single from it and disposes that resource once the + * Single terminates. + * @param the value type of the Single + * @param the resource type + */ +public final class SingleOnSubscribeUsing implements Single.OnSubscribe { + final Func0 resourceFactory; + final Func1> singleFactory; + final Action1 disposeAction; + final boolean disposeEagerly; + + public SingleOnSubscribeUsing(Func0 resourceFactory, + Func1> observableFactory, + Action1 disposeAction, boolean disposeEagerly) { + this.resourceFactory = resourceFactory; + this.singleFactory = observableFactory; + this.disposeAction = disposeAction; + this.disposeEagerly = disposeEagerly; + } + + @Override + public void call(final SingleSubscriber child) { + final Resource resource; + + try { + resource = resourceFactory.call(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + child.onError(ex); + return; + } + + Single single; + + try { + single = singleFactory.call(resource); + } catch (Throwable ex) { + handleSubscriptionTimeError(child, resource, ex); + return; + } + + if (single == null) { + handleSubscriptionTimeError(child, resource, new NullPointerException("The single")); + return; + } + + SingleSubscriber parent = new SingleSubscriber() { + @Override + public void onSuccess(T value) { + if (disposeEagerly) { + try { + disposeAction.call(resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + child.onError(ex); + return; + } + } + + child.onSuccess(value); + + if (!disposeEagerly) { + try { + disposeAction.call(resource); + } catch (Throwable ex2) { + Exceptions.throwIfFatal(ex2); + RxJavaPlugins.getInstance().getErrorHandler().handleError(ex2); + } + } + } + + @Override + public void onError(Throwable error) { + handleSubscriptionTimeError(child, resource, error); + } + }; + child.add(parent); + + single.subscribe(parent); + } + + void handleSubscriptionTimeError(SingleSubscriber t, Resource resource, Throwable ex) { + Exceptions.throwIfFatal(ex); + + if (disposeEagerly) { + try { + disposeAction.call(resource); + } catch (Throwable ex2) { + Exceptions.throwIfFatal(ex2); + ex = new CompositeException(Arrays.asList(ex, ex2)); + } + } + + t.onError(ex); + + if (!disposeEagerly) { + try { + disposeAction.call(resource); + } catch (Throwable ex2) { + Exceptions.throwIfFatal(ex2); + RxJavaPlugins.getInstance().getErrorHandler().handleError(ex2); + } + } + } +} diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 17794e4dbb..d2457da4e9 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -12,29 +12,28 @@ */ package rx; -import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; import java.io.IOException; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + import rx.Single.OnSubscribe; -import rx.exceptions.CompositeException; +import rx.exceptions.*; import rx.functions.*; import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.schedulers.TestScheduler; +import rx.schedulers.*; import rx.singles.BlockingSingle; import rx.subjects.PublishSubject; import rx.subscriptions.Subscriptions; -import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; - public class SingleTest { @Test @@ -1681,4 +1680,38 @@ public void takeUntilError_withSingle_shouldMatch() { assertFalse(until.hasObservers()); assertFalse(ts.isUnsubscribed()); } + + @Test + public void subscribeWithObserver() { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Single.just(1).subscribe(o); + + verify(o).onNext(1); + verify(o).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void subscribeWithObserverAndGetError() { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Single.error(new TestException()).subscribe(o); + + verify(o, never()).onNext(anyInt()); + verify(o, never()).onCompleted(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void subscribeWithNullObserver() { + try { + Single.just(1).subscribe((Observer)null); + fail("Failed to throw NullPointerException"); + } catch (NullPointerException ex) { + assertEquals("observer is null", ex.getMessage()); + } + } } diff --git a/src/test/java/rx/internal/operators/SingleOnSubscribeUsingTest.java b/src/test/java/rx/internal/operators/SingleOnSubscribeUsingTest.java new file mode 100644 index 0000000000..238f373115 --- /dev/null +++ b/src/test/java/rx/internal/operators/SingleOnSubscribeUsingTest.java @@ -0,0 +1,492 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.internal.operators; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import rx.*; +import rx.Observer; +import rx.exceptions.TestException; +import rx.functions.*; +import rx.subscriptions.Subscriptions; + +public class SingleOnSubscribeUsingTest { + + private interface Resource { + String getTextFromWeb(); + + void dispose(); + } + + private static class DisposeAction implements Action1 { + + @Override + public void call(Resource r) { + r.dispose(); + } + + } + + private final Action1 disposeSubscription = new Action1() { + + @Override + public void call(Subscription s) { + s.unsubscribe(); + } + + }; + + @Test + public void nonEagerly() { + performTestUsing(false); + } + + @Test + public void eagerly() { + performTestUsing(true); + } + + private void performTestUsing(boolean disposeEagerly) { + final Resource resource = mock(Resource.class); + when(resource.getTextFromWeb()).thenReturn("Hello world!"); + + Func0 resourceFactory = new Func0() { + @Override + public Resource call() { + return resource; + } + }; + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Resource resource) { + return Single.just(resource.getTextFromWeb()); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Single observable = Single.using(resourceFactory, observableFactory, + new DisposeAction(), disposeEagerly); + observable.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onNext("Hello world!"); + inOrder.verify(observer).onCompleted(); + inOrder.verifyNoMoreInteractions(); + + // The resouce should be closed + verify(resource).dispose(); + } + + @Test + public void withSubscribingTwice() { + performTestUsingWithSubscribingTwice(false); + } + + @Test + public void withSubscribingTwiceDisposeEagerly() { + performTestUsingWithSubscribingTwice(true); + } + + private void performTestUsingWithSubscribingTwice(boolean disposeEagerly) { + // When subscribe is called, a new resource should be created. + Func0 resourceFactory = new Func0() { + @Override + public Resource call() { + return new Resource() { + + boolean first = true; + + @Override + public String getTextFromWeb() { + if (first) { + first = false; + return "Hello world!"; + } + return "Nothing"; + } + + @Override + public void dispose() { + // do nothing + } + + }; + } + }; + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Resource resource) { + return Single.just(resource.getTextFromWeb()); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Single observable = Single.using(resourceFactory, observableFactory, + new DisposeAction(), disposeEagerly); + observable.subscribe(observer); + observable.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + inOrder.verify(observer).onNext("Hello world!"); + inOrder.verify(observer).onCompleted(); + + inOrder.verify(observer).onNext("Hello world!"); + inOrder.verify(observer).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test(expected = TestException.class) + public void withResourceFactoryError() { + performTestUsingWithResourceFactoryError(false); + } + + @Test(expected = TestException.class) + public void withResourceFactoryErrorDisposeEagerly() { + performTestUsingWithResourceFactoryError(true); + } + + private void performTestUsingWithResourceFactoryError(boolean disposeEagerly) { + Func0 resourceFactory = new Func0() { + @Override + public Subscription call() { + throw new TestException(); + } + }; + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Subscription subscription) { + return Single.just(1); + } + }; + + Single.using(resourceFactory, observableFactory, disposeSubscription) + .toBlocking().value(); + } + + @Test + public void withSingleFactoryError() { + performTestUsingWithSingleFactoryError(false); + } + + @Test + public void withSingleFactoryErrorDisposeEagerly() { + performTestUsingWithSingleFactoryError(true); + } + + private void performTestUsingWithSingleFactoryError(boolean disposeEagerly) { + final Action0 unsubscribe = mock(Action0.class); + Func0 resourceFactory = new Func0() { + @Override + public Subscription call() { + return Subscriptions.create(unsubscribe); + } + }; + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Subscription subscription) { + throw new TestException(); + } + }; + + try { + Single.using(resourceFactory, observableFactory, disposeSubscription) + .toBlocking().value(); + fail("Should throw a TestException when the observableFactory throws it"); + } catch (TestException e) { + // Make sure that unsubscribe is called so that users can close + // the resource if some error happens. + verify(unsubscribe).call(); + } + } + + @Test + public void withSingleFactoryErrorInOnSubscribe() { + performTestUsingWithSingleFactoryErrorInOnSubscribe(false); + } + + @Test + public void withSingleFactoryErrorInOnSubscribeDisposeEagerly() { + performTestUsingWithSingleFactoryErrorInOnSubscribe(true); + } + + private void performTestUsingWithSingleFactoryErrorInOnSubscribe(boolean disposeEagerly) { + final Action0 unsubscribe = mock(Action0.class); + Func0 resourceFactory = new Func0() { + @Override + public Subscription call() { + return Subscriptions.create(unsubscribe); + } + }; + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Subscription subscription) { + return Single.create(new Single.OnSubscribe() { + @Override + public void call(SingleSubscriber t1) { + throw new TestException(); + } + }); + } + }; + + try { + Single + .using(resourceFactory, observableFactory, disposeSubscription, disposeEagerly) + .toBlocking().value(); + fail("Should throw a TestException when the observableFactory throws it"); + } catch (TestException e) { + // Make sure that unsubscribe is called so that users can close + // the resource if some error happens. + verify(unsubscribe).call(); + } + } + + @Test + public void disposesEagerlyBeforeCompletion() { + final List events = new ArrayList(); + Func0 resourceFactory = createResourceFactory(events); + final Action1 completion = createOnSuccessAction(events); + final Action0 unsub =createUnsubAction(events); + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Resource resource) { + return Single.just(resource.getTextFromWeb()); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Single observable = Single.using(resourceFactory, observableFactory, + new DisposeAction(), true).doOnUnsubscribe(unsub) + .doOnSuccess(completion); + observable.subscribe(observer); + + assertEquals(Arrays.asList("disposed", "completed", "unsub"), events); + + } + + @Test + public void doesNotDisposesEagerlyBeforeCompletion() { + final List events = new ArrayList(); + Func0 resourceFactory = createResourceFactory(events); + final Action1 completion = createOnSuccessAction(events); + final Action0 unsub =createUnsubAction(events); + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Resource resource) { + return Single.just(resource.getTextFromWeb()); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Single observable = Single.using(resourceFactory, observableFactory, + new DisposeAction(), false).doOnUnsubscribe(unsub) + .doOnSuccess(completion); + observable.subscribe(observer); + + assertEquals(Arrays.asList("completed", "unsub", "disposed"), events); + + } + + + + @Test + public void disposesEagerlyBeforeError() { + final List events = new ArrayList(); + Func0 resourceFactory = createResourceFactory(events); + final Action1 onError = createOnErrorAction(events); + final Action0 unsub = createUnsubAction(events); + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Resource resource) { + return Single.error(new RuntimeException()); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Single observable = Single.using(resourceFactory, observableFactory, + new DisposeAction(), true).doOnUnsubscribe(unsub) + .doOnError(onError); + observable.subscribe(observer); + + assertEquals(Arrays.asList("disposed", "error", "unsub"), events); + + } + + @Test + public void doesNotDisposesEagerlyBeforeError() { + final List events = new ArrayList(); + Func0 resourceFactory = createResourceFactory(events); + final Action1 onError = createOnErrorAction(events); + final Action0 unsub = createUnsubAction(events); + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Resource resource) { + return Single.error(new RuntimeException()); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Single observable = Single.using(resourceFactory, observableFactory, + new DisposeAction(), false).doOnUnsubscribe(unsub) + .doOnError(onError); + observable.subscribe(observer); + + assertEquals(Arrays.asList("error", "unsub", "disposed"), events); + } + + private static Action0 createUnsubAction(final List events) { + return new Action0() { + @Override + public void call() { + events.add("unsub"); + } + }; + } + + private static Action1 createOnErrorAction(final List events) { + return new Action1() { + @Override + public void call(Throwable t) { + events.add("error"); + } + }; + } + + private static Func0 createResourceFactory(final List events) { + return new Func0() { + @Override + public Resource call() { + return new Resource() { + + @Override + public String getTextFromWeb() { + return "hello world"; + } + + @Override + public void dispose() { + events.add("disposed"); + } + }; + } + }; + } + + private static Action1 createOnSuccessAction(final List events) { + return new Action1() { + @Override + public void call(String s) { + events.add("completed"); + } + }; + } + + @Test + public void nullResourceFactory() { + try { + final Resource resource = mock(Resource.class); + when(resource.getTextFromWeb()).thenReturn("Hello world!"); + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Resource resource) { + return Single.just(resource.getTextFromWeb()); + } + }; + + Single.using(null, observableFactory, + new DisposeAction(), false); + + fail("Failed to throw NullPointerException"); + } catch (NullPointerException ex) { + assertEquals("resourceFactory is null", ex.getMessage()); + } + } + + @Test + public void nullSingeFactory() { + try { + final Resource resource = mock(Resource.class); + when(resource.getTextFromWeb()).thenReturn("Hello world!"); + + Func0 resourceFactory = new Func0() { + @Override + public Resource call() { + return resource; + } + }; + + Single.using(resourceFactory, null, + new DisposeAction(), false); + + fail("Failed to throw NullPointerException"); + } catch (NullPointerException ex) { + assertEquals("singleFactory is null", ex.getMessage()); + } + } + + @Test + public void nullDisposeAction() { + try { + final Resource resource = mock(Resource.class); + when(resource.getTextFromWeb()).thenReturn("Hello world!"); + + Func0 resourceFactory = new Func0() { + @Override + public Resource call() { + return resource; + } + }; + + Func1> observableFactory = new Func1>() { + @Override + public Single call(Resource resource) { + return Single.just(resource.getTextFromWeb()); + } + }; + + Single.using(resourceFactory, observableFactory, + null, false); + + fail("Failed to throw NullPointerException"); + } catch (NullPointerException ex) { + assertEquals("disposeAction is null", ex.getMessage()); + } + } + +} From 498c1529559bc28997cf359cf4f4bd29215013e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Sun, 13 Mar 2016 22:55:54 +0100 Subject: [PATCH 007/322] 1.x: fix SerializedObserverTest.testNotificationDelay --- .../rx/observers/SerializedObserverTest.java | 98 +++++++++---------- 1 file changed, 44 insertions(+), 54 deletions(-) diff --git a/src/test/java/rx/observers/SerializedObserverTest.java b/src/test/java/rx/observers/SerializedObserverTest.java index 7f833dda28..a814162695 100644 --- a/src/test/java/rx/observers/SerializedObserverTest.java +++ b/src/test/java/rx/observers/SerializedObserverTest.java @@ -19,7 +19,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; -import java.util.Arrays; +import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; @@ -27,7 +27,9 @@ import org.mockito.*; import rx.*; +import rx.Observable; import rx.Observable.OnSubscribe; +import rx.Observer; import rx.exceptions.TestException; import rx.schedulers.Schedulers; @@ -255,74 +257,62 @@ public void runConcurrencyTest() { * * @throws InterruptedException */ - @Ignore - // this is non-deterministic ... haven't figured out what's wrong with the test yet (benjchristensen: July 2014) @Test public void testNotificationDelay() throws InterruptedException { - ExecutorService tp1 = Executors.newFixedThreadPool(1); - ExecutorService tp2 = Executors.newFixedThreadPool(1); + final ExecutorService tp1 = Executors.newFixedThreadPool(1); try { - int n = 10; + int n = 10000; for (int i = 0; i < n; i++) { - final CountDownLatch firstOnNext = new CountDownLatch(1); - final CountDownLatch onNextCount = new CountDownLatch(2); - final CountDownLatch latch = new CountDownLatch(1); - final CountDownLatch running = new CountDownLatch(2); - - TestSubscriber to = new TestSubscriber(new Observer() { - + + @SuppressWarnings("unchecked") + final Observer[] os = new Observer[1]; + + final List threads = new ArrayList(); + + final Observer o = new SerializedObserver(new Observer() { + boolean first; @Override - public void onCompleted() { - + public void onNext(Integer t) { + threads.add(Thread.currentThread()); + if (!first) { + first = true; + try { + tp1.submit(new Runnable() { + @Override + public void run() { + os[0].onNext(2); + } + }).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } } - + @Override - public void onError(Throwable e) { - + public void onError(Throwable e) { + e.printStackTrace(); } - + @Override - public void onNext(String t) { - firstOnNext.countDown(); - // force it to take time when delivering so the second one is enqueued - try { - latch.await(); - } catch (InterruptedException e) { - } + public void onCompleted() { + } - }); - Observer o = serializedObserver(to); - - Future f1 = tp1.submit(new OnNextThread(o, 1, onNextCount, running)); - Future f2 = tp2.submit(new OnNextThread(o, 1, onNextCount, running)); - - running.await(); // let one of the OnNextThread actually run before proceeding - firstOnNext.await(); - - Thread t1 = to.getLastSeenThread(); - System.out.println("first onNext on thread: " + t1); - - latch.countDown(); - - waitOnThreads(f1, f2); - // not completed yet - - assertEquals(2, to.getOnNextEvents().size()); - - Thread t2 = to.getLastSeenThread(); - System.out.println("second onNext on thread: " + t2); - - assertSame(t1, t2); - - System.out.println(to.getOnNextEvents()); - o.onCompleted(); - System.out.println(to.getOnNextEvents()); + os[0] = o; + + o.onNext(1); + + System.out.println(threads); + assertEquals(2, threads.size()); + + assertSame(threads.get(0), threads.get(1)); } } finally { tp1.shutdown(); - tp2.shutdown(); } } From fcfa4d4062ce26071086901db89c55e54635c4f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Sun, 13 Mar 2016 23:54:41 +0100 Subject: [PATCH 008/322] 1.x: measure flatMap/concatMap performance when used as filter --- .../rx/operators/FlatMapAsFilterPerf.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/perf/java/rx/operators/FlatMapAsFilterPerf.java diff --git a/src/perf/java/rx/operators/FlatMapAsFilterPerf.java b/src/perf/java/rx/operators/FlatMapAsFilterPerf.java new file mode 100644 index 0000000000..e8ca109fdf --- /dev/null +++ b/src/perf/java/rx/operators/FlatMapAsFilterPerf.java @@ -0,0 +1,119 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rx.operators; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import rx.Observable; +import rx.functions.Func1; +import rx.jmh.LatchedObserver; + +/** + * Benchmark flatMap running over a mixture of normal and empty Observables. + *

+ * gradlew benchmarks "-Pjmh=-f 1 -tu s -bm thrpt -wi 5 -i 5 -r 1 .*FlatMapAsFilterPerf.*" + *

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*FlatMapAsFilterPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class FlatMapAsFilterPerf { + + @Param({"1", "1000", "1000000"}) + public int count; + + @Param({"0", "1", "3", "7"}) + public int mask; + + public Observable justEmptyFlatMap; + + public Observable rangeEmptyFlatMap; + + public Observable justEmptyConcatMap; + + public Observable rangeEmptyConcatMap; + + @Setup + public void setup() { + if (count == 1 && mask != 0) { + throw new RuntimeException("Force skip"); + } + Integer[] values = new Integer[count]; + for (int i = 0; i < count; i++) { + values[i] = i; + } + final Observable just = Observable.just(1); + + final Observable range = Observable.range(1, 2); + + final Observable empty = Observable.empty(); + + final int m = mask; + + justEmptyFlatMap = Observable.from(values).flatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return (v & m) == 0 ? empty : just; + } + }); + + rangeEmptyFlatMap = Observable.from(values).flatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return (v & m) == 0 ? empty : range; + } + }); + + justEmptyConcatMap = Observable.from(values).concatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return (v & m) == 0 ? empty : just; + } + }); + + rangeEmptyConcatMap = Observable.from(values).concatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return (v & m) == 0 ? empty : range; + } + }); + } + + @Benchmark + public void justEmptyFlatMap(Blackhole bh) { + justEmptyFlatMap.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void rangeEmptyFlatMap(Blackhole bh) { + rangeEmptyFlatMap.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void justEmptyConcatMap(Blackhole bh) { + justEmptyConcatMap.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void rangeEmptyConcatMap(Blackhole bh) { + rangeEmptyConcatMap.subscribe(new LatchedObserver(bh)); + } +} \ No newline at end of file From a3ca1fd37d79ff9417c756ccf2537be025dfc1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 14 Mar 2016 00:43:33 +0100 Subject: [PATCH 009/322] 1.x: fix attempt 2 for testErrorThrownIssue1685 --- src/test/java/rx/ObservableTests.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index 4b0d2f2330..e0c12b2d37 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -1099,16 +1099,17 @@ public void uncaughtException(Thread t, Throwable e) { } }).get(); + subject.subscribe(); + Observable.error(new RuntimeException("oops")) .materialize() .delay(1, TimeUnit.SECONDS, s) .dematerialize() .subscribe(subject); - subject.subscribe(); subject.materialize().toBlocking().first(); - for (int i = 0; i < 20 && err.get() == null; i++) { + for (int i = 0; i < 50 && err.get() == null; i++) { Thread.sleep(100); // the uncaught exception comes after the terminal event reaches toBlocking } From c1730e3367872fea68d5add758a0b0cfd259d63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 14 Mar 2016 01:49:00 +0100 Subject: [PATCH 010/322] 1.x: clarify join/groupJoin no ordering guarantees --- src/main/java/rx/Observable.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 8f4cb464ee..f3be61283d 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5888,6 +5888,9 @@ public final Observable> groupBy(final Func1 + * There are no guarantees in what order the items get combined when multiple + * items from one or both source Observables overlap. + *

* *

*
Scheduler:
@@ -5959,6 +5962,9 @@ private static class HolderAnyForEmpty { /** * Correlates the items emitted by two Observables based on overlapping durations. *

+ * There are no guarantees in what order the items get combined when multiple + * items from one or both source Observables overlap. + *

* *

*
Scheduler:
From 3e307febd447aad9d83da843fc367c5dedd40197 Mon Sep 17 00:00:00 2001 From: Kirill Boyarshinov Date: Mon, 14 Mar 2016 09:53:31 +0800 Subject: [PATCH 011/322] Operator sample emits last sampled value before termination --- .../OperatorSampleWithObservable.java | 4 +-- .../operators/OperatorSampleWithTime.java | 5 +++ .../operators/OperatorSampleTest.java | 35 ++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java b/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java index 45614dfc28..f8065a610b 100644 --- a/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java +++ b/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java @@ -66,7 +66,7 @@ public void onError(Throwable e) { @Override public void onCompleted() { - // onNext(null); // emit the very last value? + onNext(null); s.onCompleted(); // no need to null check, main is assigned before any of the two gets subscribed main.get().unsubscribe(); @@ -88,7 +88,7 @@ public void onError(Throwable e) { @Override public void onCompleted() { - // samplerSub.onNext(null); // emit the very last value? + samplerSub.onNext(null); s.onCompleted(); samplerSub.unsubscribe(); diff --git a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java index 0fdcbd2c68..39e783062c 100644 --- a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java @@ -89,12 +89,17 @@ public void onError(Throwable e) { @Override public void onCompleted() { + emitIfNonEmpty(); subscriber.onCompleted(); unsubscribe(); } @Override public void call() { + emitIfNonEmpty(); + } + + private void emitIfNonEmpty() { Object localValue = value.getAndSet(EMPTY_TOKEN); if (localValue != EMPTY_TOKEN) { try { diff --git a/src/test/java/rx/internal/operators/OperatorSampleTest.java b/src/test/java/rx/internal/operators/OperatorSampleTest.java index 78d3633d6f..c05abda5d2 100644 --- a/src/test/java/rx/internal/operators/OperatorSampleTest.java +++ b/src/test/java/rx/internal/operators/OperatorSampleTest.java @@ -109,6 +109,39 @@ public void call() { verify(observer, never()).onError(any(Throwable.class)); } + @Test + public void sampleWithTimeEmitAndTerminate() { + Observable source = Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber observer1) { + innerScheduler.schedule(new Action0() { + @Override + public void call() { + observer1.onNext(1L); + } + }, 1, TimeUnit.SECONDS); + innerScheduler.schedule(new Action0() { + @Override + public void call() { + observer1.onNext(2L); + observer1.onCompleted(); + } + }, 2, TimeUnit.SECONDS); + } + }); + + Observable sampled = source.sample(400L, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(2000L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(1L); + inOrder.verify(observer, times(1)).onNext(2L); + verify(observer, times(1)).onCompleted(); + verify(observer, never()).onError(any(Throwable.class)); + } + @Test public void sampleWithSamplerNormal() { PublishSubject source = PublishSubject.create(); @@ -208,7 +241,7 @@ public void sampleWithSamplerEmitAndTerminate() { InOrder inOrder = inOrder(observer2); inOrder.verify(observer2, never()).onNext(1); inOrder.verify(observer2, times(1)).onNext(2); - inOrder.verify(observer2, never()).onNext(3); + inOrder.verify(observer2, times(1)).onNext(3); inOrder.verify(observer2, times(1)).onCompleted(); inOrder.verify(observer2, never()).onNext(any()); verify(observer, never()).onError(any(Throwable.class)); From fbefa23ceb947fd48013029f9831840bd5e7ccee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 14 Mar 2016 13:46:34 +0100 Subject: [PATCH 012/322] 1.x: concatMap full rewrite + delayError + performance --- src/main/java/rx/Observable.java | 74 +++- .../rx/exceptions/CompositeException.java | 41 +- .../operators/OnSubscribeConcatMap.java | 363 ++++++++++++++++++ .../rx/internal/operators/OperatorConcat.java | 241 ------------ .../rx/internal/util/ExceptionsUtils.java | 102 +++++ .../rx/exceptions/CompositeExceptionTest.java | 2 +- .../OnSubscribeConcatDelayErrorTest.java | 197 ++++++++++ .../OperatorRetryWithPredicateTest.java | 2 +- .../operators/OperatorWindowWithSizeTest.java | 1 + 9 files changed, 769 insertions(+), 254 deletions(-) create mode 100644 src/main/java/rx/internal/operators/OnSubscribeConcatMap.java delete mode 100644 src/main/java/rx/internal/operators/OperatorConcat.java create mode 100644 src/main/java/rx/internal/util/ExceptionsUtils.java create mode 100644 src/test/java/rx/internal/operators/OnSubscribeConcatDelayErrorTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 8f4cb464ee..574451fe81 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -922,8 +922,9 @@ public static Observable combineLatest(IterableReactiveX operators documentation: Concat */ + @SuppressWarnings({ "unchecked", "rawtypes" }) public static Observable concat(Observable> observables) { - return observables.lift(OperatorConcat.instance()); + return observables.concatMap((Func1)UtilityFunctions.identity()); } /** @@ -1158,6 +1159,45 @@ public static Observable concat(Observable t1, Observable + *
Backpressure:
+ *
{@code concatDelayError} fully supports backpressure.
+ *
Scheduler:
+ *
{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param sources the Observable sequence of Observables + * @return the new Observable with the concatenating behavior + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Experimental + public static Observable concatDelayError(Observable> sources) { + return sources.concatMapDelayError((Func1)UtilityFunctions.identity()); + } + + /** + * Concatenates the Iterable sequence of Observables into a single sequence by subscribing to each Observable, + * one after the other, one at a time and delays any errors till the all inner Observables terminate. + * + *
+ *
Backpressure:
+ *
{@code concatDelayError} fully supports backpressure.
+ *
Scheduler:
+ *
{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param sources the Iterable sequence of Observables + * @return the new Observable with the concatenating behavior + */ + @Experimental + public static Observable concatDelayError(Iterable> sources) { + return concatDelayError(from(sources)); + } + /** * Returns an Observable that calls an Observable factory to create an Observable for each new Observer * that subscribes. That is, for each subscriber, the actual Observable that subscriber observes is @@ -3957,7 +3997,37 @@ public final R call(R state, T value) { * @see ReactiveX operators documentation: FlatMap */ public final Observable concatMap(Func1> func) { - return concat(map(func)); + if (this instanceof ScalarSynchronousObservable) { + ScalarSynchronousObservable scalar = (ScalarSynchronousObservable) this; + return scalar.scalarFlatMap(func); + } + return create(new OnSubscribeConcatMap(this, func, 2, OnSubscribeConcatMap.IMMEDIATE)); + } + + /** + * Maps each of the items into an Observable, subscribes to them one after the other, + * one at a time and emits their values in order + * while delaying any error from either this or any of the inner Observables + * till all of them terminate. + * + *
+ *
Backpressure:
+ *
{@code concatMapDelayError} fully supports backpressure.
+ *
Scheduler:
+ *
{@code concatMapDelayError} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param the result value type + * @param func the function that maps the items of this Observable into the inner Observables. + * @return the new Observable instance with the concatenation behavior + */ + @Experimental + public final Observable concatMapDelayError(Func1> func) { + if (this instanceof ScalarSynchronousObservable) { + ScalarSynchronousObservable scalar = (ScalarSynchronousObservable) this; + return scalar.scalarFlatMap(func); + } + return create(new OnSubscribeConcatMap(this, func, 2, OnSubscribeConcatMap.END)); } /** diff --git a/src/main/java/rx/exceptions/CompositeException.java b/src/main/java/rx/exceptions/CompositeException.java index 7891c13dd1..58930c061a 100644 --- a/src/main/java/rx/exceptions/CompositeException.java +++ b/src/main/java/rx/exceptions/CompositeException.java @@ -15,15 +15,10 @@ */ package rx.exceptions; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; +import java.io.*; +import java.util.*; + +import rx.annotations.Experimental; /** * Represents an exception that is a composite of one or more other exceptions. A {@code CompositeException} @@ -73,6 +68,34 @@ public CompositeException(Collection errors) { this(null, errors); } + /** + * Constructs a CompositeException instance with the supplied initial Throwables. + * @param errors the array of Throwables + */ + @Experimental + public CompositeException(Throwable... errors) { + Set deDupedExceptions = new LinkedHashSet(); + List _exceptions = new ArrayList(); + if (errors != null) { + for (Throwable ex : errors) { + if (ex instanceof CompositeException) { + deDupedExceptions.addAll(((CompositeException) ex).getExceptions()); + } else + if (ex != null) { + deDupedExceptions.add(ex); + } else { + deDupedExceptions.add(new NullPointerException()); + } + } + } else { + deDupedExceptions.add(new NullPointerException()); + } + + _exceptions.addAll(deDupedExceptions); + this.exceptions = Collections.unmodifiableList(_exceptions); + this.message = exceptions.size() + " exceptions occurred. "; + } + /** * Retrieves the list of exceptions that make up the {@code CompositeException} * diff --git a/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java b/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java new file mode 100644 index 0000000000..001058763b --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java @@ -0,0 +1,363 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package rx.internal.operators; + +import java.util.Queue; +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.Observable.OnSubscribe; +import rx.exceptions.*; +import rx.functions.Func1; +import rx.internal.producers.ProducerArbiter; +import rx.internal.util.*; +import rx.internal.util.atomic.SpscAtomicArrayQueue; +import rx.internal.util.unsafe.*; +import rx.observers.SerializedSubscriber; +import rx.plugins.RxJavaPlugins; +import rx.subscriptions.SerialSubscription; + +/** + * Maps a source sequence into Observables and concatenates them in order, subscribing + * to one at a time. + * @param the source value type + * @param the output value type + * + * @since 1.1.2 + */ +public final class OnSubscribeConcatMap implements OnSubscribe { + final Observable source; + + final Func1> mapper; + + final int prefetch; + + /** + * How to handle errors from the main and inner Observables. + * See the constants below. + */ + final int delayErrorMode; + + /** Whenever any Observable fires an error, terminate with that error immediately. */ + public static final int IMMEDIATE = 0; + + /** Whenever the main fires an error, wait until the inner terminates. */ + public static final int BOUNDARY = 1; + + /** Delay all errors to the very end. */ + public static final int END = 2; + + public OnSubscribeConcatMap(Observable source, Func1> mapper, int prefetch, + int delayErrorMode) { + this.source = source; + this.mapper = mapper; + this.prefetch = prefetch; + this.delayErrorMode = delayErrorMode; + } + + @Override + public void call(Subscriber child) { + Subscriber s; + + if (delayErrorMode == IMMEDIATE) { + s = new SerializedSubscriber(child); + } else { + s = child; + } + + final ConcatMapSubscriber parent = new ConcatMapSubscriber(s, mapper, prefetch, delayErrorMode); + + child.add(parent); + child.add(parent.inner); + child.setProducer(new Producer() { + @Override + public void request(long n) { + parent.requestMore(n); + } + }); + + if (!child.isUnsubscribed()) { + source.unsafeSubscribe(parent); + } + } + + static final class ConcatMapSubscriber extends Subscriber { + final Subscriber actual; + + final Func1> mapper; + + final int delayErrorMode; + + final ProducerArbiter arbiter; + + final Queue queue; + + final AtomicInteger wip; + + final AtomicReference error; + + final SerialSubscription inner; + + volatile boolean done; + + volatile boolean active; + + public ConcatMapSubscriber(Subscriber actual, + Func1> mapper, int prefetch, int delayErrorMode) { + this.actual = actual; + this.mapper = mapper; + this.delayErrorMode = delayErrorMode; + this.arbiter = new ProducerArbiter(); + this.wip = new AtomicInteger(); + this.error = new AtomicReference(); + Queue q; + if (UnsafeAccess.isUnsafeAvailable()) { + q = new SpscArrayQueue(prefetch); + } else { + q = new SpscAtomicArrayQueue(prefetch); + } + this.queue = q; + this.inner = new SerialSubscription(); + this.request(prefetch); + } + + @Override + public void onNext(T t) { + if (!queue.offer(NotificationLite.instance().next(t))) { + unsubscribe(); + onError(new MissingBackpressureException()); + } else { + drain(); + } + } + + @Override + public void onError(Throwable mainError) { + if (ExceptionsUtils.addThrowable(error, mainError)) { + done = true; + if (delayErrorMode == IMMEDIATE) { + Throwable ex = ExceptionsUtils.terminate(error); + if (!ExceptionsUtils.isTerminated(ex)) { + actual.onError(ex); + } + inner.unsubscribe(); + } else { + drain(); + } + } else { + pluginError(mainError); + } + } + + @Override + public void onCompleted() { + done = true; + drain(); + } + + void requestMore(long n) { + if (n > 0) { + arbiter.request(n); + } else + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); + } + } + + void innerNext(R value) { + actual.onNext(value); + } + + void innerError(Throwable innerError, long produced) { + if (!ExceptionsUtils.addThrowable(error, innerError)) { + pluginError(innerError); + } else + if (delayErrorMode == IMMEDIATE) { + Throwable ex = ExceptionsUtils.terminate(error); + if (!ExceptionsUtils.isTerminated(ex)) { + actual.onError(ex); + } + unsubscribe(); + } else { + if (produced != 0L) { + arbiter.produced(produced); + } + active = false; + drain(); + } + } + + void innerCompleted(long produced) { + if (produced != 0L) { + arbiter.produced(produced); + } + active = false; + drain(); + } + + void pluginError(Throwable e) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } + + void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + + final int delayErrorMode = this.delayErrorMode; + + do { + if (actual.isUnsubscribed()) { + return; + } + + if (!active) { + if (delayErrorMode == BOUNDARY) { + if (error.get() != null) { + Throwable ex = ExceptionsUtils.terminate(error); + if (!ExceptionsUtils.isTerminated(ex)) { + actual.onError(ex); + } + return; + } + } + + boolean mainDone = done; + Object v = queue.poll(); + boolean empty = v == null; + + if (mainDone && empty) { + Throwable ex = ExceptionsUtils.terminate(error); + if (ex == null) { + actual.onCompleted(); + } else + if (!ExceptionsUtils.isTerminated(ex)) { + actual.onError(ex); + } + return; + } + + if (!empty) { + + Observable source; + + try { + source = mapper.call(NotificationLite.instance().getValue(v)); + } catch (Throwable mapperError) { + Exceptions.throwIfFatal(mapperError); + drainError(mapperError); + return; + } + + if (source == null) { + drainError(new NullPointerException("The source returned by the mapper was null")); + return; + } + + if (source != Observable.empty()) { + + if (source instanceof ScalarSynchronousObservable) { + ScalarSynchronousObservable scalarSource = (ScalarSynchronousObservable) source; + + arbiter.setProducer(new ConcatMapInnerScalarProducer(scalarSource.get(), this)); + } else { + ConcatMapInnerSubscriber innerSubscriber = new ConcatMapInnerSubscriber(this); + inner.set(innerSubscriber); + + if (!innerSubscriber.isUnsubscribed()) { + active = true; + + source.unsafeSubscribe(innerSubscriber); + } else { + return; + } + } + } + request(1); + } + } + } while (wip.decrementAndGet() != 0); + } + + void drainError(Throwable mapperError) { + unsubscribe(); + + if (ExceptionsUtils.addThrowable(error, mapperError)) { + Throwable ex = ExceptionsUtils.terminate(error); + if (!ExceptionsUtils.isTerminated(ex)) { + actual.onError(ex); + } + } else { + pluginError(mapperError); + } + } + } + + static final class ConcatMapInnerSubscriber extends Subscriber { + final ConcatMapSubscriber parent; + + long produced; + + public ConcatMapInnerSubscriber(ConcatMapSubscriber parent) { + this.parent = parent; + } + + @Override + public void setProducer(Producer p) { + parent.arbiter.setProducer(p); + } + + @Override + public void onNext(R t) { + produced++; + parent.innerNext(t); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e, produced); + } + + @Override + public void onCompleted() { + parent.innerCompleted(produced); + } + } + + static final class ConcatMapInnerScalarProducer implements Producer { + final R value; + + final ConcatMapSubscriber parent; + + boolean once; + + public ConcatMapInnerScalarProducer(R value, ConcatMapSubscriber parent) { + this.value = value; + this.parent = parent; + } + + @Override + public void request(long n) { + if (!once) { + once = true; + ConcatMapSubscriber p = parent; + p.innerNext(value); + p.innerCompleted(1); + } + } + } +} diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java deleted file mode 100644 index e251841f18..0000000000 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package rx.internal.operators; - -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.*; - -import rx.*; -import rx.Observable.Operator; -import rx.functions.Action0; -import rx.internal.producers.ProducerArbiter; -import rx.observers.SerializedSubscriber; -import rx.subscriptions.*; - -/** - * Returns an Observable that emits the items emitted by two or more Observables, one after the other. - *

- * - * - * @param - * the source and result value type - */ -public final class OperatorConcat implements Operator> { - /** Lazy initialization via inner-class holder. */ - private static final class Holder { - /** A singleton instance. */ - static final OperatorConcat INSTANCE = new OperatorConcat(); - } - /** - * @return a singleton instance of this stateless operator. - */ - @SuppressWarnings("unchecked") - public static OperatorConcat instance() { - return (OperatorConcat)Holder.INSTANCE; - } - OperatorConcat() { } - @Override - public Subscriber> call(final Subscriber child) { - final SerializedSubscriber s = new SerializedSubscriber(child); - final SerialSubscription current = new SerialSubscription(); - child.add(current); - ConcatSubscriber cs = new ConcatSubscriber(s, current); - ConcatProducer cp = new ConcatProducer(cs); - child.setProducer(cp); - return cs; - } - - static final class ConcatProducer implements Producer { - final ConcatSubscriber cs; - - ConcatProducer(ConcatSubscriber cs) { - this.cs = cs; - } - - @Override - public void request(long n) { - cs.requestFromChild(n); - } - - } - - static final class ConcatSubscriber extends Subscriber> { - final NotificationLite> nl = NotificationLite.instance(); - private final Subscriber child; - private final SerialSubscription current; - final ConcurrentLinkedQueue queue; - - volatile ConcatInnerSubscriber currentSubscriber; - - final AtomicInteger wip = new AtomicInteger(); - - // accessed by REQUESTED - private final AtomicLong requested = new AtomicLong(); - private final ProducerArbiter arbiter; - - public ConcatSubscriber(Subscriber s, SerialSubscription current) { - super(s); - this.child = s; - this.current = current; - this.arbiter = new ProducerArbiter(); - this.queue = new ConcurrentLinkedQueue(); - add(Subscriptions.create(new Action0() { - @Override - public void call() { - queue.clear(); - } - })); - } - - @Override - public void onStart() { - // no need for more than 1 at a time since we concat 1 at a time, so we'll request 2 to start ... - // 1 to be subscribed to, 1 in the queue, then we'll keep requesting 1 at a time after that - request(2); - } - - private void requestFromChild(long n) { - if (n <= 0) return; - // we track 'requested' so we know whether we should subscribe the next or not - - final AtomicLong requestedField = requested; - - long previous; - - if (requestedField.get() != Long.MAX_VALUE) { - previous = BackpressureUtils.getAndAddRequest(requestedField, n); - } else { - previous = Long.MAX_VALUE; - } - - arbiter.request(n); - if (previous == 0) { - if (currentSubscriber == null && wip.get() > 0) { - // this means we may be moving from one subscriber to another after having stopped processing - // so need to kick off the subscribe via this request notification - subscribeNext(); - } - } - } - - @Override - public void onNext(Observable t) { - queue.add(nl.next(t)); - if (wip.getAndIncrement() == 0) { - subscribeNext(); - } - } - - @Override - public void onError(Throwable e) { - child.onError(e); - unsubscribe(); - } - - @Override - public void onCompleted() { - queue.add(nl.completed()); - if (wip.getAndIncrement() == 0) { - subscribeNext(); - } - } - - - void completeInner() { - currentSubscriber = null; - if (wip.decrementAndGet() > 0) { - subscribeNext(); - } - request(1); - } - - void subscribeNext() { - if (requested.get() > 0) { - Object o = queue.poll(); - if (nl.isCompleted(o)) { - child.onCompleted(); - } else if (o != null) { - Observable obs = nl.getValue(o); - - currentSubscriber = new ConcatInnerSubscriber(this, child, arbiter); - current.set(currentSubscriber); - - obs.unsafeSubscribe(currentSubscriber); - } - } else { - // requested == 0, so we'll peek to see if we are completed, otherwise wait until another request - Object o = queue.peek(); - if (nl.isCompleted(o)) { - child.onCompleted(); - } - } - } - - void produced(long c) { - if (c != 0L) { - arbiter.produced(c); - BackpressureUtils.produced(requested, c); - } - } - } - - static class ConcatInnerSubscriber extends Subscriber { - - private final Subscriber child; - private final ConcatSubscriber parent; - private final AtomicBoolean once = new AtomicBoolean(); - private final ProducerArbiter arbiter; - - long produced; - - public ConcatInnerSubscriber(ConcatSubscriber parent, Subscriber child, ProducerArbiter arbiter) { - this.parent = parent; - this.child = child; - this.arbiter = arbiter; - } - - @Override - public void onNext(T t) { - produced++; - - child.onNext(t); - } - - @Override - public void onError(Throwable e) { - if (once.compareAndSet(false, true)) { - // terminal error through parent so everything gets cleaned up, including this inner - parent.onError(e); - } - } - - @Override - public void onCompleted() { - if (once.compareAndSet(false, true)) { - ConcatSubscriber p = parent; - // signal the production count at once instead of one by one - p.produced(produced); - // terminal completion to parent so it continues to the next - p.completeInner(); - } - } - - @Override - public void setProducer(Producer producer) { - arbiter.setProducer(producer); - } - } -} diff --git a/src/main/java/rx/internal/util/ExceptionsUtils.java b/src/main/java/rx/internal/util/ExceptionsUtils.java new file mode 100644 index 0000000000..b714e7525a --- /dev/null +++ b/src/main/java/rx/internal/util/ExceptionsUtils.java @@ -0,0 +1,102 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package rx.internal.util; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +import rx.exceptions.CompositeException; + +/** + * Utility methods for terminal atomics with Throwables. + * + * @since 1.1.2 + */ +public enum ExceptionsUtils { + ; + + /** The single instance of a Throwable indicating a terminal state. */ + private static final Throwable TERMINATED = new Throwable("Terminated"); + + /** + * Atomically sets or combines the error with the contents of the field, wrapping multiple + * errors into CompositeException if necessary. + * + * @param field the target field + * @param error the error to add + * @return true if successful, false if the target field contains the terminal Throwable. + */ + public static boolean addThrowable(AtomicReference field, Throwable error) { + for (;;) { + Throwable current = field.get(); + if (current == TERMINATED) { + return false; + } + + Throwable next; + if (current == null) { + next = error; + } else + if (current instanceof CompositeException) { + List list = new ArrayList(((CompositeException)current).getExceptions()); + list.add(error); + next = new CompositeException(list); + } else { + next = new CompositeException(current, error); + } + + if (field.compareAndSet(current, next)) { + return true; + } + } + } + + /** + * Atomically swaps in the terminal Throwable and returns the previous + * contents of the field + * + * @param field the target field + * @return the previous contents of the field before the swap, may be null + */ + public static Throwable terminate(AtomicReference field) { + Throwable current = field.get(); + if (current != TERMINATED) { + current = field.getAndSet(TERMINATED); + } + return current; + } + + /** + * Checks if the given field holds the terminated Throwable instance. + * + * @param field the target field + * @return true if the given field holds the terminated Throwable instance + */ + public static boolean isTerminated(AtomicReference field) { + return isTerminated(field.get()); + } + + /** + * Returns true if the value is the terminated Throwable instance. + * + * @param error the error to check + * @return true if the value is the terminated Throwable instance + */ + public static boolean isTerminated(Throwable error) { + return error == TERMINATED; + } +} diff --git a/src/test/java/rx/exceptions/CompositeExceptionTest.java b/src/test/java/rx/exceptions/CompositeExceptionTest.java index fc28e5b21b..ec3bd7b6c5 100644 --- a/src/test/java/rx/exceptions/CompositeExceptionTest.java +++ b/src/test/java/rx/exceptions/CompositeExceptionTest.java @@ -168,7 +168,7 @@ private static Throwable getRootCause(Throwable ex) { @Test public void testNullCollection() { - CompositeException composite = new CompositeException(null); + CompositeException composite = new CompositeException((List)null); composite.getCause(); composite.printStackTrace(); } diff --git a/src/test/java/rx/internal/operators/OnSubscribeConcatDelayErrorTest.java b/src/test/java/rx/internal/operators/OnSubscribeConcatDelayErrorTest.java new file mode 100644 index 0000000000..86e929d8de --- /dev/null +++ b/src/test/java/rx/internal/operators/OnSubscribeConcatDelayErrorTest.java @@ -0,0 +1,197 @@ +package rx.internal.operators; + +import org.junit.Test; + +import rx.Observable; +import rx.exceptions.*; +import rx.functions.Func1; +import rx.observers.TestSubscriber; +import rx.subjects.PublishSubject; + +public class OnSubscribeConcatDelayErrorTest { + + @Test + public void mainCompletes() { + PublishSubject source = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + source.concatMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2); + } + }).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onCompleted(); + + ts.assertValues(1, 2, 2, 3); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void mainErrors() { + PublishSubject source = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + source.concatMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2); + } + }).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.assertValues(1, 2, 2, 3); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void innerErrors() { + final Observable inner = Observable.range(1, 2).concatWith(Observable.error(new TestException())); + + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(1, 3).concatMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return inner; + } + }).subscribe(ts); + + ts.assertValues(1, 2, 1, 2, 1, 2); + ts.assertError(CompositeException.class); + ts.assertNotCompleted(); + } + + @Test + public void singleInnerErrors() { + final Observable inner = Observable.range(1, 2).concatWith(Observable.error(new TestException())); + + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1) + .asObservable() // prevent scalar optimization + .concatMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return inner; + } + }).subscribe(ts); + + ts.assertValues(1, 2); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void innerNull() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1) + .asObservable() // prevent scalar optimization + .concatMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return null; + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(NullPointerException.class); + ts.assertNotCompleted(); + } + + @Test + public void innerThrows() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1) + .asObservable() // prevent scalar optimization + .concatMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void innerWithEmpty() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(1, 3) + .concatMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return v == 2 ? Observable.empty() : Observable.range(1, 2); + } + }).subscribe(ts); + + ts.assertValues(1, 2, 1, 2); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void innerWithScalar() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(1, 3) + .concatMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return v == 2 ? Observable.just(3) : Observable.range(1, 2); + } + }).subscribe(ts); + + ts.assertValues(1, 2, 3, 1, 2); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void backpressure() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.range(1, 3).concatMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(3); + ts.assertValues(1, 2, 2, 3); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(1, 2, 2, 3, 3, 4); + ts.assertNoErrors(); + ts.assertCompleted(); + } + +} diff --git a/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java b/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java index df878de13a..c3d438b200 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java @@ -393,6 +393,6 @@ public Boolean call(Integer t1, Throwable t2) { assertEquals(Arrays.asList(3L, 2L, 1L), requests); ts.assertValues(1, 1, 1); ts.assertNotCompleted(); - ts.assertNoErrors(); + ts.assertError(TestException.class); } } diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java index 9dade31fbc..60e63fe34f 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java @@ -291,6 +291,7 @@ public Observable call(Observable w) { Assert.assertFalse(ts.getOnNextEvents().isEmpty()); } + @Ignore("Requires #3678") @Test @SuppressWarnings("unchecked") public void testBackpressureOuterInexact() { From 4b80956ab3f2feafbe0fe195e6bcdd1a165d0cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 14 Mar 2016 17:21:04 +0100 Subject: [PATCH 013/322] 1.x: combineLatestDelayError --- src/main/java/rx/Observable.java | 27 +++++ .../operators/OnSubscribeCombineLatest.java | 2 +- .../OnSubscribeCombineLatestTest.java | 105 +++++++++++++++++- 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 8f4cb464ee..42c3406237 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -906,6 +906,33 @@ public static Observable combineLatest(Iterable(sources, combineFunction)); } + /** + * Combines a collection of source Observables by emitting an item that aggregates the latest values of each of + * the source Observables each time an item is received from any of the source Observables, where this + * aggregation is defined by a specified function and delays any error from the sources until + * all source Observables terminate. + * + *
+ *
Scheduler:
+ *
{@code combineLatest} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the common base type of source values + * @param + * the result type + * @param sources + * the collection of source Observables + * @param combineFunction + * the aggregation function used to combine the items emitted by the source Observables + * @return an Observable that emits items that are the result of combining the items emitted by the source + * Observables by means of the given aggregation function + * @see ReactiveX operators documentation: CombineLatest + */ + public static Observable combineLatestDelayError(Iterable> sources, FuncN combineFunction) { + return create(new OnSubscribeCombineLatest(null, sources, combineFunction, RxRingBuffer.SIZE, true)); + } + /** * Returns an Observable that emits the items emitted by each of the Observables emitted by the source * Observable, one after the other, without interleaving them. diff --git a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java index 152a0831b0..93dcb5de5d 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java +++ b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java @@ -213,7 +213,7 @@ void combine(Object value, int index) { if (value != null && allSourcesFinished) { queue.offer(combinerSubscriber, latest.clone()); } else - if (value == null && error.get() != null) { + if (value == null && error.get() != null && (o == MISSING || !delayError)) { done = true; // if this source completed without a value } } else { diff --git a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java index 67085640e2..840077af59 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java @@ -29,6 +29,7 @@ import rx.*; import rx.Observable; import rx.Observer; +import rx.exceptions.*; import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; @@ -954,5 +955,107 @@ public Integer call(Object... args) { throw new RuntimeException(); } - }; + }; + + @SuppressWarnings("unchecked") + @Test + public void firstJustError() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.combineLatestDelayError( + Arrays.asList(Observable.just(1), Observable.error(new TestException())), + new FuncN() { + @Override + public Integer call(Object... args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @SuppressWarnings("unchecked") + @Test + public void secondJustError() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.combineLatestDelayError( + Arrays.asList(Observable.error(new TestException()), Observable.just(1)), + new FuncN() { + @Override + public Integer call(Object... args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @SuppressWarnings("unchecked") + @Test + public void oneErrors() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.combineLatestDelayError( + Arrays.asList(Observable.just(10).concatWith(Observable.error(new TestException())), Observable.just(1)), + new FuncN() { + @Override + public Integer call(Object... args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertValues(11); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @SuppressWarnings("unchecked") + @Test + public void twoErrors() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.combineLatestDelayError( + Arrays.asList(Observable.just(1), Observable.just(10).concatWith(Observable.error(new TestException()))), + new FuncN() { + @Override + public Integer call(Object... args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertValues(11); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @SuppressWarnings("unchecked") + @Test + public void bothError() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.combineLatestDelayError( + Arrays.asList(Observable.just(1).concatWith(Observable.error(new TestException())), + Observable.just(10).concatWith(Observable.error(new TestException()))), + new FuncN() { + @Override + public Integer call(Object... args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertValues(11); + ts.assertError(CompositeException.class); + ts.assertNotCompleted(); + } + } From 92a255c259ef3451c37f0294de0f53eec1f4d727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Tue, 15 Mar 2016 00:23:20 +0100 Subject: [PATCH 014/322] 1.x: switchOnNextDelayError and switchMapDelayError --- src/main/java/rx/Observable.java | 55 ++- .../rx/internal/operators/OperatorSwitch.java | 343 +++++++++++------- .../operators/OperatorSwitchTest.java | 169 +++++++-- 3 files changed, 401 insertions(+), 166 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index f3be61283d..5b474e7791 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -2814,7 +2814,36 @@ public static Observable sequenceEqual(Observable firs * @see ReactiveX operators documentation: Switch */ public static Observable switchOnNext(Observable> sequenceOfSequences) { - return sequenceOfSequences.lift(OperatorSwitch.instance()); + return sequenceOfSequences.lift(OperatorSwitch.instance(false)); + } + + /** + * Converts an Observable that emits Observables into an Observable that emits the items emitted by the + * most recently emitted of those Observables and delays any exception until all Observables terminate. + *

+ * + *

+ * {@code switchOnNext} subscribes to an Observable that emits Observables. Each time it observes one of + * these emitted Observables, the Observable returned by {@code switchOnNext} begins emitting the items + * emitted by that Observable. When a new Observable is emitted, {@code switchOnNext} stops emitting items + * from the earlier-emitted Observable and begins emitting items from the new one. + *

+ *
Scheduler:
+ *
{@code switchOnNext} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param the item type + * @param sequenceOfSequences + * the source Observable that emits Observables + * @return an Observable that emits the items emitted by the Observable most recently emitted by the source + * Observable + * @see ReactiveX operators documentation: Switch + * @Experimental The behavior of this can change at any time. + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public static Observable switchOnNextDelayError(Observable> sequenceOfSequences) { + return sequenceOfSequences.lift(OperatorSwitch.instance(true)); } /** @@ -8637,6 +8666,30 @@ public final Observable switchMap(Func1 + * + *
+ *
Scheduler:
+ *
{@code switchMap} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param func + * a function that, when applied to an item emitted by the source Observable, returns an + * Observable + * @return an Observable that emits the items emitted by the Observable returned from applying {@code func} to the most recently emitted item emitted by the source Observable + * @see ReactiveX operators documentation: FlatMap + * @Experimental The behavior of this can change at any time. + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public final Observable switchMapDelayError(Func1> func) { + return switchOnNextDelayError(map(func)); + } + /** * Returns an Observable that emits only the first {@code count} items emitted by the source Observable. If the source emits fewer than * {@code count} items then all of its items are emitted. diff --git a/src/main/java/rx/internal/operators/OperatorSwitch.java b/src/main/java/rx/internal/operators/OperatorSwitch.java index 5f95f38c3d..7d706f2a95 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitch.java +++ b/src/main/java/rx/internal/operators/OperatorSwitch.java @@ -15,15 +15,14 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import rx.*; import rx.Observable; import rx.Observable.Operator; -import rx.Producer; -import rx.Subscriber; +import rx.exceptions.CompositeException; import rx.internal.producers.ProducerArbiter; -import rx.observers.SerializedSubscriber; +import rx.plugins.RxJavaPlugins; import rx.subscriptions.SerialSubscription; /** @@ -38,49 +37,67 @@ public final class OperatorSwitch implements Operator INSTANCE = new OperatorSwitch(); + static final OperatorSwitch INSTANCE = new OperatorSwitch(false); + } + /** Lazy initialization via inner-class holder. */ + private static final class HolderDelayError { + /** A singleton instance. */ + static final OperatorSwitch INSTANCE = new OperatorSwitch(true); } /** * @return a singleton instance of this stateless operator. */ @SuppressWarnings({ "unchecked" }) - public static OperatorSwitch instance() { + public static OperatorSwitch instance(boolean delayError) { + if (delayError) { + return (OperatorSwitch)HolderDelayError.INSTANCE; + } return (OperatorSwitch)Holder.INSTANCE; } - OperatorSwitch() { } + final boolean delayError; + + OperatorSwitch(boolean delayError) { + this.delayError = delayError; + } @Override public Subscriber> call(final Subscriber child) { - SwitchSubscriber sws = new SwitchSubscriber(child); + SwitchSubscriber sws = new SwitchSubscriber(child, delayError); child.add(sws); + sws.init(); return sws; } private static final class SwitchSubscriber extends Subscriber> { - final SerializedSubscriber serializedChild; + final Subscriber child; final SerialSubscription ssub; - final Object guard = new Object(); - final NotificationLite nl = NotificationLite.instance(); final ProducerArbiter arbiter; - /** Guarded by guard. */ - int index; - /** Guarded by guard. */ - boolean active; - /** Guarded by guard. */ + final boolean delayError; + + long index; + + Throwable error; + boolean mainDone; - /** Guarded by guard. */ - List queue; - /** Guarded by guard. */ + + List queue; + + boolean innerActive; + boolean emitting; - /** Guarded by guard. */ - InnerSubscriber currentSubscriber; + + boolean missed; - SwitchSubscriber(Subscriber child) { - serializedChild = new SerializedSubscriber(child); - arbiter = new ProducerArbiter(); - ssub = new SerialSubscription(); + SwitchSubscriber(Subscriber child, boolean delayError) { + this.child = child; + this.arbiter = new ProducerArbiter(); + this.ssub = new SerialSubscription(); + this.delayError = delayError; + } + + void init() { child.add(ssub); child.setProducer(new Producer(){ @@ -95,186 +112,232 @@ public void request(long n) { @Override public void onNext(Observable t) { - final int id; - synchronized (guard) { - id = ++index; - active = true; - currentSubscriber = new InnerSubscriber(id, arbiter, this); + InnerSubscriber inner; + synchronized (this) { + long id = ++index; + inner = new InnerSubscriber(id, this); + innerActive = true; } - ssub.set(currentSubscriber); - t.unsafeSubscribe(currentSubscriber); + ssub.set(inner); + + t.unsafeSubscribe(inner); } @Override public void onError(Throwable e) { - serializedChild.onError(e); - unsubscribe(); + synchronized (this) { + e = updateError(e); + mainDone = true; + + if (emitting) { + missed = true; + return; + } + if (delayError && innerActive) { + return; + } + emitting = true; + } + + child.onError(e); } @Override public void onCompleted() { - List localQueue; - synchronized (guard) { + Throwable ex; + synchronized (this) { mainDone = true; - if (active) { + if (emitting) { + missed = true; return; } - if (emitting) { - if (queue == null) { - queue = new ArrayList(); - } - queue.add(nl.completed()); + if (innerActive) { return; } - localQueue = queue; - queue = null; emitting = true; + ex = error; } - drain(localQueue); - serializedChild.onCompleted(); - unsubscribe(); + if (ex == null) { + child.onCompleted(); + } else { + child.onError(ex); + } + } + + Throwable updateError(Throwable e) { + Throwable ex = error; + if (ex == null) { + error = e; + } else + if (ex instanceof CompositeException) { + CompositeException ce = (CompositeException) ex; + List list = new ArrayList(ce.getExceptions()); + list.add(e); + e = new CompositeException(list); + error = e; + } else { + e = new CompositeException(Arrays.asList(ex, e)); + error = e; + } + return e; } - void emit(T value, int id, InnerSubscriber innerSubscriber) { - List localQueue; - synchronized (guard) { + + void emit(T value, long id) { + synchronized (this) { if (id != index) { return; } + if (emitting) { - if (queue == null) { - queue = new ArrayList(); + List q = queue; + if (q == null) { + q = new ArrayList(4); + queue = q; } - queue.add(value); + q.add(value); + missed = true; return; } - localQueue = queue; - queue = null; + emitting = true; } - boolean once = true; - boolean skipFinal = false; - try { - do { - drain(localQueue); - if (once) { - once = false; - serializedChild.onNext(value); - arbiter.produced(1); + + child.onNext(value); + + arbiter.produced(1); + + for (;;) { + if (child.isUnsubscribed()) { + return; + } + + Throwable localError; + boolean localMainDone; + boolean localActive; + List localQueue; + synchronized (this) { + if (!missed) { + emitting = false; + return; } - synchronized (guard) { - localQueue = queue; - queue = null; - if (localQueue == null) { - emitting = false; - skipFinal = true; - break; - } + + localError = error; + localMainDone = mainDone; + localQueue = queue; + localActive = innerActive; + } + + if (!delayError && localError != null) { + child.onError(localError); + return; + } + + if (localQueue == null && !localActive && localMainDone) { + if (localError != null) { + child.onError(localError); + } else { + child.onCompleted(); } - } while (!serializedChild.isUnsubscribed()); - } finally { - if (!skipFinal) { - synchronized (guard) { - emitting = false; + return; + } + + if (localQueue != null) { + int n = 0; + for (T v : localQueue) { + if (child.isUnsubscribed()) { + return; + } + + child.onNext(v); + n++; } + + arbiter.produced(n); } } } - void drain(List localQueue) { - if (localQueue == null) { - return; - } - for (Object o : localQueue) { - if (nl.isCompleted(o)) { - serializedChild.onCompleted(); - break; - } else - if (nl.isError(o)) { - serializedChild.onError(nl.getError(o)); - break; + + void error(Throwable e, long id) { + boolean drop; + synchronized (this) { + if (id == index) { + innerActive = false; + + e = updateError(e); + + if (emitting) { + missed = true; + return; + } + if (delayError && !mainDone) { + return; + } + emitting = true; + + drop = false; } else { - @SuppressWarnings("unchecked") - T t = (T)o; - serializedChild.onNext(t); - arbiter.produced(1); + drop = true; } } + + if (drop) { + pluginError(e); + } else { + child.onError(e); + } } - - void error(Throwable e, int id) { - List localQueue; - synchronized (guard) { + + void complete(long id) { + Throwable ex; + synchronized (this) { if (id != index) { return; } + innerActive = false; + if (emitting) { - if (queue == null) { - queue = new ArrayList(); - } - queue.add(nl.error(e)); + missed = true; return; } + + ex = error; - localQueue = queue; - queue = null; - emitting = true; - } - - drain(localQueue); - serializedChild.onError(e); - unsubscribe(); - } - void complete(int id) { - List localQueue; - synchronized (guard) { - if (id != index) { - return; - } - active = false; if (!mainDone) { return; } - if (emitting) { - if (queue == null) { - queue = new ArrayList(); - } - queue.add(nl.completed()); - return; - } - - localQueue = queue; - queue = null; - emitting = true; } - - drain(localQueue); - serializedChild.onCompleted(); - unsubscribe(); + + if (ex != null) { + child.onError(ex); + } else { + child.onCompleted(); + } } + void pluginError(Throwable e) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } } private static final class InnerSubscriber extends Subscriber { - private final int id; - - private final ProducerArbiter arbiter; + private final long id; private final SwitchSubscriber parent; - InnerSubscriber(int id, ProducerArbiter arbiter, SwitchSubscriber parent) { + InnerSubscriber(long id, SwitchSubscriber parent) { this.id = id; - this.arbiter = arbiter; this.parent = parent; } @Override public void setProducer(Producer p) { - arbiter.setProducer(p); + parent.arbiter.setProducer(p); } @Override public void onNext(T t) { - parent.emit(t, id, this); + parent.emit(t, id); } @Override diff --git a/src/test/java/rx/internal/operators/OperatorSwitchTest.java b/src/test/java/rx/internal/operators/OperatorSwitchTest.java index 63de5d0d81..55170ab9ff 100644 --- a/src/test/java/rx/internal/operators/OperatorSwitchTest.java +++ b/src/test/java/rx/internal/operators/OperatorSwitchTest.java @@ -15,38 +15,25 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; import org.mockito.InOrder; +import rx.*; import rx.Observable; import rx.Observer; -import rx.Producer; -import rx.Scheduler; -import rx.Subscriber; -import rx.exceptions.TestException; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; +import rx.exceptions.*; +import rx.functions.*; import rx.observers.TestSubscriber; import rx.schedulers.TestScheduler; +import rx.subjects.PublishSubject; public class OperatorSwitchTest { @@ -667,8 +654,140 @@ public Observable call(Long t) { ts.requestMore(Long.MAX_VALUE - 1); ts.awaitTerminalEvent(); assertTrue(ts.getOnNextEvents().size() > 0); - assertEquals(5, (int) requests.size()); + assertEquals(5, requests.size()); assertEquals(Long.MAX_VALUE, (long) requests.get(requests.size()-1)); } + @Test + public void mainError() { + TestSubscriber ts = TestSubscriber.create(); + + PublishSubject source = PublishSubject.create(); + + source.switchMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2); + } + }).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.assertValues(1, 2, 2, 3); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void innerError() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(0, 3).switchMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return v == 1 ? Observable.error(new TestException()) : Observable.range(v, 2); + } + }).subscribe(ts); + + ts.assertValues(0, 1, 2, 3); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void innerAllError() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(0, 3).switchMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2).concatWith(Observable.error(new TestException())); + } + }).subscribe(ts); + + ts.assertValues(0, 1, 1, 2, 2, 3); + ts.assertError(CompositeException.class); + ts.assertNotCompleted(); + + List exceptions = ((CompositeException)ts.getOnErrorEvents().get(0)).getExceptions(); + + assertEquals(3, exceptions.size()); + + for (Throwable ex : exceptions) { + assertTrue(ex.toString(), ex instanceof TestException); + } + } + + @Test + public void backpressure() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.range(0, 3).switchMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(2, 3); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void backpressureWithSwitch() { + TestSubscriber ts = TestSubscriber.create(0); + + PublishSubject source = PublishSubject.create(); + + source.switchMapDelayError(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + source.onNext(0); + + ts.assertValues(0); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + source.onNext(1); + + ts.assertValues(0); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValues(0, 1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + source.onNext(2); + + ts.requestMore(2); + + source.onCompleted(); + + ts.assertValues(0, 1, 2, 3); + ts.assertNoErrors(); + ts.assertCompleted(); + } + } From 049504469f1b9fbd228f4e5c7c80b7c160597510 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Tue, 15 Mar 2016 02:37:09 +0300 Subject: [PATCH 015/322] 1.x: Add Single.onErrorResumeNext(Func) --- src/main/java/rx/Single.java | 39 ++++++++++- src/main/java/rx/exceptions/Exceptions.java | 15 +++++ .../SingleOperatorOnErrorResumeNext.java | 65 ++++++++++++++++++ ...gleOperatorOnErrorResumeNextViaSingle.java | 45 ------------- src/test/java/rx/SingleTest.java | 67 ++++++++++++++++++- 5 files changed, 184 insertions(+), 47 deletions(-) create mode 100644 src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNext.java delete mode 100644 src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNextViaSingle.java diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index bacdd90667..4303f2dd52 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1430,9 +1430,46 @@ public final Single onErrorReturn(Func1 resumeFunctio * @param resumeSingleInCaseOfError a Single that will take control if source Single encounters an error. * @return the original Single, with appropriately modified behavior. * @see ReactiveX operators documentation: Catch + * @Experimental The behavior of this can change at any time. + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ + @Experimental public final Single onErrorResumeNext(Single resumeSingleInCaseOfError) { - return new Single(new SingleOperatorOnErrorResumeNextViaSingle(this, resumeSingleInCaseOfError)); + return new Single(SingleOperatorOnErrorResumeNext.withOther(this, resumeSingleInCaseOfError)); + } + + /** + * Instructs a Single to pass control to another Single rather than invoking + * {@link Observer#onError(Throwable)} if it encounters an error. + *

+ * + *

+ * By default, when a Single encounters an error that prevents it from emitting the expected item to + * its {@link Observer}, the Single invokes its Observer's {@code onError} method, and then quits + * without invoking any more of its Observer's methods. The {@code onErrorResumeNext} method changes this + * behavior. If you pass a function that will return another Single ({@code resumeFunctionInCaseOfError}) to an Single's + * {@code onErrorResumeNext} method, if the original Single encounters an error, instead of invoking its + * Observer's {@code onError} method, it will instead relinquish control to {@code resumeSingleInCaseOfError} which + * will invoke the Observer's {@link Observer#onNext onNext} method if it is able to do so. In such a case, + * because no Single necessarily invokes {@code onError}, the Observer may never know that an error + * happened. + *

+ * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + *

+ *
Scheduler:
+ *
{@code onErrorResumeNext} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param resumeFunctionInCaseOfError a function that returns a Single that will take control if source Single encounters an error. + * @return the original Single, with appropriately modified behavior. + * @see ReactiveX operators documentation: Catch + * @Experimental The behavior of this can change at any time. + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public final Single onErrorResumeNext(final Func1> resumeFunctionInCaseOfError) { + return new Single(SingleOperatorOnErrorResumeNext.withFunction(this, resumeFunctionInCaseOfError)); } /** diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index 081e4830a8..2b94504c08 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -18,6 +18,7 @@ import java.util.*; import rx.Observer; +import rx.SingleSubscriber; import rx.annotations.Experimental; /** @@ -188,6 +189,7 @@ public static void throwOrReport(Throwable t, Observer o, Object value) { Exceptions.throwIfFatal(t); o.onError(OnErrorThrowable.addValueAsLastCause(t, value)); } + /** * Forwards a fatal exception or reports it to the given Observer. * @param t the exception @@ -199,4 +201,17 @@ public static void throwOrReport(Throwable t, Observer o) { Exceptions.throwIfFatal(t); o.onError(t); } + + /** + * Forwards a fatal exception or reports it to the given Observer. + * + * @param throwable the exception. + * @param subscriber the subscriber to report to. + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number). + */ + @Experimental + public static void throwOrReport(Throwable throwable, SingleSubscriber subscriber) { + Exceptions.throwIfFatal(throwable); + subscriber.onError(throwable); + } } diff --git a/src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNext.java b/src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNext.java new file mode 100644 index 0000000000..584551376c --- /dev/null +++ b/src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNext.java @@ -0,0 +1,65 @@ +package rx.internal.operators; + +import rx.Single; +import rx.SingleSubscriber; +import rx.exceptions.Exceptions; +import rx.functions.Func1; +import rx.plugins.RxJavaPlugins; + +public class SingleOperatorOnErrorResumeNext implements Single.OnSubscribe { + + private final Single originalSingle; + private final Func1> resumeFunctionInCaseOfError; + + private SingleOperatorOnErrorResumeNext(Single originalSingle, Func1> resumeFunctionInCaseOfError) { + if (originalSingle == null) { + throw new NullPointerException("originalSingle must not be null"); + } + + if (resumeFunctionInCaseOfError == null) { + throw new NullPointerException("resumeFunctionInCaseOfError must not be null"); + } + + this.originalSingle = originalSingle; + this.resumeFunctionInCaseOfError = resumeFunctionInCaseOfError; + } + + public static SingleOperatorOnErrorResumeNext withFunction(Single originalSingle, Func1> resumeFunctionInCaseOfError) { + return new SingleOperatorOnErrorResumeNext(originalSingle, resumeFunctionInCaseOfError); + } + + public static SingleOperatorOnErrorResumeNext withOther(Single originalSingle, final Single resumeSingleInCaseOfError) { + if (resumeSingleInCaseOfError == null) { + throw new NullPointerException("resumeSingleInCaseOfError must not be null"); + } + + return new SingleOperatorOnErrorResumeNext(originalSingle, new Func1>() { + @Override + public Single call(Throwable throwable) { + return resumeSingleInCaseOfError; + } + }); + } + + @Override + public void call(final SingleSubscriber child) { + final SingleSubscriber parent = new SingleSubscriber() { + @Override + public void onSuccess(T value) { + child.onSuccess(value); + } + + @Override + public void onError(Throwable error) { + try { + resumeFunctionInCaseOfError.call(error).subscribe(child); + } catch (Throwable innerError) { + Exceptions.throwOrReport(innerError, child); + } + } + }; + + child.add(parent); + originalSingle.subscribe(parent); + } +} diff --git a/src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNextViaSingle.java b/src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNextViaSingle.java deleted file mode 100644 index ca47f9c3e9..0000000000 --- a/src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNextViaSingle.java +++ /dev/null @@ -1,45 +0,0 @@ -package rx.internal.operators; - -import rx.Single; -import rx.SingleSubscriber; -import rx.plugins.RxJavaPlugins; - -public class SingleOperatorOnErrorResumeNextViaSingle implements Single.OnSubscribe { - - private final Single originalSingle; - private final Single resumeSingleInCaseOfError; - - public SingleOperatorOnErrorResumeNextViaSingle(Single originalSingle, Single resumeSingleInCaseOfError) { - if (originalSingle == null) { - throw new NullPointerException("originalSingle must not be null"); - } - - if (resumeSingleInCaseOfError == null) { - throw new NullPointerException("resumeSingleInCaseOfError must not be null"); - } - - this.originalSingle = originalSingle; - this.resumeSingleInCaseOfError = resumeSingleInCaseOfError; - } - - @Override - public void call(final SingleSubscriber child) { - final SingleSubscriber parent = new SingleSubscriber() { - @Override - public void onSuccess(T value) { - child.onSuccess(value); - } - - @Override - public void onError(Throwable error) { - RxJavaPlugins.getInstance().getErrorHandler().handleError(error); - unsubscribe(); - - resumeSingleInCaseOfError.subscribe(child); - } - }; - - child.add(parent); - originalSingle.subscribe(parent); - } -} diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index d2457da4e9..4cd6cda14d 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -1220,13 +1220,78 @@ public void onErrorResumeNextViaSingleShouldPreventNullSingle() { try { Single .just("value") - .onErrorResumeNext(null); + .onErrorResumeNext((Single) null); fail(); } catch (NullPointerException expected) { assertEquals("resumeSingleInCaseOfError must not be null", expected.getMessage()); } } + @Test + public void onErrorResumeNextViaFunctionShouldNotInterruptSuccesfulSingle() { + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .just("success") + .onErrorResumeNext(new Func1>() { + @Override + public Single call(Throwable throwable) { + return Single.just("fail"); + } + }) + .subscribe(testSubscriber); + + testSubscriber.assertValue("success"); + } + + @Test + public void onErrorResumeNextViaFunctionShouldResumeWithPassedSingleInCaseOfError() { + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + . error(new RuntimeException("test exception")) + .onErrorResumeNext(new Func1>() { + @Override + public Single call(Throwable throwable) { + return Single.just("fallback"); + } + }) + .subscribe(testSubscriber); + + testSubscriber.assertValue("fallback"); + } + + @Test + public void onErrorResumeNextViaFunctionShouldPreventNullFunction() { + try { + Single + .just("value") + .onErrorResumeNext((Func1>) null); + fail(); + } catch (NullPointerException expected) { + assertEquals("resumeFunctionInCaseOfError must not be null", expected.getMessage()); + } + } + + @Test + public void onErrorResumeNextViaFunctionShouldFailIfFunctionReturnsNull() { + try { + Single + .error(new TestException()) + .onErrorResumeNext(new Func1>() { + @Override + public Single call(Throwable throwable) { + return null; + } + }) + .subscribe(); + + fail(); + } catch (OnErrorNotImplementedException expected) { + assertTrue(expected.getCause() instanceof NullPointerException); + } + } + @Test(expected = NullPointerException.class) public void iterableToArrayShouldThrowNullPointerExceptionIfIterableNull() { Single.iterableToArray(null); From 651a49216bf71329d6448171ec7932ae77d76c02 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Tue, 15 Mar 2016 03:09:36 +0300 Subject: [PATCH 016/322] 1.x: Update Gradle wrapper to 2.12 --- gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53639 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5ccda13e9cb94678ba179b32452cf3d60dc36353..2c6137b87896c8f70315ae454e00a969ef5f6019 100644 GIT binary patch delta 1762 zcmY*Z3rv$&6u$inv`UK;1cg>AAP<3I*QyZ#iC_c)5wQ50V{{aR$v}Zv1viU2n4rAw zHXk9|7`Qrh0WIp7z(AnoQJ^@Taf|a2Ky)&#+2S6eyZ^ZaY?J0Y_xrzd&i9{t+M-%+ zaV=LE7tOVri4dQUq%m2QLN7jn$jkc8K9xaR9n3lA91fb6coNBJH!cfCAAsjl7O*ep z9*a6VCYJ%?kktbqvaIWX&^huQY=H5zyG0q^Y^gOcE1W7Q(?4$`4;Zfn8yz6nFBecv z*>WdaV6@@SXF^aDdz%(4Oytq@(oKncK5-G5byoW!9(y<9ji>AU6QoPxr45a;WtU`2 z6gV_lHe()9e0DOx*@W|xJ@zjxZ^`PA3J$4Tqh=RYi36P*^Zepe8K#S-S>rwp3&X39 zuKZ}+>)vk3-r#Ei%4f$sxB9LaS)HujDXe^7zUybEDXb?bcx~Y`;brDnieS8Bhu^@# zi)Z9XTNK{gM>K{StzFB8klihJ?`O`x`sU5gV-}8QjAZ)j*LUVPyIrqWC5`6yt(%p0 z#!9U_neDrDxGwN_=a*k;wk^K$kGyU~?NHyU+9nJB^N}>+YkRTL^G?swiAc@;FTQL~ z`1XawRDG*RRQ%WZ;oFL92X>j6^@g&SuiX}TQM^~_&n2ikt^9;x11wiP1VWPf3J9HB z`a>EBcVG@Ys?C(}A?V7Ja3Of04x)i)!B5t}{HOVsivK=vg9nVMWQa0#N6s>K?2tb` z)i`&%Jwke4EG<}opXS-<4wkF!K|N7prd`c-cWH24d&vqO9X-dT&2arw`l#r_JGAtu zZWYz|es7}8M3aJQ6wR2+XS+6(y0oqhaBl8O1e~L%byfNlIQQyfrgz!Zu=cgJ-DwD62Zb99BF+ccXmEwoxIx5J zE3tII8JmOq(M($4;qUt9gR}lV5%c%} zu0H3E1x8q5>}C`(ohA5AN$}LL4-@M65lHSf${=xqP;1Hw<%16o(kqGY7cu46L2-sK*z`-)^Mgj{S93bIJ-#)}7{ zz{0)(5mR`Mcn_F*_e*UJxyMPrGh_uUZ=|?>s-Jk!o!-izh{?Y|XfYO)&SGB{JckcC zjXol?+ecbkuF)?#sBv@9N5XoObLlMC-@c~YRNFxkX96ALjV35h+ zD2{+Zvr%sKpq9kbB<)Nun7`{umQR(Dsi}T|C`9JO>Vw(zJA~TI_KVuYjpZG z+B8T*o6JW@BtrITb&jc0L_i%~`zkKSYp2zVgy#u7G$%19lCotq1Dz`XUaAwwT(i>w5|IGYWyjL<^G2gcLpdzR^1yh8|#Qoh3q7N^|BtmgcB zn+3p>`n{YFi{dRqY{1k|A!|SPd8kN4s!)f^PcFq{d;J&2YXXB+l|ib?8aGv?n@14# ziEx`o6GiTzhieZ`j&L~To$VXfBp0Vmy}5Wp^hl6PU;14cSf?F4LOr=2!c)lmPR{1u zDu|oX7Zv@Lr+RI)lv?8i#nYqH7K;7@PqaF;TsM|BDF|A<&pCZVYww=A@fnfdZ+xlzjFDU^>CNsOu?nmF*6<(c_Rciezti0&#Gq>uXKk((<6E5o#Z*5wiMSJ#WJQ>MRNPjTyoj+O%YOZ#EY@Y zxE8V(YIpUNlAf;92(9O6CQ~5$Pev)squVHg(uq1!|U1A7>LvfxWxfaC^-+{d|q|wvzPb&IvbN3|`e$ z%T+-d9<_*OKk7`6oR^AY8r5N5$y(?44abxtArU4B*)KrIi(@cgRd)as_f5BiN+~D3 ze)#SWRk(?6uIMXX&PSPF)48_qzEw&>=iDo+C#Q(aQ2$x`Orv#GZ_eiJ# zJv27Z;|K?akyk!5&^N@pf#a28S+5#w2YV&d^gVVS_br&S2D*dL{ Date: Tue, 15 Mar 2016 11:17:00 +0100 Subject: [PATCH 017/322] 1.x: observeOn - fix in-sequence termination/unsubscription --- .../internal/operators/OperatorObserveOn.java | 22 +++++++++--------- .../operators/OperatorObserveOnTest.java | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index 98464efb89..51d6fc7a23 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -183,15 +183,10 @@ public void call() { // less frequently (usually after each RxRingBuffer.SIZE elements) for (;;) { - if (checkTerminated(finished, q.isEmpty(), localChild, q)) { - return; - } - long requestAmount = requested.get(); - boolean unbounded = requestAmount == Long.MAX_VALUE; long currentEmission = 0L; - while (requestAmount != 0L) { + while (requestAmount != currentEmission) { boolean done = finished; Object v = q.poll(); boolean empty = v == null; @@ -205,14 +200,19 @@ public void call() { } localChild.onNext(localOn.getValue(v)); - - requestAmount--; - currentEmission--; + + currentEmission++; emitted++; } - if (currentEmission != 0L && !unbounded) { - requested.addAndGet(currentEmission); + if (requestAmount == currentEmission) { + if (checkTerminated(finished, q.isEmpty(), localChild, q)) { + return; + } + } + + if (currentEmission != 0L) { + BackpressureUtils.produced(requested, currentEmission); } missed = counter.addAndGet(-missed); diff --git a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java index 0b4b98bc8e..d0ba44be23 100644 --- a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java @@ -834,4 +834,27 @@ public void testErrorDelayedAsync() { ts.assertError(TestException.class); ts.assertNotCompleted(); } + + @Test + public void requestExactCompletesImmediately() { +TestSubscriber ts = TestSubscriber.create(0); + + TestScheduler test = Schedulers.test(); + + Observable.range(1, 10).observeOn(test).subscribe(ts); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(10); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValueCount(10); + ts.assertNoErrors(); + ts.assertCompleted(); + } } From 0b8344b09068e2a97e4ac7fbe38c6b1f8e50aea1 Mon Sep 17 00:00:00 2001 From: Pawel Hajduk Date: Thu, 11 Feb 2016 22:41:55 +0100 Subject: [PATCH 018/322] 1.x: Added Single execution hooks 1.x: Enabled Single onSubscribeStart hook 1.x: Added more Single hooks unit tests --- src/main/java/rx/Single.java | 18 ++- src/main/java/rx/plugins/RxJavaPlugins.java | 44 +++++++ .../rx/plugins/RxJavaSingleExecutionHook.java | 120 ++++++++++++++++++ .../RxJavaSingleExecutionHookDefault.java | 28 ++++ src/test/java/rx/SingleTest.java | 104 ++++++++++++++- .../java/rx/plugins/RxJavaPluginsTest.java | 15 +++ 6 files changed, 313 insertions(+), 16 deletions(-) create mode 100644 src/main/java/rx/plugins/RxJavaSingleExecutionHook.java create mode 100644 src/main/java/rx/plugins/RxJavaSingleExecutionHookDefault.java diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index bacdd90667..d81bd3bd9f 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -29,6 +29,7 @@ import rx.observers.SerializedSubscriber; import rx.plugins.RxJavaObservableExecutionHook; import rx.plugins.RxJavaPlugins; +import rx.plugins.RxJavaSingleExecutionHook; import rx.schedulers.Schedulers; import rx.singles.BlockingSingle; import rx.subscriptions.Subscriptions; @@ -101,7 +102,7 @@ private Single(final Observable.OnSubscribe f) { this.onSubscribe = f; } - static final RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); + static RxJavaSingleExecutionHook hook = RxJavaPlugins.getInstance().getSingleExecutionHook(); /** * Returns a Single that will execute the specified function when a {@link SingleSubscriber} executes it or @@ -130,7 +131,7 @@ private Single(final Observable.OnSubscribe f) { * @see ReactiveX operators documentation: Create */ public static Single create(OnSubscribe f) { - return new Single(f); // TODO need hook + return new Single(hook.onCreate(f)); } /** @@ -1570,14 +1571,12 @@ public final void onNext(T args) { * @param subscriber * the Subscriber that will handle the emission or notification from the Single */ - public final void unsafeSubscribe(Subscriber subscriber) { + public final Subscription unsafeSubscribe(Subscriber subscriber) { try { // new Subscriber so onStart it subscriber.onStart(); - // TODO add back the hook - // hook.onSubscribeStart(this, onSubscribe).call(subscriber); - onSubscribe.call(subscriber); - hook.onSubscribeReturn(subscriber); + hook.onSubscribeStart(this, onSubscribe).call(subscriber); + return hook.onSubscribeReturn(subscriber); } catch (Throwable e) { // special handling for certain Throwable/Error/Exception types Exceptions.throwIfFatal(e); @@ -1594,6 +1593,7 @@ public final void unsafeSubscribe(Subscriber subscriber) { // TODO why aren't we throwing the hook's return value. throw r; } + return Subscriptions.unsubscribed(); } } @@ -1685,9 +1685,7 @@ public final Subscription subscribe(Subscriber subscriber) { // The code below is exactly the same an unsafeSubscribe but not used because it would add a sigificent depth to alreay huge call stacks. try { // allow the hook to intercept and/or decorate - // TODO add back the hook - // hook.onSubscribeStart(this, onSubscribe).call(subscriber); - onSubscribe.call(subscriber); + hook.onSubscribeStart(this, onSubscribe).call(subscriber); return hook.onSubscribeReturn(subscriber); } catch (Throwable e) { // special handling for certain Throwable/Error/Exception types diff --git a/src/main/java/rx/plugins/RxJavaPlugins.java b/src/main/java/rx/plugins/RxJavaPlugins.java index 09e542779d..9678a32e15 100644 --- a/src/main/java/rx/plugins/RxJavaPlugins.java +++ b/src/main/java/rx/plugins/RxJavaPlugins.java @@ -50,6 +50,7 @@ public class RxJavaPlugins { private final AtomicReference errorHandler = new AtomicReference(); private final AtomicReference observableExecutionHook = new AtomicReference(); + private final AtomicReference singleExecutionHook = new AtomicReference(); private final AtomicReference schedulersHook = new AtomicReference(); /** @@ -68,6 +69,7 @@ public static RxJavaPlugins getInstance() { /* package accessible for unit tests */void reset() { INSTANCE.errorHandler.set(null); INSTANCE.observableExecutionHook.set(null); + INSTANCE.singleExecutionHook.set(null); INSTANCE.schedulersHook.set(null); } @@ -156,6 +158,48 @@ public void registerObservableExecutionHook(RxJavaObservableExecutionHook impl) } } + /** + * Retrieves the instance of {@link RxJavaSingleExecutionHook} to use based on order of precedence as + * defined in {@link RxJavaPlugins} class header. + *

+ * Override the default by calling {@link #registerSingleExecutionHook(RxJavaSingleExecutionHook)} + * or by setting the property {@code rxjava.plugin.RxJavaSingleExecutionHook.implementation} with the + * full classname to load. + * + * @return {@link RxJavaSingleExecutionHook} implementation to use + */ + public RxJavaSingleExecutionHook getSingleExecutionHook() { + if (singleExecutionHook.get() == null) { + // check for an implementation from System.getProperty first + Object impl = getPluginImplementationViaProperty(RxJavaSingleExecutionHook.class, System.getProperties()); + if (impl == null) { + // nothing set via properties so initialize with default + singleExecutionHook.compareAndSet(null, RxJavaSingleExecutionHookDefault.getInstance()); + // we don't return from here but call get() again in case of thread-race so the winner will always get returned + } else { + // we received an implementation from the system property so use it + singleExecutionHook.compareAndSet(null, (RxJavaSingleExecutionHook) impl); + } + } + return singleExecutionHook.get(); + } + + /** + * Register an {@link RxJavaSingleExecutionHook} implementation as a global override of any injected or + * default implementations. + * + * @param impl + * {@link RxJavaSingleExecutionHook} implementation + * @throws IllegalStateException + * if called more than once or after the default was initialized (if usage occurs before trying + * to register) + */ + public void registerSingleExecutionHook(RxJavaSingleExecutionHook impl) { + if (!singleExecutionHook.compareAndSet(null, impl)) { + throw new IllegalStateException("Another strategy was already registered: " + singleExecutionHook.get()); + } + } + /* test */ static Object getPluginImplementationViaProperty(Class pluginClass, Properties props) { final String classSimpleName = pluginClass.getSimpleName(); /* diff --git a/src/main/java/rx/plugins/RxJavaSingleExecutionHook.java b/src/main/java/rx/plugins/RxJavaSingleExecutionHook.java new file mode 100644 index 0000000000..9fce6531f3 --- /dev/null +++ b/src/main/java/rx/plugins/RxJavaSingleExecutionHook.java @@ -0,0 +1,120 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.plugins; + +import rx.Observable; +import rx.Single; +import rx.Subscriber; +import rx.Subscription; +import rx.functions.Func1; + +/** + * Abstract ExecutionHook with invocations at different lifecycle points of {@link Single} execution with a + * default no-op implementation. + *

+ * See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: + * https://github.com/ReactiveX/RxJava/wiki/Plugins. + *

+ * Note on thread-safety and performance: + *

+ * A single implementation of this class will be used globally so methods on this class will be invoked + * concurrently from multiple threads so all functionality must be thread-safe. + *

+ * Methods are also invoked synchronously and will add to execution time of the single so all behavior + * should be fast. If anything time-consuming is to be done it should be spawned asynchronously onto separate + * worker threads. + * + */ +public abstract class RxJavaSingleExecutionHook { + /** + * Invoked during the construction by {@link Single#create(Single.OnSubscribe)} + *

+ * This can be used to decorate or replace the onSubscribe function or just perform extra + * logging, metrics and other such things and pass-thru the function. + * + * @param f + * original {@link Single.OnSubscribe}<{@code T}> to be executed + * @return {@link Single.OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or just + * returned as a pass-thru + */ + public Single.OnSubscribe onCreate(Single.OnSubscribe f) { + return f; + } + + /** + * Invoked before {@link Single#subscribe(Subscriber)} is about to be executed. + *

+ * This can be used to decorate or replace the onSubscribe function or just perform extra + * logging, metrics and other such things and pass-thru the function. + * + * @param onSubscribe + * original {@link Observable.OnSubscribe}<{@code T}> to be executed + * @return {@link Observable.OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or just + * returned as a pass-thru + */ + public Observable.OnSubscribe onSubscribeStart(Single singleInstance, final Observable.OnSubscribe onSubscribe) { + // pass-thru by default + return onSubscribe; + } + + /** + * Invoked after successful execution of {@link Single#subscribe(Subscriber)} with returned + * {@link Subscription}. + *

+ * This can be used to decorate or replace the {@link Subscription} instance or just perform extra logging, + * metrics and other such things and pass-thru the subscription. + * + * @param subscription + * original {@link Subscription} + * @return {@link Subscription} subscription that can be modified, decorated, replaced or just returned as a + * pass-thru + */ + public Subscription onSubscribeReturn(Subscription subscription) { + // pass-thru by default + return subscription; + } + + /** + * Invoked after failed execution of {@link Single#subscribe(Subscriber)} with thrown Throwable. + *

+ * This is not errors emitted via {@link Subscriber#onError(Throwable)} but exceptions thrown when + * attempting to subscribe to a {@link Func1}<{@link Subscriber}{@code }, {@link Subscription}>. + * + * @param e + * Throwable thrown by {@link Single#subscribe(Subscriber)} + * @return Throwable that can be decorated, replaced or just returned as a pass-thru + */ + public Throwable onSubscribeError(Throwable e) { + // pass-thru by default + return e; + } + + /** + * Invoked just as the operator functions is called to bind two operations together into a new + * {@link Single} and the return value is used as the lifted function + *

+ * This can be used to decorate or replace the {@link Observable.Operator} instance or just perform extra + * logging, metrics and other such things and pass-thru the onSubscribe. + * + * @param lift + * original {@link Observable.Operator}{@code } + * @return {@link Observable.Operator}{@code } function that can be modified, decorated, replaced or just + * returned as a pass-thru + */ + public Observable.Operator onLift(final Observable.Operator lift) { + return lift; + } +} diff --git a/src/main/java/rx/plugins/RxJavaSingleExecutionHookDefault.java b/src/main/java/rx/plugins/RxJavaSingleExecutionHookDefault.java new file mode 100644 index 0000000000..60a382589f --- /dev/null +++ b/src/main/java/rx/plugins/RxJavaSingleExecutionHookDefault.java @@ -0,0 +1,28 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.plugins; + +/** + * Default no-op implementation of {@link RxJavaSingleExecutionHook} + */ +class RxJavaSingleExecutionHookDefault extends RxJavaSingleExecutionHook { + + private static final RxJavaSingleExecutionHookDefault INSTANCE = new RxJavaSingleExecutionHookDefault(); + + public static RxJavaSingleExecutionHook getInstance() { + return INSTANCE; + } +} diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index d2457da4e9..ce34b6a2fe 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -21,6 +21,7 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.*; +import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -28,14 +29,28 @@ import rx.Single.OnSubscribe; import rx.exceptions.*; import rx.functions.*; +import rx.observers.SafeSubscriber; import rx.observers.TestSubscriber; import rx.schedulers.*; +import rx.plugins.RxJavaPluginsTest; +import rx.plugins.RxJavaSingleExecutionHook; +import rx.schedulers.Schedulers; +import rx.schedulers.TestScheduler; import rx.singles.BlockingSingle; import rx.subjects.PublishSubject; import rx.subscriptions.Subscriptions; public class SingleTest { + private static RxJavaSingleExecutionHook hookSpy; + + @Before + public void setUp() throws Exception { + hookSpy = spy( + new RxJavaPluginsTest.RxJavaSingleExecutionHookTestImpl()); + Single.hook = hookSpy; + } + @Test public void testHelloWorld() { TestSubscriber ts = new TestSubscriber(); @@ -359,6 +374,83 @@ public void testMergeWith() { ts.assertReceivedOnNext(Arrays.asList("A", "B")); } + @Test + public void testHookCreate() { + OnSubscribe subscriber = mock(OnSubscribe.class); + Single.create(subscriber); + + verify(hookSpy, times(1)).onCreate(subscriber); + } + + @Test + public void testHookSubscribeStart() { + TestSubscriber ts = new TestSubscriber(); + + Single single = Single.create(new OnSubscribe() { + @Override public void call(SingleSubscriber s) { + s.onSuccess("Hello"); + } + }); + single.subscribe(ts); + + verify(hookSpy, times(1)).onSubscribeStart(eq(single), any(Observable.OnSubscribe.class)); + } + + @Test + public void testHookUnsafeSubscribeStart() { + TestSubscriber ts = new TestSubscriber(); + Single single = Single.create(new OnSubscribe() { + @Override public void call(SingleSubscriber s) { + s.onSuccess("Hello"); + } + }); + single.unsafeSubscribe(ts); + + verify(hookSpy, times(1)).onSubscribeStart(eq(single), any(Observable.OnSubscribe.class)); + } + + @Test + public void testHookSubscribeReturn() { + TestSubscriber ts = new TestSubscriber(); + + Single single = Single.create(new OnSubscribe() { + @Override public void call(SingleSubscriber s) { + s.onSuccess("Hello"); + } + }); + single.subscribe(ts); + + verify(hookSpy, times(1)).onSubscribeReturn(any(SafeSubscriber.class)); + } + + @Test + public void testHookUnsafeSubscribeReturn() { + TestSubscriber ts = new TestSubscriber(); + + Single single = Single.create(new OnSubscribe() { + @Override public void call(SingleSubscriber s) { + s.onSuccess("Hello"); + } + }); + single.unsafeSubscribe(ts); + + verify(hookSpy, times(1)).onSubscribeReturn(ts); + } + + @Test + public void testReturnUnsubscribedWhenHookThrowsError() { + TestSubscriber ts = new TestSubscriber(); + + Single single = Single.create(new OnSubscribe() { + @Override public void call(SingleSubscriber s) { + throw new RuntimeException("Exception"); + } + }); + Subscription subscription = single.unsafeSubscribe(ts); + + assertTrue(subscription.isUnsubscribed()); + } + @Test public void testCreateSuccess() { TestSubscriber ts = new TestSubscriber(); @@ -1680,14 +1772,14 @@ public void takeUntilError_withSingle_shouldMatch() { assertFalse(until.hasObservers()); assertFalse(ts.isUnsubscribed()); } - + @Test public void subscribeWithObserver() { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - + Single.just(1).subscribe(o); - + verify(o).onNext(1); verify(o).onCompleted(); verify(o, never()).onError(any(Throwable.class)); @@ -1697,14 +1789,14 @@ public void subscribeWithObserver() { public void subscribeWithObserverAndGetError() { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - + Single.error(new TestException()).subscribe(o); - + verify(o, never()).onNext(anyInt()); verify(o, never()).onCompleted(); verify(o).onError(any(TestException.class)); } - + @Test public void subscribeWithNullObserver() { try { diff --git a/src/test/java/rx/plugins/RxJavaPluginsTest.java b/src/test/java/rx/plugins/RxJavaPluginsTest.java index e4cd9f69ae..64a1ba1d1a 100644 --- a/src/test/java/rx/plugins/RxJavaPluginsTest.java +++ b/src/test/java/rx/plugins/RxJavaPluginsTest.java @@ -16,6 +16,7 @@ package rx.plugins; import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; import java.util.*; import java.util.concurrent.TimeUnit; @@ -107,6 +108,15 @@ public void testObservableExecutionHookViaRegisterMethod() { assertTrue(impl instanceof RxJavaObservableExecutionHookTestImpl); } + @Test + public void testSingleExecutionHookViaRegisterMethod() { + RxJavaPlugins p = new RxJavaPlugins(); + RxJavaSingleExecutionHook customHook = mock(RxJavaSingleExecutionHook.class); + p.registerSingleExecutionHook(customHook); + RxJavaSingleExecutionHook impl = p.getSingleExecutionHook(); + assertSame(impl, customHook); + } + @Test public void testObservableExecutionHookViaProperty() { try { @@ -238,6 +248,11 @@ public static class RxJavaObservableExecutionHookTestImpl extends RxJavaObservab // just use defaults } + // inside test so it is stripped from Javadocs + public static class RxJavaSingleExecutionHookTestImpl extends RxJavaSingleExecutionHook { + // just use defaults + } + private static String getFullClassNameForTestClass(Class cls) { return RxJavaPlugins.class.getPackage() .getName() + "." + RxJavaPluginsTest.class.getSimpleName() + "$" + cls.getSimpleName(); From e4598a501907b78ec05908ffbca274eb9ea29ac9 Mon Sep 17 00:00:00 2001 From: Galo Navarro Date: Mon, 2 Nov 2015 20:56:50 +0100 Subject: [PATCH 019/322] OnBackpressureBuffer: DROP_LATEST and DROP_OLDEST Introduce a new interface BackpressureOverflow.Strategy that allows implementing different handlers for an overflow situation. This patch adds three implementations: - ON_OVERFLOW_ERROR remains the default as the existing implementation. - ON_OVERFLOW_DROP_LATEST will drop newly produced items after the buffer fills up. - ON_OVERFLOW_DROP_OLDEST will drop the oldest elements in the buffer, making room for newer ones. The default strategy remains ON_OVERFLOW_ERROR. In all cases, a drop will result in a notification to the producer by invoking the onOverflow callback. None of the two new behaviours (ON_OVERFLOW_DROP_*) will unsubscribe from the source nor onError. Fixes: #3233 --- src/main/java/rx/BackpressureOverflow.java | 90 ++++++++++++ src/main/java/rx/Observable.java | 42 +++++- .../OperatorOnBackpressureBuffer.java | 88 +++++++++--- .../OperatorOnBackpressureBufferTest.java | 132 +++++++++++++++--- ...nExceptionResumeNextViaObservableTest.java | 4 +- 5 files changed, 308 insertions(+), 48 deletions(-) create mode 100644 src/main/java/rx/BackpressureOverflow.java diff --git a/src/main/java/rx/BackpressureOverflow.java b/src/main/java/rx/BackpressureOverflow.java new file mode 100644 index 0000000000..325cc7d0c9 --- /dev/null +++ b/src/main/java/rx/BackpressureOverflow.java @@ -0,0 +1,90 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx; + +import rx.annotations.Experimental; +import rx.exceptions.MissingBackpressureException; + +/** + * Generic strategy and default implementations to deal with backpressure buffer overflows. + */ +@Experimental +public final class BackpressureOverflow { + + public interface Strategy { + + /** + * Whether the Backpressure manager should attempt to drop the oldest item, or simply + * drop the item currently causing backpressure. + * + * @return true to request drop of the oldest item, false to drop the newest. + * @throws MissingBackpressureException + */ + boolean mayAttemptDrop() throws MissingBackpressureException; + } + + public static final BackpressureOverflow.Strategy ON_OVERFLOW_DEFAULT = Error.INSTANCE; + @SuppressWarnings("unused") + public static final BackpressureOverflow.Strategy ON_OVERFLOW_ERROR = Error.INSTANCE; + @SuppressWarnings("unused") + public static final BackpressureOverflow.Strategy ON_OVERFLOW_DROP_OLDEST = DropOldest.INSTANCE; + @SuppressWarnings("unused") + public static final BackpressureOverflow.Strategy ON_OVERFLOW_DROP_LATEST = DropLatest.INSTANCE; + + /** + * Drop oldest items from the buffer making room for newer ones. + */ + static class DropOldest implements BackpressureOverflow.Strategy { + static final DropOldest INSTANCE = new DropOldest(); + + private DropOldest() {} + + @Override + public boolean mayAttemptDrop() { + return true; + } + } + + /** + * Drop most recent items, but not {@code onError} nor unsubscribe from source + * (as {code OperatorOnBackpressureDrop}). + */ + static class DropLatest implements BackpressureOverflow.Strategy { + static final DropLatest INSTANCE = new DropLatest(); + + private DropLatest() {} + + @Override + public boolean mayAttemptDrop() { + return false; + } + } + + /** + * {@code onError} a MissingBackpressureException and unsubscribe from source. + */ + static class Error implements BackpressureOverflow.Strategy { + + static final Error INSTANCE = new Error(); + + private Error() {} + + @Override + public boolean mayAttemptDrop() throws MissingBackpressureException { + throw new MissingBackpressureException("Overflowed buffer"); + } + } +} diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 93b91a8332..08335eb0ef 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -6399,7 +6399,8 @@ public final Observable onBackpressureBuffer() { *

{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.
* * - * @return the source Observable modified to buffer items up to the given capacity + * @param capacity number of slots available in the buffer. + * @return the source {@code Observable} modified to buffer items up to the given capacity. * @see ReactiveX operators documentation: backpressure operators * @since 1.1.0 */ @@ -6419,7 +6420,9 @@ public final Observable onBackpressureBuffer(long capacity) { *
{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.
* * - * @return the source Observable modified to buffer items up to the given capacity + * @param capacity number of slots available in the buffer. + * @param onOverflow action to execute if an item needs to be buffered, but there are no available slots. Null is allowed. + * @return the source {@code Observable} modified to buffer items up to the given capacity * @see ReactiveX operators documentation: backpressure operators * @since 1.1.0 */ @@ -6427,6 +6430,41 @@ public final Observable onBackpressureBuffer(long capacity, Action0 onOverflo return lift(new OperatorOnBackpressureBuffer(capacity, onOverflow)); } + /** + * Instructs an Observable that is emitting items faster than its observer can consume them to buffer up to + * a given amount of items until they can be emitted. The resulting Observable will behave as determined + * by {@code overflowStrategy} if the buffer capacity is exceeded. + * + *
    + *
  • {@code BackpressureOverflow.Strategy.ON_OVERFLOW_ERROR} (default) will {@code onError} dropping all undelivered items, + * unsubscribing from the source, and notifying the producer with {@code onOverflow}.
  • + *
  • {@code BackpressureOverflow.Strategy.ON_OVERFLOW_DROP_LATEST} will drop any new items emitted by the producer while + * the buffer is full, without generating any {@code onError}. Each drop will however invoke {@code onOverflow} + * to signal the overflow to the producer.
  • j + *
  • {@code BackpressureOverflow.Strategy.ON_OVERFLOW_DROP_OLDEST} will drop the oldest items in the buffer in order to make + * room for newly emitted ones. Overflow will not generate an{@code onError}, but each drop will invoke + * {@code onOverflow} to signal the overflow to the producer.
  • + *
+ * + *

+ * + *

+ *
Scheduler:
+ *
{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param capacity number of slots available in the buffer. + * @param onOverflow action to execute if an item needs to be buffered, but there are no available slots. Null is allowed. + * @param overflowStrategy how should the {@code Observable} react to buffer overflows. Null is not allowed. + * @return the source {@code Observable} modified to buffer items up to the given capacity + * @see ReactiveX operators documentation: backpressure operators + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public final Observable onBackpressureBuffer(long capacity, Action0 onOverflow, BackpressureOverflow.Strategy overflowStrategy) { + return lift(new OperatorOnBackpressureBuffer(capacity, onOverflow, overflowStrategy)); + } + /** * Instructs an Observable that is emitting items faster than its observer can consume them to discard, * rather than emit, those items that its observer is not prepared to observe. diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java index 9ab8f82869..4f66bbb4d7 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -19,6 +19,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import rx.BackpressureOverflow; import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; @@ -27,15 +28,18 @@ import rx.functions.Action0; import rx.internal.util.BackpressureDrainManager; +import static rx.BackpressureOverflow.*; + public class OperatorOnBackpressureBuffer implements Operator { private final Long capacity; private final Action0 onOverflow; + private final BackpressureOverflow.Strategy overflowStrategy; private static class Holder { static final OperatorOnBackpressureBuffer INSTANCE = new OperatorOnBackpressureBuffer(); } - + @SuppressWarnings("unchecked") public static OperatorOnBackpressureBuffer instance() { return (OperatorOnBackpressureBuffer) Holder.INSTANCE; @@ -44,18 +48,48 @@ public static OperatorOnBackpressureBuffer instance() { OperatorOnBackpressureBuffer() { this.capacity = null; this.onOverflow = null; + this.overflowStrategy = ON_OVERFLOW_DEFAULT; } + /** + * Construct a new instance that will handle overflows with {@code ON_OVERFLOW_DEFAULT}, providing the + * following behavior config: + * + * @param capacity the max number of items to be admitted in the buffer, must be greater than 0. + */ public OperatorOnBackpressureBuffer(long capacity) { - this(capacity, null); + this(capacity, null, ON_OVERFLOW_DEFAULT); } + /** + * Construct a new instance that will handle overflows with {@code ON_OVERFLOW_DEFAULT}, providing the + * following behavior config: + * + * @param capacity the max number of items to be admitted in the buffer, must be greater than 0. + * @param onOverflow the {@code Action0} to execute when the buffer overflows, may be null. + */ public OperatorOnBackpressureBuffer(long capacity, Action0 onOverflow) { + this(capacity, onOverflow, ON_OVERFLOW_DEFAULT); + } + + /** + * Construct a new instance feeding the following behavior config: + * + * @param capacity the max number of items to be admitted in the buffer, must be greater than 0. + * @param onOverflow the {@code Action0} to execute when the buffer overflows, may be null. + * @param overflowStrategy the {@code BackpressureOverflow.Strategy} to handle overflows, it must not be null. + */ + public OperatorOnBackpressureBuffer(long capacity, Action0 onOverflow, + BackpressureOverflow.Strategy overflowStrategy) { if (capacity <= 0) { throw new IllegalArgumentException("Buffer capacity must be > 0"); } + if (overflowStrategy == null) { + throw new NullPointerException("The BackpressureOverflow strategy must not be null"); + } this.capacity = capacity; this.onOverflow = onOverflow; + this.overflowStrategy = overflowStrategy; } @Override @@ -63,7 +97,8 @@ public Subscriber call(final Subscriber child) { // don't pass through subscriber as we are async and doing queue draining // a parent being unsubscribed should not affect the children - BufferSubscriber parent = new BufferSubscriber(child, capacity, onOverflow); + BufferSubscriber parent = new BufferSubscriber(child, capacity, onOverflow, + overflowStrategy); // if child unsubscribes it should unsubscribe the parent, but not the other way around child.add(parent); @@ -71,6 +106,7 @@ public Subscriber call(final Subscriber child) { return parent; } + private static final class BufferSubscriber extends Subscriber implements BackpressureDrainManager.BackpressureQueueCallback { // TODO get a different queue implementation private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); @@ -81,14 +117,18 @@ private static final class BufferSubscriber extends Subscriber implements private final BackpressureDrainManager manager; private final NotificationLite on = NotificationLite.instance(); private final Action0 onOverflow; + private final BackpressureOverflow.Strategy overflowStrategy; - public BufferSubscriber(final Subscriber child, Long capacity, Action0 onOverflow) { + public BufferSubscriber(final Subscriber child, Long capacity, Action0 onOverflow, + BackpressureOverflow.Strategy overflowStrategy) { this.child = child; this.baseCapacity = capacity; this.capacity = capacity != null ? new AtomicLong(capacity) : null; this.onOverflow = onOverflow; this.manager = new BackpressureDrainManager(this); + this.overflowStrategy = overflowStrategy; } + @Override public void onStart() { request(Long.MAX_VALUE); @@ -141,7 +181,7 @@ public Object poll() { } return value; } - + private boolean assertCapacity() { if (capacity == null) { return true; @@ -151,24 +191,30 @@ private boolean assertCapacity() { do { currCapacity = capacity.get(); if (currCapacity <= 0) { - if (saturated.compareAndSet(false, true)) { - unsubscribe(); - child.onError(new MissingBackpressureException( - "Overflowed buffer of " - + baseCapacity)); - if (onOverflow != null) { - try { - onOverflow.call(); - } catch (Throwable e) { - Exceptions.throwIfFatal(e); - manager.terminateAndDrain(e); - // this line not strictly necessary but nice for clarity - // and in case of future changes to code after this catch block - return false; - } + boolean hasCapacity = false; + try { + // ok if we're allowed to drop, and there is indeed an item to discard + hasCapacity = overflowStrategy.mayAttemptDrop() && poll() != null; + } catch (MissingBackpressureException e) { + if (saturated.compareAndSet(false, true)) { + unsubscribe(); + child.onError(e); } } - return false; + if (onOverflow != null) { + try { + onOverflow.call(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + manager.terminateAndDrain(e); + // this line not strictly necessary but nice for clarity + // and in case of future changes to code after this catch block + return false; + } + } + if (!hasCapacity) { + return false; + } } // ensure no other thread stole our slot, or retry } while (!capacity.compareAndSet(currCapacity, currCapacity - 1)); diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java index 48fa099735..59a971e1c1 100644 --- a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2014 Netflix, Inc. + * Copyright 2016 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,17 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static rx.BackpressureOverflow.*; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Subscriber; -import rx.Subscription; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; import rx.functions.Action1; @@ -101,24 +98,19 @@ public void testFixBackpressureBufferZeroCapacity() throws InterruptedException Observable.empty().onBackpressureBuffer(0); } + @Test(expected = NullPointerException.class) + public void testFixBackpressureBufferNullStrategy() throws InterruptedException { + Observable.empty().onBackpressureBuffer(10, new Action0() { + @Override + public void call() { } + }, null); + } + @Test public void testFixBackpressureBoundedBuffer() throws InterruptedException { final CountDownLatch l1 = new CountDownLatch(100); final CountDownLatch backpressureCallback = new CountDownLatch(1); - TestSubscriber ts = new TestSubscriber(new Observer() { - - @Override - public void onCompleted() { } - - @Override - public void onError(Throwable e) { } - - @Override - public void onNext(Long t) { - l1.countDown(); - } - - }); + final TestSubscriber ts = testSubscriber(l1); ts.requestMore(100); Subscription s = infinite.subscribeOn(Schedulers.computation()) @@ -128,11 +120,11 @@ public void call() { backpressureCallback.countDown(); } }).take(1000).subscribe(ts); - l1.await(); + assertTrue(l1.await(2, TimeUnit.SECONDS)); ts.requestMore(50); - assertTrue(backpressureCallback.await(500, TimeUnit.MILLISECONDS)); + assertTrue(backpressureCallback.await(2, TimeUnit.SECONDS)); assertTrue(ts.getOnErrorEvents().get(0) instanceof MissingBackpressureException); int size = ts.getOnNextEvents().size(); @@ -141,6 +133,100 @@ public void call() { assertTrue(s.isUnsubscribed()); } + @Test + public void testFixBackpressureBoundedBufferDroppingOldest() + throws InterruptedException { + List events = overflowBufferWithBehaviour(100, 10, ON_OVERFLOW_DROP_OLDEST); + + // The consumer takes 100 initial elements, then 10 are temporarily + // buffered and the oldest (100, 101, etc.) are dropped to make room for + // higher items. + int i = 0; + for (Long n : events) { + if (i < 100) { // backpressure is expected to kick in after the + // initial batch is consumed + assertEquals(Long.valueOf(i), n); + } else { + assertTrue(i < n); + } + i++; + } + } + + @Test + public void testFixBackpressueBoundedBufferDroppingLatest() + throws InterruptedException { + + List events = overflowBufferWithBehaviour(100, 10, ON_OVERFLOW_DROP_LATEST); + + // The consumer takes 100 initial elements, then 10 are temporarily + // buffered and the newest are dropped to make room for higher items. + int i = 0; + for (Long n : events) { + if (i < 110) { + assertEquals(Long.valueOf(i), n); + } else { + assertTrue(i < n); + } + i++; + } + } + + private List overflowBufferWithBehaviour(int initialRequest, int bufSize, + BackpressureOverflow.Strategy backpressureStrategy) + throws InterruptedException { + + final CountDownLatch l1 = new CountDownLatch(initialRequest * 2); + final CountDownLatch backpressureCallback = new CountDownLatch(1); + + final TestSubscriber ts = testSubscriber(l1); + + ts.requestMore(initialRequest); + Subscription s = infinite.subscribeOn(Schedulers.computation()) + .onBackpressureBuffer(bufSize, new Action0() { + @Override + public void call() { + backpressureCallback.countDown(); + } + }, backpressureStrategy + ).subscribe(ts); + + assertTrue(backpressureCallback.await(2, TimeUnit.SECONDS)); + + ts.requestMore(initialRequest); + + assertTrue(l1.await(2, TimeUnit.SECONDS)); + + // Stop receiving elements + s.unsubscribe(); + + // No failure despite overflows + assertTrue(ts.getOnErrorEvents().isEmpty()); + assertEquals(initialRequest * 2, ts.getOnNextEvents().size()); + + assertTrue(ts.isUnsubscribed()); + + return ts.getOnNextEvents(); + } + + static TestSubscriber testSubscriber(final CountDownLatch latch) { + return new TestSubscriber(new Observer() { + + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(T t) { + latch.countDown(); + } + }); + } + static final Observable infinite = Observable.create(new OnSubscribe() { @Override diff --git a/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java b/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java index 2ac3e6eadb..6b2d792e9c 100644 --- a/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java @@ -118,7 +118,7 @@ public void testThrowablePassesThru() { @Test public void testErrorPassesThru() { // Trigger failure on second element - TestObservable f = new TestObservable("one", "ERROR", "two", "three"); + TestObservable f = new TestObservable("one", "ON_OVERFLOW_ERROR", "two", "three"); Observable w = Observable.create(f); Observable resume = Observable.just("twoResume", "threeResume"); Observable observable = w.onExceptionResumeNext(resume); @@ -240,7 +240,7 @@ public void run() { throw new Exception("Forced Exception"); else if ("RUNTIMEEXCEPTION".equals(s)) throw new RuntimeException("Forced RuntimeException"); - else if ("ERROR".equals(s)) + else if ("ON_OVERFLOW_ERROR".equals(s)) throw new Error("Forced Error"); else if ("THROWABLE".equals(s)) throw new Throwable("Forced Throwable"); From c36456a2b3571d1b111e83572d8f1a5c28039cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Wed, 16 Mar 2016 22:42:08 +0100 Subject: [PATCH 020/322] 1.x: fix ExecutorScheduler and GenericScheduledExecutorService reorder bug --- .../GenericScheduledExecutorService.java | 64 +++++++++++++------ .../java/rx/schedulers/ExecutorScheduler.java | 9 +-- .../GenericScheduledExecutorServiceTest.java | 43 +++++++++++++ 3 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 src/test/java/rx/internal/schedulers/GenericScheduledExecutorServiceTest.java diff --git a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java index 82260207ae..87f7ec5f88 100644 --- a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java +++ b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java @@ -31,24 +31,29 @@ * the work asynchronously on the appropriate {@link Scheduler} implementation. This means for example that you would not use this approach * along with {@link TrampolineScheduler} or {@link ImmediateScheduler}. */ -public final class GenericScheduledExecutorService implements SchedulerLifecycle{ +public final class GenericScheduledExecutorService implements SchedulerLifecycle { private static final String THREAD_NAME_PREFIX = "RxScheduledExecutorPool-"; private static final RxThreadFactory THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX); - private static final ScheduledExecutorService NONE; + private static final ScheduledExecutorService[] NONE = new ScheduledExecutorService[0]; + + private static final ScheduledExecutorService SHUTDOWN; static { - NONE = Executors.newScheduledThreadPool(0); - NONE.shutdownNow(); + SHUTDOWN = Executors.newScheduledThreadPool(0); + SHUTDOWN.shutdown(); } /* Schedulers needs acces to this in order to work with the lifecycle. */ public final static GenericScheduledExecutorService INSTANCE = new GenericScheduledExecutorService(); - private final AtomicReference executor; + private final AtomicReference executor; + /** We don't use atomics with this because thread-assignment is random anyway. */ + private static int roundRobin; + private GenericScheduledExecutorService() { - executor = new AtomicReference(NONE); + executor = new AtomicReference(NONE); start(); } @@ -63,39 +68,60 @@ public void start() { count = 8; } - ScheduledExecutorService exec = Executors.newScheduledThreadPool(count, THREAD_FACTORY); - if (executor.compareAndSet(NONE, exec)) { - if (!NewThreadWorker.tryEnableCancelPolicy(exec)) { - if (exec instanceof ScheduledThreadPoolExecutor) { - NewThreadWorker.registerExecutor((ScheduledThreadPoolExecutor)exec); + // A multi-threaded executor can reorder tasks, having a set of them + // and handing one of those out on getInstance() ensures a proper order + + ScheduledExecutorService[] execs = new ScheduledExecutorService[count]; + for (int i = 0; i < count; i++) { + execs[i] = Executors.newScheduledThreadPool(1, THREAD_FACTORY); + } + if (executor.compareAndSet(NONE, execs)) { + for (ScheduledExecutorService exec : execs) { + if (!NewThreadWorker.tryEnableCancelPolicy(exec)) { + if (exec instanceof ScheduledThreadPoolExecutor) { + NewThreadWorker.registerExecutor((ScheduledThreadPoolExecutor)exec); + } } } } else { - exec.shutdownNow(); + for (ScheduledExecutorService exec : execs) { + exec.shutdownNow(); + } } } @Override public void shutdown() { for (;;) { - ScheduledExecutorService exec = executor.get(); - if (exec == NONE) { + ScheduledExecutorService[] execs = executor.get(); + if (execs == NONE) { return; } - if (executor.compareAndSet(exec, NONE)) { - NewThreadWorker.deregisterExecutor(exec); - exec.shutdownNow(); + if (executor.compareAndSet(execs, NONE)) { + for (ScheduledExecutorService exec : execs) { + NewThreadWorker.deregisterExecutor(exec); + exec.shutdownNow(); + } return; } } } /** - * See class Javadoc for information on what this is for and how to use. + * Returns one of the single-threaded ScheduledExecutorService helper executors. * * @return {@link ScheduledExecutorService} for generic use. */ public static ScheduledExecutorService getInstance() { - return INSTANCE.executor.get(); + ScheduledExecutorService[] execs = INSTANCE.executor.get(); + if (execs == NONE) { + return SHUTDOWN; + } + int r = roundRobin + 1; + if (r >= execs.length) { + r = 0; + } + roundRobin = r; + return execs[r]; } } \ No newline at end of file diff --git a/src/main/java/rx/schedulers/ExecutorScheduler.java b/src/main/java/rx/schedulers/ExecutorScheduler.java index d447400184..8e5c9bf22e 100644 --- a/src/main/java/rx/schedulers/ExecutorScheduler.java +++ b/src/main/java/rx/schedulers/ExecutorScheduler.java @@ -54,11 +54,14 @@ static final class ExecutorSchedulerWorker extends Scheduler.Worker implements R final ConcurrentLinkedQueue queue; final AtomicInteger wip; + final ScheduledExecutorService service; + public ExecutorSchedulerWorker(Executor executor) { this.executor = executor; this.queue = new ConcurrentLinkedQueue(); this.wip = new AtomicInteger(); this.tasks = new CompositeSubscription(); + this.service = GenericScheduledExecutorService.getInstance(); } @Override @@ -108,12 +111,6 @@ public Subscription schedule(final Action0 action, long delayTime, TimeUnit unit if (isUnsubscribed()) { return Subscriptions.unsubscribed(); } - ScheduledExecutorService service; - if (executor instanceof ScheduledExecutorService) { - service = (ScheduledExecutorService)executor; - } else { - service = GenericScheduledExecutorService.getInstance(); - } final MultipleAssignmentSubscription first = new MultipleAssignmentSubscription(); final MultipleAssignmentSubscription mas = new MultipleAssignmentSubscription(); diff --git a/src/test/java/rx/internal/schedulers/GenericScheduledExecutorServiceTest.java b/src/test/java/rx/internal/schedulers/GenericScheduledExecutorServiceTest.java new file mode 100644 index 0000000000..0b90bce072 --- /dev/null +++ b/src/test/java/rx/internal/schedulers/GenericScheduledExecutorServiceTest.java @@ -0,0 +1,43 @@ +package rx.internal.schedulers; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; + +public class GenericScheduledExecutorServiceTest { + @Test + public void verifyInstanceIsSingleThreaded() throws Exception { + ScheduledExecutorService exec = GenericScheduledExecutorService.getInstance(); + + final AtomicInteger state = new AtomicInteger(); + + final AtomicInteger found1 = new AtomicInteger(); + final AtomicInteger found2 = new AtomicInteger(); + + Future f1 = exec.schedule(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(250); + } catch (InterruptedException e) { + e.printStackTrace(); + } + found1.set(state.getAndSet(1)); + } + }, 250, TimeUnit.MILLISECONDS); + Future f2 = exec.schedule(new Runnable() { + @Override + public void run() { + found2.set(state.getAndSet(2)); + } + }, 250, TimeUnit.MILLISECONDS); + + f1.get(); + f2.get(); + + Assert.assertEquals(2, state.get()); + Assert.assertEquals(0, found1.get()); + Assert.assertEquals(1, found2.get()); + } +} From be8d144e65cf8dc66661ec4451e7b717b52a70b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Thu, 17 Mar 2016 23:18:52 +0100 Subject: [PATCH 021/322] 1.x: fix counted buffer and window backpressure --- src/main/java/rx/Observable.java | 16 +- .../internal/operators/BackpressureUtils.java | 210 ++++++- .../operators/OperatorBufferWithSize.java | 396 ++++++++----- .../operators/OperatorWindowWithSize.java | 560 +++++++++++++----- .../rx/internal/operators/UnicastSubject.java | 57 +- .../java/rx/observers/TestSubscriber.java | 19 +- .../operators/OperatorBufferTest.java | 85 ++- .../operators/OperatorWindowWithSizeTest.java | 67 +++ 8 files changed, 1082 insertions(+), 328 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 7a2d91a2af..ed08939889 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -9744,8 +9744,8 @@ public final Observable> window(Func0 *
*
Backpressure Support:
- *
The operator honors backpressure on its outer subscriber, ignores backpressure in its inner Observables - * but each of them will emit at most {@code count} elements.
+ *
The operator honors backpressure of its inner and outer subscribers, however, the inner Observable uses an + * unbounded buffer that may hold at most {@code count} elements.
*
Scheduler:
*
This version of {@code window} does not operate by default on a particular {@link Scheduler}.
*
@@ -9754,6 +9754,7 @@ public final Observable> window(Func0ReactiveX operators documentation: Window */ public final Observable> window(int count) { @@ -9769,8 +9770,8 @@ public final Observable> window(int count) { * *
*
Backpressure Support:
- *
The operator honors backpressure on its outer subscriber, ignores backpressure in its inner Observables - * but each of them will emit at most {@code count} elements.
+ *
The operator honors backpressure of its inner and outer subscribers, however, the inner Observable uses an + * unbounded buffer that may hold at most {@code count} elements.
*
Scheduler:
*
This version of {@code window} does not operate by default on a particular {@link Scheduler}.
*
@@ -9782,9 +9783,16 @@ public final Observable> window(int count) { * {@code count} are equal this is the same operation as {@link #window(int)}. * @return an Observable that emits windows every {@code skip} items containing at most {@code count} items * from the source Observable + * @throws IllegalArgumentException if either count or skip is non-positive * @see ReactiveX operators documentation: Window */ public final Observable> window(int count, int skip) { + if (count <= 0) { + throw new IllegalArgumentException("count > 0 required but it was " + count); + } + if (skip <= 0) { + throw new IllegalArgumentException("skip > 0 required but it was " + skip); + } return lift(new OperatorWindowWithSize(count, skip)); } diff --git a/src/main/java/rx/internal/operators/BackpressureUtils.java b/src/main/java/rx/internal/operators/BackpressureUtils.java index 0d4adef0a8..cfbe282901 100644 --- a/src/main/java/rx/internal/operators/BackpressureUtils.java +++ b/src/main/java/rx/internal/operators/BackpressureUtils.java @@ -15,8 +15,10 @@ */ package rx.internal.operators; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.Queue; +import java.util.concurrent.atomic.*; + +import rx.Subscriber; /** * Utility functions for use with backpressure. @@ -32,6 +34,8 @@ private BackpressureUtils() { * addition once the addition is successful (uses CAS semantics). If * overflows then sets {@code requested} field to {@code Long.MAX_VALUE}. * + * @param the type of the target object on which the field updater operates + * * @param requested * atomic field updater for a request count * @param object @@ -103,6 +107,208 @@ public static long addCap(long a, long b) { return u; } + /** + * Masks the most significant bit, i.e., 0x8000_0000_0000_0000L. + */ + static final long COMPLETED_MASK = Long.MIN_VALUE; + /** + * Masks the request amount bits, i.e., 0x7FFF_FFFF_FFFF_FFFF. + */ + static final long REQUESTED_MASK = Long.MAX_VALUE; + + /** + * Signals the completion of the main sequence and switches to post-completion replay mode. + * + *

+ * Don't modify the queue after calling this method! + * + *

+ * Post-completion backpressure handles the case when a source produces values based on + * requests when it is active but more values are available even after its completion. + * In this case, the onCompleted() can't just emit the contents of the queue but has to + * coordinate with the requested amounts. This requires two distinct modes: active and + * completed. In active mode, requests flow through and the queue is not accessed but + * in completed mode, requests no-longer reach the upstream but help in draining the queue. + *

+ * The algorithm utilizes the most significant bit (bit 63) of a long value (AtomicLong) since + * request amount only goes up to Long.MAX_VALUE (bits 0-62) and negative values aren't + * allowed. + * + * @param the value type to emit + * @param requested the holder of current requested amount + * @param queue the queue holding values to be emitted after completion + * @param actual the subscriber to receive the values + */ + public static void postCompleteDone(AtomicLong requested, Queue queue, Subscriber actual) { + for (;;) { + long r = requested.get(); + + // switch to completed mode only once + if ((r & COMPLETED_MASK) != 0L) { + return; + } + + // + long u = r | COMPLETED_MASK; + + if (requested.compareAndSet(r, u)) { + // if we successfully switched to post-complete mode and there + // are requests available start draining the queue + if (r != 0L) { + // if the switch happened when there was outstanding requests, start draining + postCompleteDrain(requested, queue, actual); + } + return; + } + } + } + + /** + * Accumulates requests (validated) and handles the completed mode draining of the queue based on the requests. + * + *

+ * Post-completion backpressure handles the case when a source produces values based on + * requests when it is active but more values are available even after its completion. + * In this case, the onCompleted() can't just emit the contents of the queue but has to + * coordinate with the requested amounts. This requires two distinct modes: active and + * completed. In active mode, requests flow through and the queue is not accessed but + * in completed mode, requests no-longer reach the upstream but help in draining the queue. + * + * @param the value type to emit + * @param requested the holder of current requested amount + * @param n the value requested; + * @param queue the queue holding values to be emitted after completion + * @param actual the subscriber to receive the values + * @return true if in the active mode and the request amount of n can be relayed to upstream, false if + * in the post-completed mode and the queue is draining. + */ + public static boolean postCompleteRequest(AtomicLong requested, long n, Queue queue, Subscriber actual) { + if (n < 0L) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); + } + if (n == 0) { + return (requested.get() & COMPLETED_MASK) == 0; + } + + for (;;) { + long r = requested.get(); + + // mask of the completed flag + long c = r & COMPLETED_MASK; + // mask of the requested amount + long u = r & REQUESTED_MASK; + + // add the current requested amount and the new requested amount + // cap at Long.MAX_VALUE; + long v = addCap(u, n); + + // restore the completed flag + v |= c; + + if (requested.compareAndSet(r, v)) { + // if there was no outstanding request before and in + // the post-completed state, start draining + if (r == COMPLETED_MASK) { + postCompleteDrain(requested, queue, actual); + return false; + } + // returns true for active mode and false if the completed flag was set + return c == 0L; + } + } + } + + /** + * Drains the queue based on the outstanding requests in post-completed mode (only!). + * + * @param the value type to emit + * @param requested the holder of current requested amount + * @param queue the queue holding values to be emitted after completion + * @param actual the subscriber to receive the values + */ + static void postCompleteDrain(AtomicLong requested, Queue queue, Subscriber subscriber) { + + long r = requested.get(); + /* + * Since we are supposed to be in the post-complete state, + * requested will have its top bit set. + * To allow direct comparison, we start with an emission value which has also + * this flag set, then increment it as usual. + * Since COMPLETED_MASK is essentially Long.MIN_VALUE, + * there won't be any overflow or sign flip. + */ + long e = COMPLETED_MASK; + + for (;;) { + + /* + * This is an improved queue-drain algorithm with a specialization + * in which we know the queue won't change anymore (i.e., done is always true + * when looking at the classical algorithm and there is no error). + * + * Note that we don't check for cancellation or emptyness upfront for two reasons: + * 1) if e != r, the loop will do this and we quit appropriately + * 2) if e == r, then either there was no outstanding requests or we emitted the requested amount + * and the execution simply falls to the e == r check below which checks for emptyness anyway. + */ + + while (e != r) { + if (subscriber.isUnsubscribed()) { + return; + } + + T v = queue.poll(); + + if (v == null) { + subscriber.onCompleted(); + return; + } + + subscriber.onNext(v); + + e++; + } + + /* + * If the emission count reaches the requested amount the same time the queue becomes empty + * this will make sure the subscriber is completed immediately instead of on the next request. + * This is also true if there are no outstanding requests (this the while loop doesn't run) + * and the queue is empty from the start. + */ + if (e == r) { + if (subscriber.isUnsubscribed()) { + return; + } + if (queue.isEmpty()) { + subscriber.onCompleted(); + return; + } + } + + /* + * Fast flow: see if more requests have arrived in the meantime. + * This avoids an atomic add (~40 cycles) and resumes the emission immediately. + */ + r = requested.get(); + + if (r == e) { + /* + * Atomically decrement the requested amount by the emission amount. + * We can't use the full emission value because of the completed flag, + * however, due to two's complement representation, the flag on requested + * is preserved. + */ + r = requested.addAndGet(-(e & REQUESTED_MASK)); + // The requested amount actually reached zero, quit + if (r == COMPLETED_MASK) { + return; + } + // reset the emission count + e = COMPLETED_MASK; + } + } + } + /** * Atomically subtracts a value from the requested amount unless it's at Long.MAX_VALUE. * @param requested the requested amount holder diff --git a/src/main/java/rx/internal/operators/OperatorBufferWithSize.java b/src/main/java/rx/internal/operators/OperatorBufferWithSize.java index e08fd440c2..6475547563 100644 --- a/src/main/java/rx/internal/operators/OperatorBufferWithSize.java +++ b/src/main/java/rx/internal/operators/OperatorBufferWithSize.java @@ -15,16 +15,13 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; +import java.util.*; +import java.util.concurrent.atomic.*; +import rx.*; import rx.Observable; import rx.Observable.Operator; -import rx.Producer; -import rx.Subscriber; -import rx.exceptions.Exceptions; +import rx.exceptions.MissingBackpressureException; /** * This operation takes @@ -66,167 +63,274 @@ public OperatorBufferWithSize(int count, int skip) { @Override public Subscriber call(final Subscriber> child) { - if (count == skip) { - return new Subscriber(child) { - List buffer; - - @Override - public void setProducer(final Producer producer) { - child.setProducer(new Producer() { - - private volatile boolean infinite = false; - - @Override - public void request(long n) { - if (infinite) { - return; - } - if (n >= Long.MAX_VALUE / count) { - // n == Long.MAX_VALUE or n * count >= Long.MAX_VALUE - infinite = true; - producer.request(Long.MAX_VALUE); - } else { - producer.request(n * count); - } - } - }); - } + if (skip == count) { + BufferExact parent = new BufferExact(child, count); + + child.add(parent); + child.setProducer(parent.createProducer()); + + return parent; + } + if (skip > count) { + BufferSkip parent = new BufferSkip(child, count, skip); + + child.add(parent); + child.setProducer(parent.createProducer()); + + return parent; + } + BufferOverlap parent = new BufferOverlap(child, count, skip); + + child.add(parent); + child.setProducer(parent.createProducer()); + + return parent; + } + + static final class BufferExact extends Subscriber { + final Subscriber> actual; + final int count; + List buffer; + + public BufferExact(Subscriber> actual, int count) { + this.actual = actual; + this.count = count; + this.request(0L); + } + + @Override + public void onNext(T t) { + List b = buffer; + if (b == null) { + b = new ArrayList(count); + buffer = b; + } + + b.add(t); + + if (b.size() == count) { + buffer = null; + actual.onNext(b); + } + } + + @Override + public void onError(Throwable e) { + buffer = null; + actual.onError(e); + } + + @Override + public void onCompleted() { + List b = buffer; + if (b != null) { + actual.onNext(b); + } + actual.onCompleted(); + } + + Producer createProducer() { + return new Producer() { @Override - public void onNext(T t) { - if (buffer == null) { - buffer = new ArrayList(count); + public void request(long n) { + if (n < 0L) { + throw new IllegalArgumentException("n >= required but it was " + n); } - buffer.add(t); - if (buffer.size() == count) { - List oldBuffer = buffer; - buffer = null; - child.onNext(oldBuffer); + if (n != 0L) { + long u = BackpressureUtils.multiplyCap(n, count); + BufferExact.this.request(u); } } + }; + } + } + + static final class BufferSkip extends Subscriber { + final Subscriber> actual; + final int count; + final int skip; + + long index; + + List buffer; - @Override - public void onError(Throwable e) { - buffer = null; - child.onError(e); - } - - @Override - public void onCompleted() { - List oldBuffer = buffer; + public BufferSkip(Subscriber> actual, int count, int skip) { + this.actual = actual; + this.count = count; + this.skip = skip; + this.request(0L); + } + + @Override + public void onNext(T t) { + long i = index; + List b = buffer; + if (i == 0) { + b = new ArrayList(count); + buffer = b; + } + i++; + if (i == skip) { + index = 0; + } else { + index = i; + } + + if (b != null) { + b.add(t); + + if (b.size() == count) { buffer = null; - if (oldBuffer != null) { - try { - child.onNext(oldBuffer); - } catch (Throwable t) { - Exceptions.throwOrReport(t, this); - return; - } - } - child.onCompleted(); + actual.onNext(b); } - }; + } } - return new Subscriber(child) { - final List> chunks = new LinkedList>(); - int index; - - @Override - public void setProducer(final Producer producer) { - child.setProducer(new Producer() { - - private volatile boolean firstRequest = true; - private volatile boolean infinite = false; - - private void requestInfinite() { - infinite = true; - producer.request(Long.MAX_VALUE); - } - - @Override - public void request(long n) { - if (n == 0) { - return; - } - if (n < 0) { - throw new IllegalArgumentException("request a negative number: " + n); - } - if (infinite) { - return; - } - if (n == Long.MAX_VALUE) { - requestInfinite(); - } else { - if (firstRequest) { - firstRequest = false; - if (n - 1 >= (Long.MAX_VALUE - count) / skip) { - // count + skip * (n - 1) >= Long.MAX_VALUE - requestInfinite(); - return; - } - // count = 5, skip = 2, n = 3 - // * * * * * - // * * * * * - // * * * * * - // request = 5 + 2 * ( 3 - 1) - producer.request(count + skip * (n - 1)); - } else { - if (n >= Long.MAX_VALUE / skip) { - // skip * n >= Long.MAX_VALUE - requestInfinite(); - return; - } - // count = 5, skip = 2, n = 3 - // (* * *) * * - // ( *) * * * * - // * * * * * - // request = skip * n - // "()" means the items already emitted before this request - producer.request(skip * n); - } - } - } - }); + + @Override + public void onError(Throwable e) { + buffer = null; + actual.onError(e); + } + + @Override + public void onCompleted() { + List b = buffer; + if (b != null) { + buffer = null; + actual.onNext(b); } + actual.onCompleted(); + } + + Producer createProducer() { + return new BufferSkipProducer(); + } + + final class BufferSkipProducer + extends AtomicBoolean + implements Producer { + /** */ + private static final long serialVersionUID = 3428177408082367154L; @Override - public void onNext(T t) { - if (index++ % skip == 0) { - chunks.add(new ArrayList(count)); + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); } - - Iterator> it = chunks.iterator(); - while (it.hasNext()) { - List chunk = it.next(); - chunk.add(t); - if (chunk.size() == count) { - it.remove(); - child.onNext(chunk); + if (n != 0) { + BufferSkip parent = BufferSkip.this; + if (!get() && compareAndSet(false, true)) { + long u = BackpressureUtils.multiplyCap(n, parent.count); + long v = BackpressureUtils.multiplyCap(parent.skip - parent.count, n - 1); + long w = BackpressureUtils.addCap(u, v); + parent.request(w); + } else { + long u = BackpressureUtils.multiplyCap(n, parent.skip); + parent.request(u); } } } + } + } + + static final class BufferOverlap extends Subscriber { + final Subscriber> actual; + final int count; + final int skip; + + long index; + + final ArrayDeque> queue; + + final AtomicLong requested; + + long produced; - @Override - public void onError(Throwable e) { - chunks.clear(); - child.onError(e); + public BufferOverlap(Subscriber> actual, int count, int skip) { + this.actual = actual; + this.count = count; + this.skip = skip; + this.queue = new ArrayDeque>(); + this.requested = new AtomicLong(); + this.request(0L); + } + + @Override + public void onNext(T t) { + long i = index; + if (i == 0) { + List b = new ArrayList(count); + queue.offer(b); + } + i++; + if (i == skip) { + index = 0; + } else { + index = i; + } + + for (List list : queue) { + list.add(t); + } + + List b = queue.peek(); + if (b != null && b.size() == count) { + queue.poll(); + produced++; + actual.onNext(b); + } + } + + @Override + public void onError(Throwable e) { + queue.clear(); + + actual.onError(e); + } + + @Override + public void onCompleted() { + long p = produced; + + if (p != 0L) { + if (p > requested.get()) { + actual.onError(new MissingBackpressureException("More produced than requested? " + p)); + return; + } + requested.addAndGet(-p); } + + BackpressureUtils.postCompleteDone(requested, queue, actual); + } + + Producer createProducer() { + return new BufferOverlapProducer(); + } + + final class BufferOverlapProducer extends AtomicBoolean implements Producer { + + /** */ + private static final long serialVersionUID = -4015894850868853147L; + @Override - public void onCompleted() { - try { - for (List chunk : chunks) { - try { - child.onNext(chunk); - } catch (Throwable t) { - Exceptions.throwOrReport(t, this); - return; + public void request(long n) { + BufferOverlap parent = BufferOverlap.this; + if (BackpressureUtils.postCompleteRequest(parent.requested, n, parent.queue, parent.actual)) { + if (n != 0L) { + if (!get() && compareAndSet(false, true)) { + long u = BackpressureUtils.multiplyCap(parent.skip, n - 1); + long v = BackpressureUtils.addCap(u, parent.count); + + parent.request(v); + } else { + long u = BackpressureUtils.multiplyCap(parent.skip, n); + parent.request(u); } } - child.onCompleted(); - } finally { - chunks.clear(); } } - }; + + } } } diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java index 48d80d9cfb..3538991526 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java @@ -16,12 +16,14 @@ package rx.internal.operators; import java.util.*; +import java.util.concurrent.atomic.*; import rx.*; -import rx.Observable.Operator; import rx.Observable; -import rx.Observer; +import rx.Observable.Operator; import rx.functions.Action0; +import rx.internal.util.atomic.SpscLinkedArrayQueue; +import rx.subjects.Subject; import rx.subscriptions.Subscriptions; /** @@ -48,215 +50,455 @@ public OperatorWindowWithSize(int size, int skip) { @Override public Subscriber call(Subscriber> child) { if (skip == size) { - ExactSubscriber e = new ExactSubscriber(child); - e.init(); - return e; + WindowExact parent = new WindowExact(child, size); + + child.add(parent.cancel); + child.setProducer(parent.createProducer()); + + return parent; + } else + if (skip > size) { + WindowSkip parent = new WindowSkip(child, size, skip); + + child.add(parent.cancel); + child.setProducer(parent.createProducer()); + + return parent; } - InexactSubscriber ie = new InexactSubscriber(child); - ie.init(); - return ie; + + WindowOverlap parent = new WindowOverlap(child, size, skip); + + child.add(parent.cancel); + child.setProducer(parent.createProducer()); + + return parent; + } - /** Subscriber with exact, non-overlapping window bounds. */ - final class ExactSubscriber extends Subscriber { - final Subscriber> child; - int count; - UnicastSubject window; - volatile boolean noWindow = true; - public ExactSubscriber(Subscriber> child) { - /** - * See https://github.com/ReactiveX/RxJava/issues/1546 - * We cannot compose through a Subscription because unsubscribing - * applies to the outer, not the inner. - */ - this.child = child; - /* - * Add unsubscribe hook to child to get unsubscribe on outer (unsubscribing on next window, not on the inner window itself) - */ + + static final class WindowExact extends Subscriber implements Action0 { + final Subscriber> actual; + + final int size; + + final AtomicInteger wip; + + final Subscription cancel; + + int index; + + Subject window; + + public WindowExact(Subscriber> actual, int size) { + this.actual = actual; + this.size = size; + this.wip = new AtomicInteger(1); + this.cancel = Subscriptions.create(this); + this.add(cancel); + this.request(0); } - void init() { - child.add(Subscriptions.create(new Action0() { - - @Override - public void call() { - // if no window we unsubscribe up otherwise wait until window ends - if (noWindow) { - unsubscribe(); - } - } + + @Override + public void onNext(T t) { + int i = index; + + Subject w = window; + if (i == 0) { + wip.getAndIncrement(); - })); - child.setProducer(new Producer() { + w = UnicastSubject.create(size, this); + window = w; + + actual.onNext(w); + } + i++; + + w.onNext(t); + + if (i == size) { + index = 0; + window = null; + w.onCompleted(); + } else { + index = i; + } + } + + @Override + public void onError(Throwable e) { + Subject w = window; + + if (w != null) { + window = null; + w.onError(e); + } + actual.onError(e); + } + + @Override + public void onCompleted() { + Subject w = window; + + if (w != null) { + window = null; + w.onCompleted(); + } + actual.onCompleted(); + } + + Producer createProducer() { + return new Producer() { @Override public void request(long n) { - if (n > 0) { - long u = n * size; - if (((u >>> 31) != 0) && (u / n != size)) { - u = Long.MAX_VALUE; - } - requestMore(u); + if (n < 0L) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); + } + if (n != 0L) { + long u = BackpressureUtils.multiplyCap(size, n); + WindowExact.this.request(u); } } - }); + }; } - void requestMore(long n) { - request(n); + @Override + public void call() { + if (wip.decrementAndGet() == 0) { + unsubscribe(); + } } - + } + + static final class WindowSkip extends Subscriber implements Action0 { + final Subscriber> actual; + + final int size; + + final int skip; + + final AtomicInteger wip; + + final Subscription cancel; + + int index; + + Subject window; + + public WindowSkip(Subscriber> actual, int size, int skip) { + this.actual = actual; + this.size = size; + this.skip = skip; + this.wip = new AtomicInteger(1); + this.cancel = Subscriptions.create(this); + this.add(cancel); + this.request(0); + } + @Override public void onNext(T t) { - if (window == null) { - noWindow = false; - window = UnicastSubject.create(); - child.onNext(window); + int i = index; + + Subject w = window; + if (i == 0) { + wip.getAndIncrement(); + + w = UnicastSubject.create(size, this); + window = w; + + actual.onNext(w); } - window.onNext(t); - if (++count % size == 0) { - window.onCompleted(); + i++; + + if (w != null) { + w.onNext(t); + } + + if (i == size) { + index = i; window = null; - noWindow = true; - if (child.isUnsubscribed()) { - unsubscribe(); - } + w.onCompleted(); + } else + if (i == skip) { + index = 0; + } else { + index = i; } + } - + @Override public void onError(Throwable e) { - if (window != null) { - window.onError(e); + Subject w = window; + + if (w != null) { + window = null; + w.onError(e); } - child.onError(e); + actual.onError(e); } - + @Override public void onCompleted() { - if (window != null) { - window.onCompleted(); + Subject w = window; + + if (w != null) { + window = null; + w.onCompleted(); } - child.onCompleted(); + actual.onCompleted(); } - } - - /** Subscriber with inexact, possibly overlapping or skipping windows. */ - final class InexactSubscriber extends Subscriber { - final Subscriber> child; - int count; - final List> chunks = new LinkedList>(); - volatile boolean noWindow = true; - - public InexactSubscriber(Subscriber> child) { - /** - * See https://github.com/ReactiveX/RxJava/issues/1546 - * We cannot compose through a Subscription because unsubscribing - * applies to the outer, not the inner. - */ - this.child = child; + + Producer createProducer() { + return new WindowSkipProducer(); } + + @Override + public void call() { + if (wip.decrementAndGet() == 0) { + unsubscribe(); + } + } + + final class WindowSkipProducer extends AtomicBoolean implements Producer { + /** */ + private static final long serialVersionUID = 4625807964358024108L; - void init() { - /* - * Add unsubscribe hook to child to get unsubscribe on outer (unsubscribing on next window, not on the inner window itself) - */ - child.add(Subscriptions.create(new Action0() { - - @Override - public void call() { - // if no window we unsubscribe up otherwise wait until window ends - if (noWindow) { - unsubscribe(); - } + @Override + public void request(long n) { + if (n < 0L) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); } - - })); - - child.setProducer(new Producer() { - @Override - public void request(long n) { - if (n > 0) { - long u = n * size; - if (((u >>> 31) != 0) && (u / n != size)) { - u = Long.MAX_VALUE; - } - requestMore(u); + if (n != 0L) { + WindowSkip parent = WindowSkip.this; + if (!get() && compareAndSet(false, true)) { + long u = BackpressureUtils.multiplyCap(n, parent.size); + long v = BackpressureUtils.multiplyCap(parent.skip - parent.size, n - 1); + long w = BackpressureUtils.addCap(u, v); + parent.request(w); + } else { + long u = BackpressureUtils.multiplyCap(n, parent.skip); + parent.request(u); } } - }); + } } + } + + static final class WindowOverlap extends Subscriber implements Action0 { + final Subscriber> actual; - void requestMore(long n) { - request(n); - } + final int size; + + final int skip; + + final AtomicInteger wip; + + final Subscription cancel; + + final ArrayDeque> windows; + final AtomicLong requested; + + final AtomicInteger drainWip; + + final Queue> queue; + + Throwable error; + + volatile boolean done; + + int index; + + int produced; + + public WindowOverlap(Subscriber> actual, int size, int skip) { + this.actual = actual; + this.size = size; + this.skip = skip; + this.wip = new AtomicInteger(1); + this.windows = new ArrayDeque>(); + this.drainWip = new AtomicInteger(); + this.requested = new AtomicLong(); + this.cancel = Subscriptions.create(this); + this.add(cancel); + this.request(0); + int maxWindows = (size + (skip - 1)) / skip; + this.queue = new SpscLinkedArrayQueue>(maxWindows); + } + @Override public void onNext(T t) { - if (count++ % skip == 0) { - if (!child.isUnsubscribed()) { - if (chunks.isEmpty()) { - noWindow = false; - } - CountedSubject cs = createCountedSubject(); - chunks.add(cs); - child.onNext(cs.producer); - } + int i = index; + + ArrayDeque> q = windows; + + if (i == 0 && !actual.isUnsubscribed()) { + wip.getAndIncrement(); + + Subject w = UnicastSubject.create(16, this); + q.offer(w); + + queue.offer(w); + drain(); } - Iterator> it = chunks.iterator(); - while (it.hasNext()) { - CountedSubject cs = it.next(); - cs.consumer.onNext(t); - if (++cs.count == size) { - it.remove(); - cs.consumer.onCompleted(); - } + for (Subject w : windows) { + w.onNext(t); } - if (chunks.isEmpty()) { - noWindow = true; - if (child.isUnsubscribed()) { - unsubscribe(); + + int p = produced + 1; + + if (p == size) { + produced = p - skip; + + Subject w = q.poll(); + if (w != null) { + w.onCompleted(); } + } else { + produced = p; + } + + i++; + if (i == skip) { + index = 0; + } else { + index = i; } } - + @Override public void onError(Throwable e) { - List> list = new ArrayList>(chunks); - chunks.clear(); - noWindow = true; - for (CountedSubject cs : list) { - cs.consumer.onError(e); + for (Subject w : windows) { + w.onError(e); } - child.onError(e); + windows.clear(); + + error = e; + done = true; + drain(); } - + @Override public void onCompleted() { - List> list = new ArrayList>(chunks); - chunks.clear(); - noWindow = true; - for (CountedSubject cs : list) { - cs.consumer.onCompleted(); + for (Subject w : windows) { + w.onCompleted(); + } + windows.clear(); + + done = true; + drain(); + } + + Producer createProducer() { + return new WindowOverlapProducer(); + } + + @Override + public void call() { + if (wip.decrementAndGet() == 0) { + unsubscribe(); } - child.onCompleted(); } + + void drain() { + AtomicInteger dw = drainWip; + if (dw.getAndIncrement() != 0) { + return; + } - CountedSubject createCountedSubject() { - final UnicastSubject bus = UnicastSubject.create(); - return new CountedSubject(bus, bus); + final Subscriber> a = actual; + final Queue> q = queue; + + int missed = 1; + + for (;;) { + + long r = requested.get(); + long e = 0L; + + while (e != r) { + boolean d = done; + Subject v = q.poll(); + boolean empty = v == null; + + if (checkTerminated(d, empty, a, q)) { + return; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + } + + if (e == r) { + if (checkTerminated(done, q.isEmpty(), a, q)) { + return; + } + } + + if (e != 0 && r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + + missed = dw.addAndGet(-missed); + if (missed == 0) { + break; + } + } } - } - /** - * Record to store the subject and the emission count. - * @param the subject's in-out type - */ - static final class CountedSubject { - final Observer consumer; - final Observable producer; - int count; + + boolean checkTerminated(boolean d, boolean empty, Subscriber> a, Queue> q) { + if (a.isUnsubscribed()) { + q.clear(); + return true; + } + if (d) { + Throwable e = error; + if (e != null) { + q.clear(); + a.onError(e); + return true; + } else + if (empty) { + a.onCompleted(); + return true; + } + } + return false; + } + + final class WindowOverlapProducer extends AtomicBoolean implements Producer { + /** */ + private static final long serialVersionUID = 4625807964358024108L; - public CountedSubject(Observer consumer, Observable producer) { - this.consumer = consumer; - this.producer = producer; + @Override + public void request(long n) { + if (n < 0L) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); + } + if (n != 0L) { + + WindowOverlap parent = WindowOverlap.this; + + if (!get() && compareAndSet(false, true)) { + long u = BackpressureUtils.multiplyCap(parent.skip, n - 1); + long v = BackpressureUtils.addCap(u, parent.size); + + parent.request(v); + } else { + long u = BackpressureUtils.multiplyCap(parent.skip, n); + WindowOverlap.this.request(u); + } + + BackpressureUtils.getAndAddRequest(parent.requested, n); + parent.drain(); + } + } } } + } diff --git a/src/main/java/rx/internal/operators/UnicastSubject.java b/src/main/java/rx/internal/operators/UnicastSubject.java index 5fb21b65f6..569745358e 100644 --- a/src/main/java/rx/internal/operators/UnicastSubject.java +++ b/src/main/java/rx/internal/operators/UnicastSubject.java @@ -32,12 +32,15 @@ * amount. In this case, the buffered values are no longer retained. If the Subscriber * requests a limited amount, queueing is involved and only those values are retained which * weren't requested by the Subscriber at that time. + * + * @param the input and output value type */ public final class UnicastSubject extends Subject { /** * Constructs an empty UnicastSubject instance with the default capacity hint of 16 elements. * + * @param the input and output value type * @return the created UnicastSubject instance */ public static UnicastSubject create() { @@ -48,14 +51,34 @@ public static UnicastSubject create() { *

The capacity hint determines the internal queue's island size: the larger * it is the less frequent allocation will happen if there is no subscriber * or the subscriber hasn't caught up. + * @param the input and output value type * @param capacityHint the capacity hint for the internal queue * @return the created BufferUntilSubscriber instance */ public static UnicastSubject create(int capacityHint) { - State state = new State(capacityHint); + State state = new State(capacityHint, null); return new UnicastSubject(state); } - + + /** + * Constructs an empty UnicastSubject instance with a capacity hint and + * an Action0 instance to call if the subject reaches its terminal state + * or the single Subscriber unsubscribes mid-sequence. + *

The capacity hint determines the internal queue's island size: the larger + * it is the less frequent allocation will happen if there is no subscriber + * or the subscriber hasn't caught up. + * @param the input and output value type + * @param capacityHint the capacity hint for the internal queue + * @param onTerminated the optional callback to call when subject reaches its terminal state + * or the single Subscriber unsubscribes mid-sequence. It will be called + * at most once. + * @return the created BufferUntilSubscriber instance + */ + public static UnicastSubject create(int capacityHint, Action0 onTerminated) { + State state = new State(capacityHint, onTerminated); + return new UnicastSubject(state); + } + final State state; private UnicastSubject(State state) { @@ -97,6 +120,8 @@ static final class State extends AtomicLong implements Producer, Observer, final Queue queue; /** JCTools queues don't accept nulls. */ final NotificationLite nl; + /** Atomically set to true on terminal condition. */ + final AtomicReference terminateOnce; /** In case the source emitted an error. */ Throwable error; /** Indicates the source has terminated. */ @@ -111,10 +136,14 @@ static final class State extends AtomicLong implements Producer, Observer, * Constructor. * @param capacityHint indicates how large each island in the Spsc queue should be to * reduce allocation frequency + * @param onTerminated the action to call when the subject reaches its terminal state or + * the single subscriber unsubscribes. */ - public State(int capacityHint) { + public State(int capacityHint, Action0 onTerminated) { this.nl = NotificationLite.instance(); this.subscriber = new AtomicReference>(); + this.terminateOnce = onTerminated != null ? new AtomicReference(onTerminated) : null; + Queue q; if (capacityHint > 1) { q = UnsafeAccess.isUnsafeAvailable() @@ -161,6 +190,9 @@ public void onNext(T t) { @Override public void onError(Throwable e) { if (!done) { + + doTerminate(); + error = e; done = true; if (!caughtUp) { @@ -179,6 +211,9 @@ public void onError(Throwable e) { @Override public void onCompleted() { if (!done) { + + doTerminate(); + done = true; if (!caughtUp) { boolean stillReplay = false; @@ -292,6 +327,9 @@ void replay() { */ @Override public void call() { + + doTerminate(); + done = true; synchronized (this) { if (emitting) { @@ -328,5 +366,18 @@ boolean checkTerminated(boolean done, boolean empty, Subscriber s) { } return false; } + + /** + * Call the optional termination action at most once. + */ + void doTerminate() { + AtomicReference ref = this.terminateOnce; + if (ref != null) { + Action0 a = ref.get(); + if (a != null && ref.compareAndSet(a, null)) { + a.call(); + } + } + } } } \ No newline at end of file diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 6b9cf90d3e..17655ab91f 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -25,14 +25,13 @@ /** * A {@code TestSubscriber} is a variety of {@link Subscriber} that you can use for unit testing, to perform * assertions, inspect received events, or wrap a mocked {@code Subscriber}. + * @param the value type */ public class TestSubscriber extends Subscriber { private final TestObserver testObserver; private final CountDownLatch latch = new CountDownLatch(1); private volatile Thread lastSeenThread; - /** Holds the initial request value. */ - private final long initialRequest; /** The shared no-op observer. */ private static final Observer INERT = new Observer() { @@ -78,7 +77,9 @@ public TestSubscriber(Observer delegate, long initialRequest) { throw new NullPointerException(); } this.testObserver = new TestObserver(delegate); - this.initialRequest = initialRequest; + if (initialRequest >= 0L) { + this.request(initialRequest); + } } /** @@ -112,6 +113,7 @@ public TestSubscriber() { /** * Factory method to construct a TestSubscriber with an initial request of Long.MAX_VALUE and no delegation. + * @param the value type * @return the created TestSubscriber instance * @since 1.1.0 */ @@ -121,6 +123,7 @@ public static TestSubscriber create() { /** * Factory method to construct a TestSubscriber with the given initial request amount and no delegation. + * @param the value type * @param initialRequest the initial request amount, negative values revert to the default unbounded mode * @return the created TestSubscriber instance * @since 1.1.0 @@ -132,6 +135,7 @@ public static TestSubscriber create(long initialRequest) { /** * Factory method to construct a TestSubscriber which delegates events to the given Observer and * issues the given initial request amount. + * @param the value type * @param delegate the observer to delegate events to * @param initialRequest the initial request amount, negative values revert to the default unbounded mode * @return the created TestSubscriber instance @@ -145,6 +149,7 @@ public static TestSubscriber create(Observer delegate, long initialReq /** * Factory method to construct a TestSubscriber which delegates events to the given Observer and * an issues an initial request of Long.MAX_VALUE. + * @param the value type * @param delegate the observer to delegate events to * @return the created TestSubscriber instance * @throws NullPointerException if delegate is null @@ -157,6 +162,7 @@ public static TestSubscriber create(Subscriber delegate) { /** * Factory method to construct a TestSubscriber which delegates events to the given Subscriber and * an issues an initial request of Long.MAX_VALUE. + * @param the value type * @param delegate the subscriber to delegate events to * @return the created TestSubscriber instance * @throws NullPointerException if delegate is null @@ -166,13 +172,6 @@ public static TestSubscriber create(Observer delegate) { return new TestSubscriber(delegate); } - @Override - public void onStart() { - if (initialRequest >= 0) { - requestMore(initialRequest); - } - } - /** * Notifies the Subscriber that the {@code Observable} has finished sending push-based notifications. *

diff --git a/src/test/java/rx/internal/operators/OperatorBufferTest.java b/src/test/java/rx/internal/operators/OperatorBufferTest.java index 75cddb8ad1..ad8b1bc9c7 100644 --- a/src/test/java/rx/internal/operators/OperatorBufferTest.java +++ b/src/test/java/rx/internal/operators/OperatorBufferTest.java @@ -949,10 +949,11 @@ public void call(final Subscriber s) { @Override public void request(long n) { - requested.set(n); - s.onNext(1); - s.onNext(2); - s.onNext(3); + if (BackpressureUtils.getAndAddRequest(requested, n) == 0) { + s.onNext(1); + s.onNext(2); + s.onNext(3); + } } }); @@ -1015,4 +1016,80 @@ public void onCompleted() { assertFalse(s.isUnsubscribed()); } + + @SuppressWarnings("unchecked") + @Test + public void testPostCompleteBackpressure() { + Observable> source = Observable.range(1, 10).buffer(3, 1); + + TestSubscriber> ts = TestSubscriber.create(0L); + + source.subscribe(ts); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertNoErrors(); + + ts.requestMore(7); + + ts.assertValues( + Arrays.asList(1, 2, 3), + Arrays.asList(2, 3, 4), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5, 6), + Arrays.asList(5, 6, 7), + Arrays.asList(6, 7, 8), + Arrays.asList(7, 8, 9) + ); + ts.assertNotCompleted(); + ts.assertNoErrors(); + + ts.requestMore(1); + + ts.assertValues( + Arrays.asList(1, 2, 3), + Arrays.asList(2, 3, 4), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5, 6), + Arrays.asList(5, 6, 7), + Arrays.asList(6, 7, 8), + Arrays.asList(7, 8, 9), + Arrays.asList(8, 9, 10) + ); + ts.assertNotCompleted(); + ts.assertNoErrors(); + + ts.requestMore(1); + + ts.assertValues( + Arrays.asList(1, 2, 3), + Arrays.asList(2, 3, 4), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5, 6), + Arrays.asList(5, 6, 7), + Arrays.asList(6, 7, 8), + Arrays.asList(7, 8, 9), + Arrays.asList(8, 9, 10), + Arrays.asList(9, 10) + ); + ts.assertNotCompleted(); + ts.assertNoErrors(); + + ts.requestMore(1); + + ts.assertValues( + Arrays.asList(1, 2, 3), + Arrays.asList(2, 3, 4), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5, 6), + Arrays.asList(5, 6, 7), + Arrays.asList(6, 7, 8), + Arrays.asList(7, 8, 9), + Arrays.asList(8, 9, 10), + Arrays.asList(9, 10), + Arrays.asList(10) + ); + ts.assertCompleted(); + ts.assertNoErrors(); + } } diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java index 9dade31fbc..3677e83e0a 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java @@ -324,4 +324,71 @@ public Observable> call(Observable t) { ts.assertNoErrors(); ts.assertCompleted(); } + + @Test + public void testBackpressureOuterOverlap() { + Observable> source = Observable.range(1, 10).window(3, 1); + + TestSubscriber> ts = TestSubscriber.create(0L); + + source.subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValueCount(1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(7); + + ts.assertValueCount(8); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(3); + + ts.assertValueCount(10); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test(expected = IllegalArgumentException.class) + public void testCountInvalid() { + Observable.range(1, 10).window(0, 1); + } + + @Test(expected = IllegalArgumentException.class) + public void testSkipInvalid() { + Observable.range(1, 10).window(3, 0); + } + @Test + public void testTake1Overlapping() { + Observable> source = Observable.range(1, 10).window(3, 1).take(1); + + TestSubscriber> ts = TestSubscriber.create(0L); + + source.subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValueCount(1); + ts.assertCompleted(); + ts.assertNoErrors(); + + TestSubscriber ts1 = TestSubscriber.create(); + + ts.getOnNextEvents().get(0).subscribe(ts1); + + ts1.assertValues(1, 2, 3); + ts1.assertCompleted(); + ts1.assertNoErrors(); + } } \ No newline at end of file From 7fefb4267a1b8ce737f0aa7371fabf1ae939e8ce Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 18 Mar 2016 11:06:26 +0100 Subject: [PATCH 022/322] 1.x: Release 1.1.2 CHANGES.md update --- CHANGES.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 09d87dda52..1d6ec229ea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,43 @@ # RxJava Releases # +### Version 1.1.2 - March 18, 2016 ([Maven](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.1.2%7C)) + +#### API Enhancements + + - [Pull 3766](https://github.com/ReactiveX/RxJava/pull/3766): Add `Single.onErrorResumeNext(Func1)` operator + - [Pull 3765](https://github.com/ReactiveX/RxJava/pull/3765): Add `Observable.switchOnNextDelayError` and `Observable.switchMapDelayError` operators + - [Pull 3759](https://github.com/ReactiveX/RxJava/pull/3759): Add `Observable.concatDelayError` and `Observable.concatMapDelayError` operators + - [Pull 3763](https://github.com/ReactiveX/RxJava/pull/3763): Add `Observable.combineLatestDelayError` operator + - [Pull 3752](https://github.com/ReactiveX/RxJava/pull/3752): Add `Single.using` operator + - [Pull 3722](https://github.com/ReactiveX/RxJava/pull/3722): Add `Observable.flatMapIterable` overload with `maxConcurrent` parameter + - [Pull 3741](https://github.com/ReactiveX/RxJava/pull/3741): Add `Single.doOnSubscribe` operator + - [Pull 3738](https://github.com/ReactiveX/RxJava/pull/3738): Add `Observable.create(SyncOnSubscribe)` and `Observable.create(AsyncOnSubscribe)` factory methods + - [Pull 3718](https://github.com/ReactiveX/RxJava/pull/3718): Add `Observable.concatMapIterable` operator + - [Pull 3712](https://github.com/ReactiveX/RxJava/pull/3712): Add `Single.takeUntil(Completable)` operator + - [Pull 3696](https://github.com/ReactiveX/RxJava/pull/3696): Added Single execution hooks via `RxJavaSingleExecutionHook` class. **Warning**! This PR introduced a binary incompatible change of `Single.unsafeSubscribe(Subscriber)` by changing its return type from `void` to `Subscription`. + - [Pull 3487](https://github.com/ReactiveX/RxJava/pull/3487): Add `onBackpressureBuffer` overflow strategies (oldest, newest, error) + +#### API deprecations + + - [Pull 3701](https://github.com/ReactiveX/RxJava/pull/3701): deprecate `Completable.doOnComplete` in favor of `Completable.doOnCompleted` (note the last d in the method name) + +#### Performance enhancements + + - [Pull 3759](https://github.com/ReactiveX/RxJava/pull/3759): Add `Observable.concatDelayError` and `Observable.concatMapDelayError` operators + - [Pull 3476](https://github.com/ReactiveX/RxJava/pull/3476): reduced `range` and `flatMap/merge` overhead + +#### Bugfixes + + - [Pull 3768](https://github.com/ReactiveX/RxJava/pull/3768): Fix `observeOn` in-sequence termination/unsubscription checking + - [Pull 3733](https://github.com/ReactiveX/RxJava/pull/3733): Avoid swallowing errors in `Completable` + - [Pull 3727](https://github.com/ReactiveX/RxJava/pull/3727): Fix `scan` not requesting `Long.MAX_VALUE` from upstream if downstream has requested `Long.MAX_VALUE` + - [Pull 3707](https://github.com/ReactiveX/RxJava/pull/3707): Lambda-based `Completable.subscribe()` methods should report `isUnsubscribed` properly + - [Pull 3702](https://github.com/ReactiveX/RxJava/pull/3702): Fix `mapNotification` backpressure handling + - [Pull 3697](https://github.com/ReactiveX/RxJava/pull/3697): Fix `ScalarSynchronousObservable` expecting the `Scheduler.computation()` to be `EventLoopsScheduler` all the time + - [Pull 3760](https://github.com/ReactiveX/RxJava/pull/3760): Fix ExecutorScheduler and GenericScheduledExecutorService reorder bug + - [Pull 3678](https://github.com/ReactiveX/RxJava/pull/3678): Fix counted buffer and window backpressure + + ### Version 1.1.1 - February 11, 2016 ([Maven](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.1.1%7C)) #### The new `Completable` class From da4474221b49d65f07f87eae506e6220b30f6db3 Mon Sep 17 00:00:00 2001 From: Niklas Baudy Date: Sat, 19 Mar 2016 13:28:02 +0100 Subject: [PATCH 023/322] Deprecate CompositeException constructor with message prefix --- .../java/rx/exceptions/CompositeException.java | 2 ++ src/main/java/rx/exceptions/Exceptions.java | 3 +-- .../rx/exceptions/CompositeExceptionTest.java | 17 +++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/exceptions/CompositeException.java b/src/main/java/rx/exceptions/CompositeException.java index 58930c061a..be6169060c 100644 --- a/src/main/java/rx/exceptions/CompositeException.java +++ b/src/main/java/rx/exceptions/CompositeException.java @@ -41,6 +41,8 @@ public final class CompositeException extends RuntimeException { private final List exceptions; private final String message; + /** @deprecated please use {@link #CompositeException(Collection)} */ + @Deprecated public CompositeException(String messagePrefix, Collection errors) { Set deDupedExceptions = new LinkedHashSet(); List _exceptions = new ArrayList(); diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index 2b94504c08..6c37167c3e 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -171,8 +171,7 @@ public static void throwIfAny(List exceptions) { throw new RuntimeException(t); } } - throw new CompositeException( - "Multiple exceptions", exceptions); + throw new CompositeException(exceptions); } } diff --git a/src/test/java/rx/exceptions/CompositeExceptionTest.java b/src/test/java/rx/exceptions/CompositeExceptionTest.java index ec3bd7b6c5..a08ce23382 100644 --- a/src/test/java/rx/exceptions/CompositeExceptionTest.java +++ b/src/test/java/rx/exceptions/CompositeExceptionTest.java @@ -23,6 +23,7 @@ import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.Test; @@ -50,7 +51,7 @@ public void testMultipleWithSameCause() { Throwable e1 = new Throwable("1", rootCause); Throwable e2 = new Throwable("2", rootCause); Throwable e3 = new Throwable("3", rootCause); - CompositeException ce = new CompositeException("3 failures with same root cause", Arrays.asList(e1, e2, e3)); + CompositeException ce = new CompositeException(Arrays.asList(e1, e2, e3)); System.err.println("----------------------------- print composite stacktrace"); ce.printStackTrace(); @@ -174,7 +175,7 @@ public void testNullCollection() { } @Test public void testNullElement() { - CompositeException composite = new CompositeException(Arrays.asList((Throwable)null)); + CompositeException composite = new CompositeException(Collections.singletonList((Throwable) null)); composite.getCause(); composite.printStackTrace(); } @@ -220,4 +221,16 @@ public synchronized Throwable initCause(Throwable cause) { System.err.println("----------------------------- print cause stacktrace"); cex.getCause().printStackTrace(); } + + @Test + public void messageCollection() { + CompositeException compositeException = new CompositeException(Arrays.asList(ex1, ex3)); + assertEquals("2 exceptions occurred. ", compositeException.getMessage()); + } + + @Test + public void messageVarargs() { + CompositeException compositeException = new CompositeException(ex1, ex2, ex3); + assertEquals("3 exceptions occurred. ", compositeException.getMessage()); + } } \ No newline at end of file From ef1d418f43e55eafb1cf9f09d13f37afb5b80b84 Mon Sep 17 00:00:00 2001 From: Galo Navarro Date: Thu, 17 Mar 2016 23:42:45 +0100 Subject: [PATCH 024/322] observeOn: allow configurable buffer size The observeOn operator is backed by a small queue of 128 slots that may overflow quickly on slow producers. This could only be avoided by adding a backpressure operator before the observeOn (not only inconvenient, but also taking a perf. hit as it forces hops between two queues). This patch allows modifying the default queue size on the observeOn operator. Fixes: #3751 Signed-off-by: Galo Navarro --- src/main/java/rx/Observable.java | 74 +++++++++++++++++-- .../internal/operators/OperatorObserveOn.java | 25 +++++-- .../operators/OperatorObserveOnTest.java | 63 ++++++++++++++++ 3 files changed, 150 insertions(+), 12 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 7468fccb47..7d8bd10f5c 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -6291,7 +6291,8 @@ public final Observable mergeWith(Observable t1) { /** * Modifies an Observable to perform its emissions and notifications on a specified {@link Scheduler}, - * asynchronously with a bounded buffer. + * asynchronously with a bounded buffer of {@link RxRingBuffer.SIZE} slots. + * *

Note that onError notifications will cut ahead of onNext notifications on the emission thread if Scheduler is truly * asynchronous. If strict event ordering is required, consider using the {@link #observeOn(Scheduler, boolean)} overload. *

@@ -6308,13 +6309,41 @@ public final Observable mergeWith(Observable t1) { * @see ReactiveX operators documentation: ObserveOn * @see RxJava Threading Examples * @see #subscribeOn + * @see #observeOn(Scheduler, int) * @see #observeOn(Scheduler, boolean) + * @see #observeOn(Scheduler, boolean, int) */ public final Observable observeOn(Scheduler scheduler) { - if (this instanceof ScalarSynchronousObservable) { - return ((ScalarSynchronousObservable)this).scalarScheduleOn(scheduler); - } - return lift(new OperatorObserveOn(scheduler, false)); + return observeOn(scheduler, RxRingBuffer.SIZE); + } + + /** + * Modifies an Observable to perform its emissions and notifications on a specified {@link Scheduler}, + * asynchronously with a bounded buffer of configurable size other than the {@link RxRingBuffer.SIZE} + * default. + * + *

Note that onError notifications will cut ahead of onNext notifications on the emission thread if Scheduler is truly + * asynchronous. If strict event ordering is required, consider using the {@link #observeOn(Scheduler, boolean)} overload. + *

+ * + *

+ *
Scheduler:
+ *
you specify which {@link Scheduler} this operator will use
+ *
+ * + * @param scheduler the {@link Scheduler} to notify {@link Observer}s on + * @param bufferSize the size of the buffer. + * @return the source Observable modified so that its {@link Observer}s are notified on the specified + * {@link Scheduler} + * @see ReactiveX operators documentation: ObserveOn + * @see RxJava Threading Examples + * @see #subscribeOn + * @see #observeOn(Scheduler) + * @see #observeOn(Scheduler, boolean) + * @see #observeOn(Scheduler, boolean, int) + */ + public final Observable observeOn(Scheduler scheduler, int bufferSize) { + return observeOn(scheduler, false, bufferSize); } /** @@ -6339,12 +6368,45 @@ public final Observable observeOn(Scheduler scheduler) { * @see RxJava Threading Examples * @see #subscribeOn * @see #observeOn(Scheduler) + * @see #observeOn(Scheduler, int) + * @see #observeOn(Scheduler, boolean, int) */ public final Observable observeOn(Scheduler scheduler, boolean delayError) { + return observeOn(scheduler, delayError, RxRingBuffer.SIZE); + } + + /** + * Modifies an Observable to perform its emissions and notifications on a specified {@link Scheduler}, + * asynchronously with a bounded buffer of configurable size other than the {@link RxRingBuffer.SIZE} + * default, and optionally delays onError notifications. + *

+ * + *

+ *
Scheduler:
+ *
you specify which {@link Scheduler} this operator will use
+ *
+ * + * @param scheduler + * the {@link Scheduler} to notify {@link Observer}s on + * @param delayError + * indicates if the onError notification may not cut ahead of onNext notification on the other side of the + * scheduling boundary. If true a sequence ending in onError will be replayed in the same order as was received + * from upstream + * @param bufferSize the size of the buffer. + * @return the source Observable modified so that its {@link Observer}s are notified on the specified + * {@link Scheduler} + * @see ReactiveX operators documentation: ObserveOn + * @see RxJava Threading Examples + * @see #subscribeOn + * @see #observeOn(Scheduler) + * @see #observeOn(Scheduler, int) + * @see #observeOn(Scheduler, boolean) + */ + public final Observable observeOn(Scheduler scheduler, boolean delayError, int bufferSize) { if (this instanceof ScalarSynchronousObservable) { return ((ScalarSynchronousObservable)this).scalarScheduleOn(scheduler); } - return lift(new OperatorObserveOn(scheduler, delayError)); + return lift(new OperatorObserveOn(scheduler, delayError, bufferSize)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index 51d6fc7a23..2a7c7684dd 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -40,14 +40,25 @@ public final class OperatorObserveOn implements Operator { private final Scheduler scheduler; private final boolean delayError; + private final int bufferSize; /** * @param scheduler the scheduler to use * @param delayError delay errors until all normal events are emitted in the other thread? */ public OperatorObserveOn(Scheduler scheduler, boolean delayError) { + this(scheduler, delayError, RxRingBuffer.SIZE); + } + + /** + * @param scheduler the scheduler to use + * @param delayError delay errors until all normal events are emitted in the other thread? + * @param bufferSize for the buffer feeding the Scheduler workers, defaults to {@code RxRingBuffer.MAX} if <= 0 + */ + public OperatorObserveOn(Scheduler scheduler, boolean delayError, int bufferSize) { this.scheduler = scheduler; this.delayError = delayError; + this.bufferSize = (bufferSize > 0) ? bufferSize : RxRingBuffer.SIZE; } @Override @@ -59,7 +70,7 @@ public Subscriber call(Subscriber child) { // avoid overhead, execute directly return child; } else { - ObserveOnSubscriber parent = new ObserveOnSubscriber(scheduler, child, delayError); + ObserveOnSubscriber parent = new ObserveOnSubscriber(scheduler, child, delayError, bufferSize); parent.init(); return parent; } @@ -72,6 +83,7 @@ private static final class ObserveOnSubscriber extends Subscriber implemen final NotificationLite on; final boolean delayError; final Queue queue; + final int bufferSize; // the status of the current stream volatile boolean finished; @@ -88,15 +100,16 @@ private static final class ObserveOnSubscriber extends Subscriber implemen // do NOT pass the Subscriber through to couple the subscription chain ... unsubscribing on the parent should // not prevent anything downstream from consuming, which will happen if the Subscription is chained - public ObserveOnSubscriber(Scheduler scheduler, Subscriber child, boolean delayError) { + public ObserveOnSubscriber(Scheduler scheduler, Subscriber child, boolean delayError, int bufferSize) { this.child = child; this.recursiveScheduler = scheduler.createWorker(); this.delayError = delayError; this.on = NotificationLite.instance(); + this.bufferSize = (bufferSize > 0) ? bufferSize : RxRingBuffer.SIZE; if (UnsafeAccess.isUnsafeAvailable()) { - queue = new SpscArrayQueue(RxRingBuffer.SIZE); + queue = new SpscArrayQueue(this.bufferSize); } else { - queue = new SpscAtomicArrayQueue(RxRingBuffer.SIZE); + queue = new SpscAtomicArrayQueue(this.bufferSize); } } @@ -123,7 +136,7 @@ public void request(long n) { @Override public void onStart() { // signal that this is an async operator capable of receiving this many - request(RxRingBuffer.SIZE); + request(this.bufferSize); } @Override @@ -180,7 +193,7 @@ public void call() { // requested and counter are not included to avoid JIT issues with register spilling // and their access is is amortized because they are part of the outer loop which runs - // less frequently (usually after each RxRingBuffer.SIZE elements) + // less frequently (usually after each bufferSize elements) for (;;) { long requestAmount = requested.get(); diff --git a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java index d0ba44be23..8ebc69eed7 100644 --- a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java @@ -580,6 +580,69 @@ public void onNext(Integer t) { } } + @Test + public void testQueueFullEmitsErrorWithVaryingBufferSize() { + final CountDownLatch latch = new CountDownLatch(1); + // randomize buffer size, note that underlying implementations may be tuning the real size to a power of 2 + // which can lead to unexpected results when adding excess capacity (e.g.: see ConcurrentCircularArrayQueue) + for (int i = 1; i <= 1024; i = i * 2) { + final int capacity = i; + Observable observable = Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber o) { + for (int i = 0; i < capacity + 10; i++) { + o.onNext(i); + } + latch.countDown(); + o.onCompleted(); + } + + }); + + TestSubscriber testSubscriber = new TestSubscriber(new Observer() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + try { + // force it to be slow wait until we have queued everything + latch.await(500, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + }); + System.out.println("Using capacity " + capacity); // for post-failure debugging + observable.observeOn(Schedulers.newThread(), capacity).subscribe(testSubscriber); + + testSubscriber.awaitTerminalEvent(); + List errors = testSubscriber.getOnErrorEvents(); + assertEquals(1, errors.size()); + System.out.println("Errors: " + errors); + Throwable t = errors.get(0); + if (t instanceof MissingBackpressureException) { + // success, we expect this + } else { + if (t.getCause() instanceof MissingBackpressureException) { + // this is also okay + } else { + fail("Expecting MissingBackpressureException"); + } + } + } + } + @Test public void testAsyncChild() { TestSubscriber ts = new TestSubscriber(); From c8a2cfa78c025bed0d6c499f80ffb2d13dc918f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 21 Mar 2016 20:10:18 +0100 Subject: [PATCH 025/322] 1.x: fix GroupBy delaying group completion till all groups were emitted --- .../internal/operators/OperatorGroupBy.java | 13 +++++----- src/test/java/rx/GroupByTests.java | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 38edc0a68f..4fe29b6c2d 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -219,6 +219,12 @@ public void onCompleted() { if (done) { return; } + + for (GroupedUnicast e : groups.values()) { + e.onComplete(); + } + groups.clear(); + done = true; GROUP_COUNT.decrementAndGet(this); drain(); @@ -328,13 +334,6 @@ boolean checkTerminated(boolean d, boolean empty, return true; } else if (empty) { - List> list = new ArrayList>(groups.values()); - groups.clear(); - - for (GroupedUnicast e : list) { - e.onComplete(); - } - actual.onCompleted(); return true; } diff --git a/src/test/java/rx/GroupByTests.java b/src/test/java/rx/GroupByTests.java index 3530c08799..a4527777ef 100644 --- a/src/test/java/rx/GroupByTests.java +++ b/src/test/java/rx/GroupByTests.java @@ -21,6 +21,7 @@ import rx.functions.Action1; import rx.functions.Func1; import rx.observables.GroupedObservable; +import rx.observers.TestSubscriber; public class GroupByTests { @@ -90,4 +91,27 @@ public void call(String v) { System.out.println("**** finished"); } + + @Test + public void groupsCompleteAsSoonAsMainCompletes() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(0, 20) + .groupBy(new Func1() { + @Override + public Integer call(Integer i) { + return i % 5; + } + }) + .concatMap(new Func1, Observable>() { + @Override + public Observable call(GroupedObservable v) { + return v; + } + }).subscribe(ts); + + ts.assertValues(0, 5, 10, 15, 1, 6, 11, 16, 2, 7, 12, 17, 3, 8, 13, 18, 4, 9, 14, 19); + ts.assertCompleted(); + ts.assertNoErrors(); + } } From db14c09d6257c537ce0b77f004410ce1606db73f Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Wed, 23 Mar 2016 03:55:35 +0300 Subject: [PATCH 026/322] 1.x: Expose Single.lift() --- src/main/java/rx/Single.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 3489f1a55c..b7e99671d2 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -163,9 +163,8 @@ public interface OnSubscribe extends Action1> { * @return a Single that is the result of applying the lifted Operator to the source Single * @see RxJava wiki: Implementing Your Own Operators */ - private Single lift(final Operator lift) { - // This method is private because not sure if we want to expose the Observable.Operator in this public API rather than a Single.Operator - + @Experimental + public final Single lift(final Operator lift) { return new Single(new Observable.OnSubscribe() { @Override public void call(Subscriber o) { From 4378e8acba1b0078cce6fc2519b0d37812d89eff Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Wed, 23 Mar 2016 03:25:22 +0300 Subject: [PATCH 027/322] 1.x: Prevent Single.zip() of zero Singles --- src/main/java/rx/Single.java | 3 ++- .../internal/operators/SingleOperatorZip.java | 6 ++++++ src/test/java/rx/SingleTest.java | 20 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 3489f1a55c..dcd296483d 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1235,7 +1235,8 @@ public R call(Object... args) { * * * @param singles - * an Iterable of source Singles + * an Iterable of source Singles. Should not be empty because {@link Single} either emits result or error. + * {@link java.util.NoSuchElementException} will be emit as error if Iterable will be empty. * @param zipFunction * a function that, when applied to an item emitted by each of the source Singles, results in * an item that will be emitted by the resulting Single diff --git a/src/main/java/rx/internal/operators/SingleOperatorZip.java b/src/main/java/rx/internal/operators/SingleOperatorZip.java index 936750941f..d80c5ae056 100644 --- a/src/main/java/rx/internal/operators/SingleOperatorZip.java +++ b/src/main/java/rx/internal/operators/SingleOperatorZip.java @@ -7,6 +7,7 @@ import rx.plugins.RxJavaPlugins; import rx.subscriptions.CompositeSubscription; +import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -16,6 +17,11 @@ public static Single zip(final Single[] singles, final Fu return Single.create(new Single.OnSubscribe() { @Override public void call(final SingleSubscriber subscriber) { + if (singles.length == 0) { + subscriber.onError(new NoSuchElementException("Can't zip 0 Singles.")); + return; + } + final AtomicInteger wip = new AtomicInteger(singles.length); final AtomicBoolean once = new AtomicBoolean(); final Object[] values = new Object[singles.length]; diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 8c6257784d..24855bf415 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -340,6 +340,26 @@ public String call(Object... args) { ts.assertCompleted(); } + @Test + public void zipEmptyIterableShouldThrow() { + TestSubscriber testSubscriber = new TestSubscriber(); + Iterable> singles = Collections.emptyList(); + + Single + .zip(singles, new FuncN() { + @Override + public Object call(Object... args) { + throw new IllegalStateException("Should not be called"); + } + }) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertNotCompleted(); + testSubscriber.assertError(NoSuchElementException.class); + assertEquals("Can't zip 0 Singles.", testSubscriber.getOnErrorEvents().get(0).getMessage()); + } + @Test public void testZipWith() { TestSubscriber ts = new TestSubscriber(); From b93ef68cec82f7fd2cf867e0fd3f0d72489f7413 Mon Sep 17 00:00:00 2001 From: Sebas LG Date: Wed, 23 Mar 2016 12:16:31 +0100 Subject: [PATCH 028/322] 1.x: Fix delay methods typos in documenation --- src/main/java/rx/Observable.java | 4 ++-- src/main/java/rx/Single.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 7468fccb47..8a122f0f6d 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4434,7 +4434,7 @@ public final Observable delay(Func1> i * *
*
Scheduler:
- *
This version of {@code delay} operates by default on the {@code compuation} {@link Scheduler}.
+ *
This version of {@code delay} operates by default on the {@code computation} {@link Scheduler}.
*
* * @param delay @@ -4477,7 +4477,7 @@ public final Observable delay(long delay, TimeUnit unit, Scheduler scheduler) * *
*
Scheduler:
- *
This version of {@code delay} operates by default on the {@code compuation} {@link Scheduler}.
+ *
This version of {@code delay} operates by default on the {@code computation} {@link Scheduler}.
*
* * @param delay diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 3489f1a55c..48ca5004f0 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -2362,7 +2362,7 @@ public final Single delay(long delay, TimeUnit unit, Scheduler scheduler) { * *
*
Scheduler:
- *
This version of {@code delay} operates by default on the {@code compuation} {@link Scheduler}.
+ *
This version of {@code delay} operates by default on the {@code computation} {@link Scheduler}.
*
* * @param delay From 26500c5ca59a10b07880c63a78881f65bdd60247 Mon Sep 17 00:00:00 2001 From: Sebas LG Date: Mon, 28 Mar 2016 15:26:33 +0200 Subject: [PATCH 029/322] 1.x: Fix typos in documentation and some code --- build.gradle | 2 +- src/main/java/rx/Completable.java | 18 ++++++------ src/main/java/rx/Observable.java | 28 +++++++++---------- src/main/java/rx/Producer.java | 2 +- src/main/java/rx/Single.java | 6 ++-- src/main/java/rx/Subscriber.java | 4 +-- .../internal/operators/BackpressureUtils.java | 4 +-- .../operators/OnSubscribeGroupJoin.java | 2 +- .../operators/OperatorBufferWithTime.java | 2 +- .../rx/internal/operators/OperatorMerge.java | 2 +- .../OperatorOnBackpressureLatest.java | 2 +- .../internal/operators/OperatorPublish.java | 2 +- .../rx/internal/operators/OperatorReplay.java | 6 ++-- .../internal/operators/OperatorTakeTimed.java | 2 +- .../rx/internal/producers/QueuedProducer.java | 2 +- .../producers/QueuedValueProducer.java | 2 +- .../GenericScheduledExecutorService.java | 2 +- .../util/BackpressureDrainManager.java | 2 +- .../unsafe/ConcurrentCircularArrayQueue.java | 2 +- .../rx/observables/BlockingObservable.java | 2 +- .../rx/observables/GroupedObservable.java | 2 +- .../RxJavaObservableExecutionHook.java | 24 ++++++++-------- src/main/java/rx/plugins/RxJavaPlugins.java | 2 +- .../java/rx/plugins/RxJavaSchedulersHook.java | 2 +- .../rx/plugins/RxJavaSingleExecutionHook.java | 24 ++++++++-------- src/main/java/rx/schedulers/Schedulers.java | 2 +- src/main/java/rx/subjects/ReplaySubject.java | 4 +-- src/main/java/rx/subjects/TestSubject.java | 6 ++-- src/test/java/rx/BackpressureTests.java | 6 ++-- src/test/java/rx/SingleTest.java | 2 +- src/test/java/rx/SubscriberTest.java | 2 +- .../java/rx/exceptions/OnNextValueTest.java | 2 +- .../java/rx/exceptions/TestException.java | 2 +- .../operators/OnSubscribeUsingTest.java | 2 +- .../operators/OperatorConcatTest.java | 2 +- .../operators/OperatorFlatMapTest.java | 2 +- .../OperatorOnBackpressureBufferTest.java | 2 +- .../OperatorPublishFunctionTest.java | 4 +-- .../internal/operators/OperatorRetryTest.java | 2 +- .../operators/OperatorTimeoutTests.java | 2 +- .../OperatorTimeoutWithSelectorTest.java | 2 +- .../internal/operators/OperatorZipTest.java | 2 +- .../operators/SingleOnSubscribeUsingTest.java | 2 +- .../schedulers/TrampolineSchedulerTest.java | 2 +- .../ReplaySubjectBoundedConcurrencyTest.java | 2 +- .../ReplaySubjectConcurrencyTest.java | 2 +- .../CompositeSubscriptionTest.java | 2 +- .../SerialSubscriptionTests.java | 2 +- .../rx/test/TestObstructionDetection.java | 2 +- 49 files changed, 104 insertions(+), 104 deletions(-) diff --git a/build.gradle b/build.gradle index f465376da3..1806a43120 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ javadoc { options { windowTitle = "RxJava Javadoc ${project.version}" } - // Clear the following options to make the docs consitent with the old format + // Clear the following options to make the docs consistent with the old format options.addStringOption('top').value = '' options.addStringOption('doctitle').value = '' options.addStringOption('header').value = '' diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index f2e752c2b2..eb3cf76d54 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -752,7 +752,7 @@ static T requireNonNull(T o) { } /** - * Returns a Completable instance that fires its onComplete event after the given delay ellapsed. + * Returns a Completable instance that fires its onComplete event after the given delay elapsed. * @param delay the delay time * @param unit the delay unit * @return the new Completable instance @@ -762,7 +762,7 @@ public static Completable timer(long delay, TimeUnit unit) { } /** - * Returns a Completable instance that fires its onComplete event after the given delay ellapsed + * Returns a Completable instance that fires its onComplete event after the given delay elapsed * by using the supplied scheduler. * @param delay the delay time * @param unit the delay unit @@ -1040,7 +1040,7 @@ public void onSubscribe(Subscription d) { * @param timeout the timeout value * @param unit the timeout unit * @return true if the this Completable instance completed normally within the time limit, - * false if the timeout ellapsed before this Completable terminated. + * false if the timeout elapsed before this Completable terminated. * @throws RuntimeException wrapping an InterruptedException if the current thread is interrupted */ public final boolean await(long timeout, TimeUnit unit) { @@ -1239,7 +1239,7 @@ public final Completable doOnCompleted(Action0 onCompleted) { } /** - * Returns a Completable which calls the giveon onUnsubscribe callback if the child subscriber cancels + * Returns a Completable which calls the given onUnsubscribe callback if the child subscriber cancels * the subscription. * @param onUnsubscribe the callback to call when the child subscriber cancels the subscription * @return the new Completable instance @@ -1395,7 +1395,7 @@ public final Observable endWith(Observable next) { } /** - * Returns a Completable instace that calls the given onAfterComplete callback after this + * Returns a Completable instance that calls the given onAfterComplete callback after this * Completable completes normally. * @param onAfterComplete the callback to call after this Completable emits an onComplete event. * @return the new Completable instance @@ -1448,10 +1448,10 @@ public void onSubscribe(Subscription d) { /** * Subscribes to this Completable instance and blocks until it terminates or the specified timeout - * ellapses, then returns null for normal termination or the emitted exception if any. + * elapses, then returns null for normal termination or the emitted exception if any. * @return the throwable if this terminated with an error, null otherwise * @throws RuntimeException that wraps an InterruptedException if the wait is interrupted or - * TimeoutException if the specified timeout ellapsed before it + * TimeoutException if the specified timeout elapsed before it */ public final Throwable get(long timeout, TimeUnit unit) { requireNonNull(unit); @@ -2128,7 +2128,7 @@ public void call(Subscriber s) { } /** - * Convers this Completable into a Single which when this Completable completes normally, + * Converts this Completable into a Single which when this Completable completes normally, * calls the given supplier and emits its returned value through onSuccess. * @param completionValueFunc0 the value supplier called when this Completable completes normally * @return the new Single instance @@ -2175,7 +2175,7 @@ public void onSubscribe(Subscription d) { } /** - * Convers this Completable into a Single which when this Completable completes normally, + * Converts this Completable into a Single which when this Completable completes normally, * emits the given value through onSuccess. * @param completionValue the value to emit when this Completable completes normally * @return the new Single instance diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index b89bc431e2..74fcf88fe9 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -102,7 +102,7 @@ public static Observable create(OnSubscribe f) { * *

Note: the {@code SyncOnSubscribe} provides a generic way to fulfill data by iterating * over a (potentially stateful) function (e.g. reading data off of a channel, a parser, ). If your - * data comes directly from an asyrchronous/potentially concurrent source then consider using the + * data comes directly from an asynchronous/potentially concurrent source then consider using the * {@link Observable#create(AsyncOnSubscribe) asynchronous overload}. * *

@@ -3052,7 +3052,7 @@ public static Observable using( * Constructs an Observable that creates a dependent resource object which is disposed of just before * termination if you have set {@code disposeEagerly} to {@code true} and unsubscription does not occur * before termination. Otherwise resource disposal will occur on unsubscription. Eager disposal is - * particularly appropriate for a synchronous Observable that resuses resources. {@code disposeAction} will + * particularly appropriate for a synchronous Observable that reuses resources. {@code disposeAction} will * only be called once per subscription. *

* @@ -3094,7 +3094,7 @@ public static Observable using( * item emitted by each of those Observables; and so forth. *

* The resulting {@code Observable} returned from {@code zip} will invoke {@code onNext} as many times as - * the number of {@code onNext} invokations of the source Observable that emits the fewest items. + * the number of {@code onNext} invocations of the source Observable that emits the fewest items. *

* *

@@ -3128,7 +3128,7 @@ public static Observable zip(Iterable> ws, FuncN< * function applied to the second item emitted by each of those Observables; and so forth. *

* The resulting {@code Observable} returned from {@code zip} will invoke {@code onNext} as many times as - * the number of {@code onNext} invokations of the source Observable that emits the fewest items. + * the number of {@code onNext} invocations of the source Observable that emits the fewest items. *

* *

@@ -3900,7 +3900,7 @@ public final Observable> buffer(Observable boundary, int initialC * subscribe/unsubscribe behavior of all the {@link Subscriber}s. *

* When you call {@code cache}, it does not yet subscribe to the source Observable and so does not yet - * begin cacheing items. This only happens when the first Subscriber calls the resulting Observable's + * begin caching items. This only happens when the first Subscriber calls the resulting Observable's * {@code subscribe} method. *

* Note: You sacrifice the ability to unsubscribe from the origin when you use the {@code cache} @@ -3943,7 +3943,7 @@ public final Observable cache(int initialCapacity) { * subscribe/unsubscribe behavior of all the {@link Subscriber}s. *

* When you call {@code cache}, it does not yet subscribe to the source Observable and so does not yet - * begin cacheing items. This only happens when the first Subscriber calls the resulting Observable's + * begin caching items. This only happens when the first Subscriber calls the resulting Observable's * {@code subscribe} method. *

* Note: You sacrifice the ability to unsubscribe from the origin when you use the {@code cache} @@ -4367,7 +4367,7 @@ public final Observable switchIfEmpty(Observable alternate) { } /** - * Returns an Observable that delays the subscription to and emissions from the souce Observable via another + * Returns an Observable that delays the subscription to and emissions from the source Observable via another * Observable on a per-item basis. *

* @@ -5357,7 +5357,7 @@ public final Observable concatMapEager(Func1 * *

@@ -5821,7 +5821,7 @@ public final Observable flatMapIterable(Func1 * the collection element type * @param - * the type of item emited by the resulting Observable + * the type of item emitted by the resulting Observable * @param collectionSelector * a function that returns an Iterable sequence of values for each item emitted by the source * Observable @@ -5851,7 +5851,7 @@ public final Observable flatMapIterable(Func1 * the collection element type * @param - * the type of item emited by the resulting Observable + * the type of item emitted by the resulting Observable * @param collectionSelector * a function that returns an Iterable sequence of values for each item emitted by the source * Observable @@ -6813,7 +6813,7 @@ public final Observable reduce(Func2 accumulator) { *

* *

- * This technique, which is called "reduce" here, is sometimec called "aggregate," "fold," "accumulate," + * This technique, which is called "reduce" here, is sometimes called "aggregate," "fold," "accumulate," * "compress," or "inject" in other programming contexts. Groovy, for instance, has an {@code inject} method * that does a similar operation on lists. *

@@ -10355,7 +10355,7 @@ public final Observable> window(long timespan, long timeshift, Tim * new window * @param unit * the unit of time that applies to the {@code timespan} argument - * @return an Observable that emits connected, non-overlapping windows represending items emitted by the + * @return an Observable that emits connected, non-overlapping windows representing items emitted by the * source Observable during fixed, consecutive durations * @see ReactiveX operators documentation: Window */ @@ -10581,9 +10581,9 @@ private static class Holder { } /** - * Returns a singleton instance of NeverObservble (cast to the generic type). + * Returns a singleton instance of NeverObservable (cast to the generic type). * - * @return + * @return singleton instance of NeverObservable (cast to the generic type) */ @SuppressWarnings("unchecked") static NeverObservable instance() { diff --git a/src/main/java/rx/Producer.java b/src/main/java/rx/Producer.java index 9bb9cc22d1..fb7f211e7c 100644 --- a/src/main/java/rx/Producer.java +++ b/src/main/java/rx/Producer.java @@ -21,7 +21,7 @@ * backpressure). * *

The request amount only affects calls to {@link Subscriber#onNext(Object)}; onError and onCompleted may appear without - * requrests. + * requests. * *

However, backpressure is somewhat optional in RxJava 1.x and Subscribers may not * receive a Producer via their {@link Subscriber#setProducer(Producer)} method and will run diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index f5e1156acc..628e716ed8 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1719,7 +1719,7 @@ public final Subscription subscribe(Subscriber subscriber) { subscriber = new SafeSubscriber(subscriber); } - // The code below is exactly the same an unsafeSubscribe but not used because it would add a sigificent depth to alreay huge call stacks. + // The code below is exactly the same an unsafeSubscribe but not used because it would add a significant depth to already huge call stacks. try { // allow the hook to intercept and/or decorate hook.onSubscribeStart(this, onSubscribe).call(subscriber); @@ -2186,7 +2186,7 @@ public final Single timeout(long timeout, TimeUnit timeUnit, SingleReactiveX operators documentation: Timeout */ public final Single timeout(long timeout, TimeUnit timeUnit, Single other, Scheduler scheduler) { @@ -2630,7 +2630,7 @@ public static Single using( * Constructs an Single that creates a dependent resource object which is disposed of just before * termination if you have set {@code disposeEagerly} to {@code true} and unsubscription does not occur * before termination. Otherwise resource disposal will occur on unsubscription. Eager disposal is - * particularly appropriate for a synchronous Single that resuses resources. {@code disposeAction} will + * particularly appropriate for a synchronous Single that reuses resources. {@code disposeAction} will * only be called once per subscription. *

* diff --git a/src/main/java/rx/Subscriber.java b/src/main/java/rx/Subscriber.java index 67ac611e4c..cfbfcc2496 100644 --- a/src/main/java/rx/Subscriber.java +++ b/src/main/java/rx/Subscriber.java @@ -193,9 +193,9 @@ public void setProducer(Producer p) { toRequest = requested; producer = p; if (subscriber != null) { - // middle operator ... we pass thru unless a request has been made + // middle operator ... we pass through unless a request has been made if (toRequest == NOT_SET) { - // we pass-thru to the next producer as nothing has been requested + // we pass through to the next producer as nothing has been requested passToSubscriber = true; } } diff --git a/src/main/java/rx/internal/operators/BackpressureUtils.java b/src/main/java/rx/internal/operators/BackpressureUtils.java index cfbe282901..3d199567c6 100644 --- a/src/main/java/rx/internal/operators/BackpressureUtils.java +++ b/src/main/java/rx/internal/operators/BackpressureUtils.java @@ -246,10 +246,10 @@ static void postCompleteDrain(AtomicLong requested, Queue queue, Subscrib * in which we know the queue won't change anymore (i.e., done is always true * when looking at the classical algorithm and there is no error). * - * Note that we don't check for cancellation or emptyness upfront for two reasons: + * Note that we don't check for cancellation or emptiness upfront for two reasons: * 1) if e != r, the loop will do this and we quit appropriately * 2) if e == r, then either there was no outstanding requests or we emitted the requested amount - * and the execution simply falls to the e == r check below which checks for emptyness anyway. + * and the execution simply falls to the e == r check below which checks for emptiness anyway. */ while (e != r) { diff --git a/src/main/java/rx/internal/operators/OnSubscribeGroupJoin.java b/src/main/java/rx/internal/operators/OnSubscribeGroupJoin.java index 4b7509c2d9..a8f13fb5e7 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeGroupJoin.java +++ b/src/main/java/rx/internal/operators/OnSubscribeGroupJoin.java @@ -28,7 +28,7 @@ import rx.subscriptions.*; /** - * Corrrelates two sequences when they overlap and groups the results. + * Correlates two sequences when they overlap and groups the results. * * @see MSDN: Observable.GroupJoin * @param the left value type diff --git a/src/main/java/rx/internal/operators/OperatorBufferWithTime.java b/src/main/java/rx/internal/operators/OperatorBufferWithTime.java index bbb723d2b3..b13fe2fa9e 100644 --- a/src/main/java/rx/internal/operators/OperatorBufferWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorBufferWithTime.java @@ -90,7 +90,7 @@ public Subscriber call(final Subscriber> child) { bsub.scheduleChunk(); return bsub; } - /** Subscriber when the buffer chunking time and lenght differ. */ + /** Subscriber when the buffer chunking time and length differ. */ final class InexactSubscriber extends Subscriber { final Subscriber> child; final Worker inner; diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index a9c7b86b09..69e92bb08e 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -614,7 +614,7 @@ void emitLoop() { } /* - * We need to read done before innerSubscribers because innerSubcribers are added + * We need to read done before innerSubscribers because innerSubscribers are added * before done is set to true. If it were the other way around, we could read an empty * innerSubscribers, get paused and then read a done flag but an async producer * might have added more subscribers between the two. diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java index 2bf909289e..15078b0614 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java @@ -50,7 +50,7 @@ public Subscriber call(Subscriber child) { return parent; } /** - * A terminatable producer which emits the latest items on request. + * A terminable producer which emits the latest items on request. * @param */ static final class LatestEmitter extends AtomicLong implements Producer, Subscription, Observer { diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index bd66fc86ab..347905e06e 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -486,7 +486,7 @@ void dispatch() { try { for (;;) { /* - * We need to read terminalEvent before checking the queue for emptyness because + * We need to read terminalEvent before checking the queue for emptiness because * all enqueue happens before setting the terminal event. * If it were the other way around, when the emission is paused between * checking isEmpty and checking terminalEvent, some other thread might diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index a76f2f3c0b..f4af56bcb9 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -377,7 +377,7 @@ boolean add(InnerProducer producer) { if (producers.compareAndSet(c, u)) { return true; } - // if failed, some other operation succeded (another add, remove or termination) + // if failed, some other operation succeeded (another add, remove or termination) // so retry } } @@ -633,7 +633,7 @@ public void request(long n) { if (compareAndSet(r, u)) { // increment the total request counter addTotalRequested(n); - // if successful, notify the parent dispacher this child can receive more + // if successful, notify the parent dispatcher this child can receive more // elements parent.manageRequests(); @@ -687,7 +687,7 @@ public long produced(long n) { } // try updating the request value if (compareAndSet(r, u)) { - // and return the udpated value + // and return the updated value return u; } // otherwise, some concurrent activity happened and we need to retry diff --git a/src/main/java/rx/internal/operators/OperatorTakeTimed.java b/src/main/java/rx/internal/operators/OperatorTakeTimed.java index ea56be87ac..faa95b4b79 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeTimed.java +++ b/src/main/java/rx/internal/operators/OperatorTakeTimed.java @@ -24,7 +24,7 @@ import rx.observers.SerializedSubscriber; /** - * Takes values from the source until the specific time ellapses. + * Takes values from the source until the specific time elapses. * * @param * the result value type diff --git a/src/main/java/rx/internal/producers/QueuedProducer.java b/src/main/java/rx/internal/producers/QueuedProducer.java index 51747dd9b9..ed892bab30 100644 --- a/src/main/java/rx/internal/producers/QueuedProducer.java +++ b/src/main/java/rx/internal/producers/QueuedProducer.java @@ -76,7 +76,7 @@ public void request(long n) { } /** - * Offers a value to this producer and tries to emit any queud values + * Offers a value to this producer and tries to emit any queued values * if the child requests allow it. * @param value the value to enqueue and attempt to drain * @return true if the queue accepted the offer, false otherwise diff --git a/src/main/java/rx/internal/producers/QueuedValueProducer.java b/src/main/java/rx/internal/producers/QueuedValueProducer.java index d165a412b7..53853f2ccc 100644 --- a/src/main/java/rx/internal/producers/QueuedValueProducer.java +++ b/src/main/java/rx/internal/producers/QueuedValueProducer.java @@ -73,7 +73,7 @@ public void request(long n) { } /** - * Offers a value to this producer and tries to emit any queud values + * Offers a value to this producer and tries to emit any queued values * if the child requests allow it. * @param value the value to enqueue and attempt to drain * @return true if the queue accepted the offer, false otherwise diff --git a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java index 87f7ec5f88..e322945068 100644 --- a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java +++ b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java @@ -44,7 +44,7 @@ public final class GenericScheduledExecutorService implements SchedulerLifecycle SHUTDOWN.shutdown(); } - /* Schedulers needs acces to this in order to work with the lifecycle. */ + /* Schedulers needs access to this in order to work with the lifecycle. */ public final static GenericScheduledExecutorService INSTANCE = new GenericScheduledExecutorService(); private final AtomicReference executor; diff --git a/src/main/java/rx/internal/util/BackpressureDrainManager.java b/src/main/java/rx/internal/util/BackpressureDrainManager.java index c90e9591df..3e670e0e53 100644 --- a/src/main/java/rx/internal/util/BackpressureDrainManager.java +++ b/src/main/java/rx/internal/util/BackpressureDrainManager.java @@ -72,7 +72,7 @@ public interface BackpressureQueueCallback { /** The callbacks to manage the drain. */ protected final BackpressureQueueCallback actual; /** - * Constructs a backpressure drain manager with 0 requesedCount, + * Constructs a backpressure drain manager with 0 requestedCount, * no terminal event and not emitting. * @param actual he queue callback to check for new element availability */ diff --git a/src/main/java/rx/internal/util/unsafe/ConcurrentCircularArrayQueue.java b/src/main/java/rx/internal/util/unsafe/ConcurrentCircularArrayQueue.java index 86a8db0b19..a8b9990b56 100644 --- a/src/main/java/rx/internal/util/unsafe/ConcurrentCircularArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/ConcurrentCircularArrayQueue.java @@ -29,7 +29,7 @@ abstract class ConcurrentCircularArrayQueueL0Pad extends AbstractQueue imp /** * A concurrent access enabling class used by circular array based queues this class exposes an offset computation * method along with differently memory fenced load/store methods into the underlying array. The class is pre-padded and - * the array is padded on either side to help with False sharing prvention. It is expected theat subclasses handle post + * the array is padded on either side to help with False sharing prevention. It is expected that subclasses handle post * padding. *

* Offset calculation is separate from access to enable the reuse of a give compute offset. diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index c5b3588e32..a44bf85558 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -543,7 +543,7 @@ public void onCompleted() { /** Constant indicating the setProducer method should be called. */ static final Object SET_PRODUCER = new Object(); - /** Indicates an unsubscripton happened */ + /** Indicates an unsubscription happened */ static final Object UNSUBSCRIBE = new Object(); /** diff --git a/src/main/java/rx/observables/GroupedObservable.java b/src/main/java/rx/observables/GroupedObservable.java index ad9ceb9370..0dccce036d 100644 --- a/src/main/java/rx/observables/GroupedObservable.java +++ b/src/main/java/rx/observables/GroupedObservable.java @@ -95,7 +95,7 @@ protected GroupedObservable(K key, OnSubscribe onSubscribe) { } /** - * Returns the key that identifies the group of items emited by this {@code GroupedObservable} + * Returns the key that identifies the group of items emitted by this {@code GroupedObservable} * * @return the key that the items emitted by this {@code GroupedObservable} were grouped by */ diff --git a/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java b/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java index 22bca81286..0413fe6a32 100644 --- a/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java +++ b/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java @@ -44,12 +44,12 @@ public abstract class RxJavaObservableExecutionHook { * Invoked during the construction by {@link Observable#create(OnSubscribe)} *

* This can be used to decorate or replace the onSubscribe function or just perform extra - * logging, metrics and other such things and pass-thru the function. + * logging, metrics and other such things and pass through the function. * * @param f * original {@link OnSubscribe}<{@code T}> to be executed * @return {@link OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or just - * returned as a pass-thru + * returned as a pass through */ public OnSubscribe onCreate(OnSubscribe f) { return f; @@ -59,15 +59,15 @@ public OnSubscribe onCreate(OnSubscribe f) { * Invoked before {@link Observable#subscribe(rx.Subscriber)} is about to be executed. *

* This can be used to decorate or replace the onSubscribe function or just perform extra - * logging, metrics and other such things and pass-thru the function. + * logging, metrics and other such things and pass through the function. * * @param onSubscribe * original {@link OnSubscribe}<{@code T}> to be executed * @return {@link OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or just - * returned as a pass-thru + * returned as a pass through */ public OnSubscribe onSubscribeStart(Observable observableInstance, final OnSubscribe onSubscribe) { - // pass-thru by default + // pass through by default return onSubscribe; } @@ -76,15 +76,15 @@ public OnSubscribe onSubscribeStart(Observable observableIns * {@link Subscription}. *

* This can be used to decorate or replace the {@link Subscription} instance or just perform extra logging, - * metrics and other such things and pass-thru the subscription. + * metrics and other such things and pass through the subscription. * * @param subscription * original {@link Subscription} * @return {@link Subscription} subscription that can be modified, decorated, replaced or just returned as a - * pass-thru + * pass through */ public Subscription onSubscribeReturn(Subscription subscription) { - // pass-thru by default + // pass through by default return subscription; } @@ -96,10 +96,10 @@ public Subscription onSubscribeReturn(Subscription subscription) { * * @param e * Throwable thrown by {@link Observable#subscribe(Subscriber)} - * @return Throwable that can be decorated, replaced or just returned as a pass-thru + * @return Throwable that can be decorated, replaced or just returned as a pass through */ public Throwable onSubscribeError(Throwable e) { - // pass-thru by default + // pass through by default return e; } @@ -108,12 +108,12 @@ public Throwable onSubscribeError(Throwable e) { * {@link Observable} and the return value is used as the lifted function *

* This can be used to decorate or replace the {@link Operator} instance or just perform extra - * logging, metrics and other such things and pass-thru the onSubscribe. + * logging, metrics and other such things and pass through the onSubscribe. * * @param lift * original {@link Operator}{@code } * @return {@link Operator}{@code } function that can be modified, decorated, replaced or just - * returned as a pass-thru + * returned as a pass through */ public Operator onLift(final Operator lift) { return lift; diff --git a/src/main/java/rx/plugins/RxJavaPlugins.java b/src/main/java/rx/plugins/RxJavaPlugins.java index 9678a32e15..6391a91185 100644 --- a/src/main/java/rx/plugins/RxJavaPlugins.java +++ b/src/main/java/rx/plugins/RxJavaPlugins.java @@ -36,7 +36,7 @@ * * Where the {@code .class} property contains the simple classname from above and the {@code .impl} * contains the fully qualified name of the implementation class. The {@code [index]} can be - * any short string or number of your chosing. For example, you can now define a custom + * any short string or number of your choosing. For example, you can now define a custom * {@code RxJavaErrorHandler} via two system property: *


  * rxjava.plugin.1.class=RxJavaErrorHandler
diff --git a/src/main/java/rx/plugins/RxJavaSchedulersHook.java b/src/main/java/rx/plugins/RxJavaSchedulersHook.java
index 3bf923464a..133cdc363a 100644
--- a/src/main/java/rx/plugins/RxJavaSchedulersHook.java
+++ b/src/main/java/rx/plugins/RxJavaSchedulersHook.java
@@ -71,7 +71,7 @@ public Scheduler getNewThreadScheduler() {
 
     /**
      * Invoked before the Action is handed over to the scheduler.  Can be used for wrapping/decorating/logging.
-     * The default is just a passthrough.
+     * The default is just a pass through.
      * @param action action to schedule
      * @return wrapped action to schedule
      */
diff --git a/src/main/java/rx/plugins/RxJavaSingleExecutionHook.java b/src/main/java/rx/plugins/RxJavaSingleExecutionHook.java
index 9fce6531f3..65c7ad3155 100644
--- a/src/main/java/rx/plugins/RxJavaSingleExecutionHook.java
+++ b/src/main/java/rx/plugins/RxJavaSingleExecutionHook.java
@@ -43,12 +43,12 @@ public abstract class RxJavaSingleExecutionHook {
      * Invoked during the construction by {@link Single#create(Single.OnSubscribe)}
      * 

* This can be used to decorate or replace the onSubscribe function or just perform extra - * logging, metrics and other such things and pass-thru the function. + * logging, metrics and other such things and pass through the function. * * @param f * original {@link Single.OnSubscribe}<{@code T}> to be executed * @return {@link Single.OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or just - * returned as a pass-thru + * returned as a pass through */ public Single.OnSubscribe onCreate(Single.OnSubscribe f) { return f; @@ -58,15 +58,15 @@ public Single.OnSubscribe onCreate(Single.OnSubscribe f) { * Invoked before {@link Single#subscribe(Subscriber)} is about to be executed. *

* This can be used to decorate or replace the onSubscribe function or just perform extra - * logging, metrics and other such things and pass-thru the function. + * logging, metrics and other such things and pass through the function. * * @param onSubscribe * original {@link Observable.OnSubscribe}<{@code T}> to be executed * @return {@link Observable.OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or just - * returned as a pass-thru + * returned as a pass through */ public Observable.OnSubscribe onSubscribeStart(Single singleInstance, final Observable.OnSubscribe onSubscribe) { - // pass-thru by default + // pass through by default return onSubscribe; } @@ -75,15 +75,15 @@ public Observable.OnSubscribe onSubscribeStart(Single single * {@link Subscription}. *

* This can be used to decorate or replace the {@link Subscription} instance or just perform extra logging, - * metrics and other such things and pass-thru the subscription. + * metrics and other such things and pass through the subscription. * * @param subscription * original {@link Subscription} * @return {@link Subscription} subscription that can be modified, decorated, replaced or just returned as a - * pass-thru + * pass through */ public Subscription onSubscribeReturn(Subscription subscription) { - // pass-thru by default + // pass through by default return subscription; } @@ -95,10 +95,10 @@ public Subscription onSubscribeReturn(Subscription subscription) { * * @param e * Throwable thrown by {@link Single#subscribe(Subscriber)} - * @return Throwable that can be decorated, replaced or just returned as a pass-thru + * @return Throwable that can be decorated, replaced or just returned as a pass through */ public Throwable onSubscribeError(Throwable e) { - // pass-thru by default + // pass through by default return e; } @@ -107,12 +107,12 @@ public Throwable onSubscribeError(Throwable e) { * {@link Single} and the return value is used as the lifted function *

* This can be used to decorate or replace the {@link Observable.Operator} instance or just perform extra - * logging, metrics and other such things and pass-thru the onSubscribe. + * logging, metrics and other such things and pass through the onSubscribe. * * @param lift * original {@link Observable.Operator}{@code } * @return {@link Observable.Operator}{@code } function that can be modified, decorated, replaced or just - * returned as a pass-thru + * returned as a pass through */ public Observable.Operator onLift(final Observable.Operator lift) { return lift; diff --git a/src/main/java/rx/schedulers/Schedulers.java b/src/main/java/rx/schedulers/Schedulers.java index 7dd8186616..0ec2d3a273 100644 --- a/src/main/java/rx/schedulers/Schedulers.java +++ b/src/main/java/rx/schedulers/Schedulers.java @@ -143,7 +143,7 @@ public static Scheduler from(Executor executor) { * Starts those standard Schedulers which support the SchedulerLifecycle interface. *

The operation is idempotent and threadsafe. */ - /* public testonly */ static void start() { + /* public test only */ static void start() { Schedulers s = INSTANCE; synchronized (s) { if (s.computationScheduler instanceof SchedulerLifecycle) { diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index 6b5b43c799..db8490f731 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -78,7 +78,7 @@ public static ReplaySubject create() { /** * Creates an unbounded replay subject with the specified initial buffer capacity. *

- * Use this method to avoid excessive array reallocation while the internal buffer grows to accomodate new + * Use this method to avoid excessive array reallocation while the internal buffer grows to accommodate new * items. For example, if you know that the buffer will hold 32k items, you can ask the * {@code ReplaySubject} to preallocate its internal array with a capacity to hold that many items. Once * the items start to arrive, the internal array won't need to grow, creating less garbage and no overhead @@ -525,7 +525,7 @@ public Integer replayObserverFromIndexTest(Integer idx, SubjectObserver 0) { Object o = list.get(idx - 1); if (nl.isCompleted(o) || nl.isError(o)) { diff --git a/src/main/java/rx/subjects/TestSubject.java b/src/main/java/rx/subjects/TestSubject.java index f7a0caee2a..db57feedd4 100644 --- a/src/main/java/rx/subjects/TestSubject.java +++ b/src/main/java/rx/subjects/TestSubject.java @@ -121,10 +121,10 @@ void _onError(final Throwable e) { * * @param e * the {@code Throwable} to pass to the {@code onError} method - * @param dalayTime + * @param delayTime * the number of milliseconds in the future relative to "now()" at which to call {@code onError} */ - public void onError(final Throwable e, long dalayTime) { + public void onError(final Throwable e, long delayTime) { innerScheduler.schedule(new Action0() { @Override @@ -132,7 +132,7 @@ public void call() { _onError(e); } - }, dalayTime, TimeUnit.MILLISECONDS); + }, delayTime, TimeUnit.MILLISECONDS); } /** diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index e46dfebcb5..393c8155cf 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -96,7 +96,7 @@ public void testMergeSync() { assertEquals(NUM, ts.getOnNextEvents().size()); // either one can starve the other, but neither should be capable of doing more than 5 batches (taking 4.1) // TODO is it possible to make this deterministic rather than one possibly starving the other? - // benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algoritms generally take a performance hit + // benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algorithms generally take a performance hit assertTrue(c1.get() < RxRingBuffer.SIZE * 5); assertTrue(c2.get() < RxRingBuffer.SIZE * 5); } @@ -118,7 +118,7 @@ public void testMergeAsync() { assertEquals(NUM, ts.getOnNextEvents().size()); // either one can starve the other, but neither should be capable of doing more than 5 batches (taking 4.1) // TODO is it possible to make this deterministic rather than one possibly starving the other? - // benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algoritms generally take a performance hit + // benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algorithms generally take a performance hit assertTrue(c1.get() < RxRingBuffer.SIZE * 5); assertTrue(c2.get() < RxRingBuffer.SIZE * 5); } @@ -164,7 +164,7 @@ public void testMergeAsyncThenObserveOn() { assertEquals(NUM, ts.getOnNextEvents().size()); // either one can starve the other, but neither should be capable of doing more than 5 batches (taking 4.1) // TODO is it possible to make this deterministic rather than one possibly starving the other? - // benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algoritms generally take a performance hit + // benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algorithms generally take a performance hit // akarnokd => run this in a loop over 10k times and never saw values get as high as 7*SIZE, but since observeOn delays the unsubscription non-deterministically, the test will remain unreliable assertTrue(c1.get() < RxRingBuffer.SIZE * 7); assertTrue(c2.get() < RxRingBuffer.SIZE * 7); diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 24855bf415..2952e22bfd 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -1340,7 +1340,7 @@ public void onErrorResumeNextViaSingleShouldPreventNullSingle() { } @Test - public void onErrorResumeNextViaFunctionShouldNotInterruptSuccesfulSingle() { + public void onErrorResumeNextViaFunctionShouldNotInterruptSuccessfulSingle() { TestSubscriber testSubscriber = new TestSubscriber(); Single diff --git a/src/test/java/rx/SubscriberTest.java b/src/test/java/rx/SubscriberTest.java index 4b0f8f3f23..95d489e2c6 100644 --- a/src/test/java/rx/SubscriberTest.java +++ b/src/test/java/rx/SubscriberTest.java @@ -160,7 +160,7 @@ public void request(long n) { } }); - // this will be Long.MAX_VALUE because it is decoupled and nothing requsted on the Operator subscriber + // this will be Long.MAX_VALUE because it is decoupled and nothing requested on the Operator subscriber assertEquals(Long.MAX_VALUE, r.get()); } diff --git a/src/test/java/rx/exceptions/OnNextValueTest.java b/src/test/java/rx/exceptions/OnNextValueTest.java index b620e3eed0..00e5392dab 100644 --- a/src/test/java/rx/exceptions/OnNextValueTest.java +++ b/src/test/java/rx/exceptions/OnNextValueTest.java @@ -37,7 +37,7 @@ * this.value = value; * } * ``` - * I know this is probably a helpful error message in some cases but this can be a really costly operation when an objects toString is an expensive call or contains alot of output. I don't think we should be printing this in any case but if so it should be on demand (overload of getMessage()) rather than eagerly. + * I know this is probably a helpful error message in some cases but this can be a really costly operation when an objects toString is an expensive call or contains a lot of output. I don't think we should be printing this in any case but if so it should be on demand (overload of getMessage()) rather than eagerly. *

* In my case it is causing a toString of a large context object that is normally only used for debugging purposes which makes the exception logs hard to use and they are rolling over the log files very quickly. *

diff --git a/src/test/java/rx/exceptions/TestException.java b/src/test/java/rx/exceptions/TestException.java index 8d44a6f3af..16422b7f25 100644 --- a/src/test/java/rx/exceptions/TestException.java +++ b/src/test/java/rx/exceptions/TestException.java @@ -28,7 +28,7 @@ public TestException() { } /** * Create the test exception with the provided message. - * @param message the mesage to use + * @param message the message to use */ public TestException(String message) { super(message); diff --git a/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java b/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java index 0ee4192add..a68605dd8c 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java @@ -108,7 +108,7 @@ public Observable call(Resource resource) { inOrder.verify(observer, times(1)).onCompleted(); inOrder.verifyNoMoreInteractions(); - // The resouce should be closed + // The resource should be closed verify(resource, times(1)).dispose(); } diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index a54c435432..65aa1f5307 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -421,7 +421,7 @@ public void testConcatUnsubscribe() { Subscription s1 = concat.subscribe(observer); //Block main thread to allow observable "w1" to complete and observable "w2" to call onNext once. callOnce.await(); - // Unsubcribe + // Unsubscribe s1.unsubscribe(); //Unblock the observable to continue. okToContinue.countDown(); diff --git a/src/test/java/rx/internal/operators/OperatorFlatMapTest.java b/src/test/java/rx/internal/operators/OperatorFlatMapTest.java index bb5127665c..56d733affd 100644 --- a/src/test/java/rx/internal/operators/OperatorFlatMapTest.java +++ b/src/test/java/rx/internal/operators/OperatorFlatMapTest.java @@ -324,7 +324,7 @@ public void call() { @Override public void call() { if (subscriptionCount.decrementAndGet() < 0) { - Assert.fail("Too many unsubscriptionss! " + subscriptionCount.get()); + Assert.fail("Too many unsubscriptions! " + subscriptionCount.get()); } } }); diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java index 59a971e1c1..ac01daa591 100644 --- a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java @@ -154,7 +154,7 @@ public void testFixBackpressureBoundedBufferDroppingOldest() } @Test - public void testFixBackpressueBoundedBufferDroppingLatest() + public void testFixBackpressureBoundedBufferDroppingLatest() throws InterruptedException { List events = overflowBufferWithBehaviour(100, 10, ON_OVERFLOW_DROP_LATEST); diff --git a/src/test/java/rx/internal/operators/OperatorPublishFunctionTest.java b/src/test/java/rx/internal/operators/OperatorPublishFunctionTest.java index 761dead9c5..0221c921ac 100644 --- a/src/test/java/rx/internal/operators/OperatorPublishFunctionTest.java +++ b/src/test/java/rx/internal/operators/OperatorPublishFunctionTest.java @@ -202,7 +202,7 @@ public Observable call(Observable o) { } @Test - public void oveflowMissingBackpressureException() { + public void overflowMissingBackpressureException() { TestSubscriber ts = TestSubscriber.create(0); PublishSubject ps = PublishSubject.create(); @@ -227,7 +227,7 @@ public Observable call(Observable o) { } @Test - public void oveflowMissingBackpressureExceptionDelayed() { + public void overflowMissingBackpressureExceptionDelayed() { TestSubscriber ts = TestSubscriber.create(0); PublishSubject ps = PublishSubject.create(); diff --git a/src/test/java/rx/internal/operators/OperatorRetryTest.java b/src/test/java/rx/internal/operators/OperatorRetryTest.java index dc6eb510a9..0941c2d9d4 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryTest.java @@ -598,7 +598,7 @@ public void call() { } } - /** Observer for listener on seperate thread */ + /** Observer for listener on separate thread */ static final class AsyncObserver implements Observer { protected CountDownLatch latch = new CountDownLatch(1); diff --git a/src/test/java/rx/internal/operators/OperatorTimeoutTests.java b/src/test/java/rx/internal/operators/OperatorTimeoutTests.java index f14901176a..4c2ae8c75b 100644 --- a/src/test/java/rx/internal/operators/OperatorTimeoutTests.java +++ b/src/test/java/rx/internal/operators/OperatorTimeoutTests.java @@ -109,7 +109,7 @@ public void shouldTimeoutIfSecondOnNextNotWithinTimeout() { } @Test - public void shouldCompleteIfUnderlyingComletes() { + public void shouldCompleteIfUnderlyingCompletes() { @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); Subscription subscription = withTimeout.subscribe(observer); diff --git a/src/test/java/rx/internal/operators/OperatorTimeoutWithSelectorTest.java b/src/test/java/rx/internal/operators/OperatorTimeoutWithSelectorTest.java index 4d2799c12b..51670b1b7a 100644 --- a/src/test/java/rx/internal/operators/OperatorTimeoutWithSelectorTest.java +++ b/src/test/java/rx/internal/operators/OperatorTimeoutWithSelectorTest.java @@ -426,7 +426,7 @@ public void run() { latchTimeout.set(true); } - assertFalse("CoundDownLatch timeout", latchTimeout.get()); + assertFalse("CountDownLatch timeout", latchTimeout.get()); InOrder inOrder = inOrder(o); inOrder.verify(o).onNext(1); diff --git a/src/test/java/rx/internal/operators/OperatorZipTest.java b/src/test/java/rx/internal/operators/OperatorZipTest.java index f13ba0ccb9..75f83f3e67 100644 --- a/src/test/java/rx/internal/operators/OperatorZipTest.java +++ b/src/test/java/rx/internal/operators/OperatorZipTest.java @@ -1241,7 +1241,7 @@ public Integer call(Integer i1, Integer i2) { assertEquals(expected, zip2.toList().toBlocking().single()); } @Test - public void testUnboundedDownstreamOverrequesting() { + public void testUnboundedDownstreamOverRequesting() { Observable source = Observable.range(1, 2).zipWith(Observable.range(1, 2), new Func2() { @Override public Integer call(Integer t1, Integer t2) { diff --git a/src/test/java/rx/internal/operators/SingleOnSubscribeUsingTest.java b/src/test/java/rx/internal/operators/SingleOnSubscribeUsingTest.java index 238f373115..96fd9ab05e 100644 --- a/src/test/java/rx/internal/operators/SingleOnSubscribeUsingTest.java +++ b/src/test/java/rx/internal/operators/SingleOnSubscribeUsingTest.java @@ -94,7 +94,7 @@ public Single call(Resource resource) { inOrder.verify(observer).onCompleted(); inOrder.verifyNoMoreInteractions(); - // The resouce should be closed + // The resource should be closed verify(resource).dispose(); } diff --git a/src/test/java/rx/schedulers/TrampolineSchedulerTest.java b/src/test/java/rx/schedulers/TrampolineSchedulerTest.java index a1bad56e34..aad14239a5 100644 --- a/src/test/java/rx/schedulers/TrampolineSchedulerTest.java +++ b/src/test/java/rx/schedulers/TrampolineSchedulerTest.java @@ -86,7 +86,7 @@ public void call() { @Override public void call() { workers.add(doWorkOnNewTrampoline("B", workDone)); - // we unsubscribe worker2 ... it should not affect work scheduled on a separate Trampline.Worker + // we unsubscribe worker2 ... it should not affect work scheduled on a separate Trampoline.Worker worker2.unsubscribe(); } diff --git a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java index 2f7461d2d5..8cbe051594 100644 --- a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java @@ -449,7 +449,7 @@ public void run() { Assert.fail("Size decreased! " + lastSize + " -> " + size); } if ((size > 0) && !hasAny) { - Assert.fail("hasAnyValue reports emptyness but size doesn't"); + Assert.fail("hasAnyValue reports emptiness but size doesn't"); } if (size > values.length) { Assert.fail("Got fewer values than size! " + size + " -> " + values.length); diff --git a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java index a90c5ac0f3..e0744ef1d2 100644 --- a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java @@ -446,7 +446,7 @@ public void run() { Assert.fail("Size decreased! " + lastSize + " -> " + size); } if ((size > 0) && !hasAny) { - Assert.fail("hasAnyValue reports emptyness but size doesn't"); + Assert.fail("hasAnyValue reports emptiness but size doesn't"); } if (size > values.length) { Assert.fail("Got fewer values than size! " + size + " -> " + values.length); diff --git a/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java b/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java index 18ac45f198..dacb8e11f0 100644 --- a/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java +++ b/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java @@ -335,7 +335,7 @@ public void testTryRemoveIfNotIn() { csub.remove(csub1); csub.add(csub2); - csub.remove(csub1); // try removing agian + csub.remove(csub1); // try removing again } @Test(expected = NullPointerException.class) diff --git a/src/test/java/rx/subscriptions/SerialSubscriptionTests.java b/src/test/java/rx/subscriptions/SerialSubscriptionTests.java index 8c4482ce54..8e1d37fe2d 100644 --- a/src/test/java/rx/subscriptions/SerialSubscriptionTests.java +++ b/src/test/java/rx/subscriptions/SerialSubscriptionTests.java @@ -48,7 +48,7 @@ public void unsubscribingWithoutUnderlyingDoesNothing() { } @Test - public void getSubscriptionShouldReturnset() { + public void getSubscriptionShouldReturnSet() { final Subscription underlying = mock(Subscription.class); serialSubscription.set(underlying); assertSame(underlying, serialSubscription.get()); diff --git a/src/test/java/rx/test/TestObstructionDetection.java b/src/test/java/rx/test/TestObstructionDetection.java index 859d138dd0..d24c6721ab 100644 --- a/src/test/java/rx/test/TestObstructionDetection.java +++ b/src/test/java/rx/test/TestObstructionDetection.java @@ -45,7 +45,7 @@ private TestObstructionDetection() { } /** * Checks if tasks can be immediately executed on the computation scheduler. - * @throws ObstructionExceptio if the schedulers don't respond within 1 second + * @throws ObstructionException if the schedulers don't respond within 1 second */ public static void checkObstruction() { final int ncpu = Runtime.getRuntime().availableProcessors(); From db2e23274f9585add9469fc4305ab1c16bfbee3d Mon Sep 17 00:00:00 2001 From: Prat Date: Sat, 26 Mar 2016 23:43:07 -0400 Subject: [PATCH 030/322] 1.x: Add Completable.andThen(Single) --- src/main/java/rx/Completable.java | 18 +++ src/main/java/rx/Single.java | 25 +++- ...ngleOnSubscribeDelaySubscriptionOther.java | 91 ++++++++++++++ src/test/java/rx/CompletableTest.java | 55 +++++++++ ...OnSubscribeDelaySubscriptionOtherTest.java | 112 ++++++++++++++++++ 5 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 src/main/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOther.java create mode 100644 src/test/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOtherTest.java diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index f2e752c2b2..357969565d 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -1114,6 +1114,24 @@ public final Observable andThen(Observable next) { requireNonNull(next); return next.delaySubscription(toObservable()); } + + /** + * Returns a Single which will subscribe to this Completable and once that is completed then + * will subscribe to the {@code next} Single. An error event from this Completable will be + * propagated to the downstream subscriber and will result in skipping the subscription of the + * Single. + *

+ *
Scheduler:
+ *
{@code andThen} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param next the Single to subscribe after this Completable is completed, not null + * @return Single that composes this Completable and next + */ + public final Single andThen(Single next) { + requireNonNull(next); + return next.delaySubscription(toObservable()); + } /** * Concatenates this Completable with another Completable. diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index f5e1156acc..a798073ff9 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -27,7 +27,6 @@ import rx.internal.util.UtilityFunctions; import rx.observers.SafeSubscriber; import rx.observers.SerializedSubscriber; -import rx.plugins.RxJavaObservableExecutionHook; import rx.plugins.RxJavaPlugins; import rx.plugins.RxJavaSingleExecutionHook; import rx.schedulers.Schedulers; @@ -2671,4 +2670,28 @@ public static Single using( return create(new SingleOnSubscribeUsing(resourceFactory, singleFactory, disposeAction, disposeEagerly)); } + /** + * Returns a Single that delays the subscription to this Single + * until the Observable completes. In case the {@code onError} of the supplied observer throws, + * the exception will be propagated to the downstream subscriber + * and will result in skipping the subscription of this Single. + * + *

+ *

+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param other the Observable that should trigger the subscription + * to this Single. + * @return a Single that delays the subscription to this Single + * until the Observable emits an element or completes normally. + */ + @Experimental + public final Single delaySubscription(Observable other) { + if (other == null) { + throw new NullPointerException(); + } + return create(new SingleOnSubscribeDelaySubscriptionOther(this, other)); + } } diff --git a/src/main/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOther.java b/src/main/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOther.java new file mode 100644 index 0000000000..efd9cf517b --- /dev/null +++ b/src/main/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOther.java @@ -0,0 +1,91 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package rx.internal.operators; + +import rx.Observable; +import rx.Single; +import rx.SingleSubscriber; +import rx.Subscriber; +import rx.plugins.RxJavaPlugins; +import rx.subscriptions.SerialSubscription; + +/** + * Delays the subscription to the Single until the Observable + * fires an event or completes. + * + * @param the Single value type + */ +public final class SingleOnSubscribeDelaySubscriptionOther implements Single.OnSubscribe { + final Single main; + final Observable other; + + public SingleOnSubscribeDelaySubscriptionOther(Single main, Observable other) { + this.main = main; + this.other = other; + } + + @Override + public void call(final SingleSubscriber subscriber) { + final SingleSubscriber child = new SingleSubscriber() { + @Override + public void onSuccess(T value) { + subscriber.onSuccess(value); + } + + @Override + public void onError(Throwable error) { + subscriber.onError(error); + } + }; + + final SerialSubscription serial = new SerialSubscription(); + subscriber.add(serial); + + Subscriber otherSubscriber = new Subscriber() { + boolean done; + @Override + public void onNext(Object t) { + onCompleted(); + } + + @Override + public void onError(Throwable e) { + if (done) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + return; + } + done = true; + child.onError(e); + } + + @Override + public void onCompleted() { + if (done) { + return; + } + done = true; + serial.set(child); + + main.subscribe(child); + } + }; + + serial.set(otherSubscriber); + + other.subscribe(otherSubscriber); + } +} diff --git a/src/test/java/rx/CompletableTest.java b/src/test/java/rx/CompletableTest.java index 894c72109f..2493da2356 100644 --- a/src/test/java/rx/CompletableTest.java +++ b/src/test/java/rx/CompletableTest.java @@ -418,6 +418,61 @@ public void andThenSubscribeOn() { ts.assertCompleted(); ts.assertNoErrors(); } + + @Test + public void andThenSingle() { + TestSubscriber ts = new TestSubscriber(0); + Completable.complete().andThen(Single.just("foo")).subscribe(ts); + ts.requestMore(1); + ts.assertValue("foo"); + ts.assertCompleted(); + ts.assertNoErrors(); + ts.assertUnsubscribed(); + } + + @Test + public void andThenSingleNever() { + TestSubscriber ts = new TestSubscriber(0); + Completable.never().andThen(Single.just("foo")).subscribe(ts); + ts.requestMore(1); + ts.assertNoValues(); + ts.assertNoTerminalEvent(); + } + + @Test + public void andThenSingleError() { + TestSubscriber ts = new TestSubscriber(0); + final AtomicBoolean hasRun = new AtomicBoolean(false); + final Exception e = new Exception(); + Completable.error(e) + .andThen(Single.create(new Single.OnSubscribe() { + @Override + public void call(SingleSubscriber s) { + hasRun.set(true); + s.onSuccess("foo"); + } + })) + .subscribe(ts); + ts.assertNoValues(); + ts.assertError(e); + ts.assertUnsubscribed(); + Assert.assertFalse("Should not have subscribed to single when completable errors", hasRun.get()); + } + + @Test + public void andThenSingleSubscribeOn() { + TestSubscriber ts = new TestSubscriber(0); + TestScheduler scheduler = new TestScheduler(); + Completable.complete().andThen(Single.just("foo").delay(1, TimeUnit.SECONDS, scheduler)).subscribe(ts); + ts.requestMore(1); + ts.assertNoValues(); + ts.assertNoTerminalEvent(); + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + ts.assertValue("foo"); + ts.assertCompleted(); + ts.assertNoErrors(); + ts.assertUnsubscribed(); + } @Test(expected = NullPointerException.class) public void createNull() { diff --git a/src/test/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOtherTest.java b/src/test/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOtherTest.java new file mode 100644 index 0000000000..c17f3b6e1f --- /dev/null +++ b/src/test/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOtherTest.java @@ -0,0 +1,112 @@ +package rx.internal.operators; + +import org.junit.Assert; +import org.junit.Test; +import rx.Single; +import rx.exceptions.TestException; +import rx.functions.Action0; +import rx.observers.TestSubscriber; +import rx.subjects.PublishSubject; + +import java.util.concurrent.atomic.AtomicInteger; + +public class SingleOnSubscribeDelaySubscriptionOtherTest { + @Test + public void noPrematureSubscription() { + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Single.just(1) + .doOnSubscribe(new Action0() { + @Override + public void call() { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(ts); + + ts.assertNotCompleted(); + ts.assertNoErrors(); + ts.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onNext(1); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void noPrematureSubscriptionToError() { + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Single.error(new TestException()) + .doOnSubscribe(new Action0() { + @Override + public void call() { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(ts); + + ts.assertNotCompleted(); + ts.assertNoErrors(); + ts.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onNext(1); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } + + @Test + public void noSubscriptionIfOtherErrors() { + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Single.error(new TestException()) + .doOnSubscribe(new Action0() { + @Override + public void call() { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(ts); + + ts.assertNotCompleted(); + ts.assertNoErrors(); + ts.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onError(new TestException()); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } +} From 47eb3061b7da7fab6e1c87b242fa604a6c0df0e7 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 29 Mar 2016 17:33:24 +0200 Subject: [PATCH 031/322] 1.x: fix merge/flatMap crashing on an inner scalar of null --- .../rx/internal/operators/OperatorMerge.java | 2 +- .../internal/operators/OperatorMergeTest.java | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index a9c7b86b09..2aede6d9d7 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -475,7 +475,7 @@ protected void queueScalar(T value) { } this.queue = q; } - if (!q.offer(value)) { + if (!q.offer(nl.next(value))) { unsubscribe(); onError(OnErrorThrowable.addValueAsLastCause(new MissingBackpressureException(), value)); return; diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index 2c40ac53d3..b93f32b580 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -28,10 +28,10 @@ import org.mockito.*; import rx.*; -import rx.Observable.OnSubscribe; -import rx.Scheduler.Worker; import rx.Observable; +import rx.Observable.OnSubscribe; import rx.Observer; +import rx.Scheduler.Worker; import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; @@ -1353,4 +1353,21 @@ public void zeroMaxConcurrent() { assertEquals("maxConcurrent > 0 required but it was 0", e.getMessage()); } } + + @Test + public void mergeJustNull() { + TestSubscriber ts = new TestSubscriber(0); + + Observable.range(1, 2).flatMap(new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.just(null); + } + }).subscribe(ts); + + ts.requestMore(2); + ts.assertValues(null, null); + ts.assertNoErrors(); + ts.assertCompleted(); + } } From f33873e79797895d27b20d64e8112f2fa38ee9f0 Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Thu, 31 Mar 2016 12:51:24 -0700 Subject: [PATCH 032/322] Upgrading SyncOnSubscribe from Experimental to Beta --- src/main/java/rx/Observable.java | 2 +- src/main/java/rx/observables/SyncOnSubscribe.java | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 74fcf88fe9..b634c92215 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -126,7 +126,7 @@ public static Observable create(OnSubscribe f) { * @see ReactiveX operators documentation: Create * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ - @Experimental + @Beta public static Observable create(SyncOnSubscribe syncOnSubscribe) { return new Observable(hook.onCreate(syncOnSubscribe)); } diff --git a/src/main/java/rx/observables/SyncOnSubscribe.java b/src/main/java/rx/observables/SyncOnSubscribe.java index 910a5acddb..c2ff69e6e8 100644 --- a/src/main/java/rx/observables/SyncOnSubscribe.java +++ b/src/main/java/rx/observables/SyncOnSubscribe.java @@ -23,6 +23,7 @@ import rx.Producer; import rx.Subscriber; import rx.Subscription; +import rx.annotations.Beta; import rx.annotations.Experimental; import rx.exceptions.Exceptions; import rx.functions.Action0; @@ -46,7 +47,7 @@ * @param * the type of {@code Subscribers} that will be compatible with {@code this}. */ -@Experimental +@Beta public abstract class SyncOnSubscribe implements OnSubscribe { /* (non-Javadoc) @@ -126,7 +127,7 @@ protected void onUnsubscribe(S state) { * next(S, Subscriber)}) * @return a SyncOnSubscribe that emits data in a protocol compatible with back-pressure. */ - @Experimental + @Beta public static SyncOnSubscribe createSingleState(Func0 generator, final Action2> next) { Func2, S> nextFunc = new Func2, S>() { @@ -155,7 +156,7 @@ public S call(S state, Observer subscriber) { * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ - @Experimental + @Beta public static SyncOnSubscribe createSingleState(Func0 generator, final Action2> next, final Action1 onUnsubscribe) { @@ -183,7 +184,7 @@ public S call(S state, Observer subscriber) { * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ - @Experimental + @Beta public static SyncOnSubscribe createStateful(Func0 generator, Func2, ? extends S> next, Action1 onUnsubscribe) { @@ -202,7 +203,7 @@ public static SyncOnSubscribe createStateful(Func0 gen * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ - @Experimental + @Beta public static SyncOnSubscribe createStateful(Func0 generator, Func2, ? extends S> next) { return new SyncOnSubscribeImpl(generator, next); @@ -221,7 +222,7 @@ public static SyncOnSubscribe createStateful(Func0 gen * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ - @Experimental + @Beta public static SyncOnSubscribe createStateless(final Action1> next) { Func2, Void> nextFunc = new Func2, Void>() { @Override @@ -248,7 +249,7 @@ public Void call(Void state, Observer subscriber) { * @return a SyncOnSubscribe that emits data downstream in a protocol compatible with * back-pressure. */ - @Experimental + @Beta public static SyncOnSubscribe createStateless(final Action1> next, final Action0 onUnsubscribe) { Func2, Void> nextFunc = new Func2, Void>() { From d9ac3b8a577d4130cd9a47992ca8d7421700549c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Sun, 3 Apr 2016 03:56:26 +0200 Subject: [PATCH 033/322] 1.x: Fix ObserveOnTest.testQueueFullEmitsErrorWithVaryingBufferSize --- .../operators/OperatorObserveOnTest.java | 74 +++++-------------- 1 file changed, 19 insertions(+), 55 deletions(-) diff --git a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java index 8ebc69eed7..323f74b786 100644 --- a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java @@ -582,64 +582,28 @@ public void onNext(Integer t) { @Test public void testQueueFullEmitsErrorWithVaryingBufferSize() { - final CountDownLatch latch = new CountDownLatch(1); - // randomize buffer size, note that underlying implementations may be tuning the real size to a power of 2 - // which can lead to unexpected results when adding excess capacity (e.g.: see ConcurrentCircularArrayQueue) for (int i = 1; i <= 1024; i = i * 2) { final int capacity = i; - Observable observable = Observable.create(new OnSubscribe() { - - @Override - public void call(Subscriber o) { - for (int i = 0; i < capacity + 10; i++) { - o.onNext(i); - } - latch.countDown(); - o.onCompleted(); - } - - }); - - TestSubscriber testSubscriber = new TestSubscriber(new Observer() { - - @Override - public void onCompleted() { - - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onNext(Integer t) { - try { - // force it to be slow wait until we have queued everything - latch.await(500, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - }); - System.out.println("Using capacity " + capacity); // for post-failure debugging - observable.observeOn(Schedulers.newThread(), capacity).subscribe(testSubscriber); - - testSubscriber.awaitTerminalEvent(); - List errors = testSubscriber.getOnErrorEvents(); - assertEquals(1, errors.size()); - System.out.println("Errors: " + errors); - Throwable t = errors.get(0); - if (t instanceof MissingBackpressureException) { - // success, we expect this - } else { - if (t.getCause() instanceof MissingBackpressureException) { - // this is also okay - } else { - fail("Expecting MissingBackpressureException"); - } + System.out.println(">> testQueueFullEmitsErrorWithVaryingBufferSize @ " + i); + + PublishSubject ps = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(0); + + TestScheduler test = Schedulers.test(); + + ps.observeOn(test, capacity).subscribe(ts); + + for (int j = 0; j < capacity + 10; j++) { + ps.onNext(j); } + ps.onCompleted(); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertNoValues(); + ts.assertError(MissingBackpressureException.class); + ts.assertNotCompleted(); } } From e971c4aa2ec3ae2c16e771b712f66cc23c95fdda Mon Sep 17 00:00:00 2001 From: Prat Date: Mon, 4 Apr 2016 03:01:33 -0400 Subject: [PATCH 034/322] 1.x: Fix TestSubscriber.create doc --- src/main/java/rx/observers/TestSubscriber.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 17655ab91f..887bd2998b 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -145,12 +145,12 @@ public static TestSubscriber create(long initialRequest) { public static TestSubscriber create(Observer delegate, long initialRequest) { return new TestSubscriber(delegate, initialRequest); } - + /** - * Factory method to construct a TestSubscriber which delegates events to the given Observer and + * Factory method to construct a TestSubscriber which delegates events to the given Subscriber and * an issues an initial request of Long.MAX_VALUE. * @param the value type - * @param delegate the observer to delegate events to + * @param delegate the subscriber to delegate events to * @return the created TestSubscriber instance * @throws NullPointerException if delegate is null * @since 1.1.0 @@ -160,10 +160,10 @@ public static TestSubscriber create(Subscriber delegate) { } /** - * Factory method to construct a TestSubscriber which delegates events to the given Subscriber and + * Factory method to construct a TestSubscriber which delegates events to the given Observer and * an issues an initial request of Long.MAX_VALUE. * @param the value type - * @param delegate the subscriber to delegate events to + * @param delegate the observer to delegate events to * @return the created TestSubscriber instance * @throws NullPointerException if delegate is null * @since 1.1.0 From 3e2dd9e60b4fa271a80156a9953e3c9ff3853798 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Mon, 4 Apr 2016 17:43:04 +0300 Subject: [PATCH 035/322] 1.x: Add system property for disabling usage of Unsafe API --- src/main/java/rx/internal/util/unsafe/UnsafeAccess.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java b/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java index a13989f4f1..076112982f 100644 --- a/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java +++ b/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java @@ -22,6 +22,9 @@ /** * All use of this class MUST first check that UnsafeAccess.isUnsafeAvailable() == true * otherwise NPEs will happen in environments without "suc.misc.Unsafe" such as Android. + *

+ * Note that you can force RxJava to not use Unsafe API by setting any value to System Property + * {@code rx.unsafe-disable}. */ public final class UnsafeAccess { private UnsafeAccess() { @@ -29,6 +32,9 @@ private UnsafeAccess() { } public static final Unsafe UNSAFE; + + private static final boolean DISABLED_BY_USER = System.getProperty("rx.unsafe-disable") != null; + static { Unsafe u = null; try { @@ -48,7 +54,7 @@ private UnsafeAccess() { } public static boolean isUnsafeAvailable() { - return UNSAFE != null; + return UNSAFE != null && !DISABLED_BY_USER; } /* From e26096d27c218242a8331aef93ace7a0ce989093 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Mon, 4 Apr 2016 19:50:32 +0200 Subject: [PATCH 036/322] 1.x: AsyncSubject now supports backpressure --- src/main/java/rx/subjects/AsyncSubject.java | 12 ++++-- .../subjects/SubjectSubscriptionManager.java | 4 +- .../java/rx/subjects/AsyncSubjectTest.java | 43 +++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index 57539fa8eb..dac007f03f 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -22,6 +22,7 @@ import rx.exceptions.Exceptions; import rx.functions.Action1; import rx.internal.operators.NotificationLite; +import rx.internal.producers.SingleProducer; import rx.subjects.SubjectSubscriptionManager.SubjectObserver; /** @@ -68,9 +69,13 @@ public static AsyncSubject create() { public void call(SubjectObserver o) { Object v = state.getLatest(); NotificationLite nl = state.nl; - o.accept(v, nl); - if (v == null || (!nl.isCompleted(v) && !nl.isError(v))) { + if (v == null || nl.isCompleted(v)) { o.onCompleted(); + } else + if (nl.isError(v)) { + o.onError(nl.getError(v)); + } else { + o.actual.setProducer(new SingleProducer(o.actual, nl.getValue(v))); } } }; @@ -97,8 +102,7 @@ public void onCompleted() { if (last == nl.completed()) { bo.onCompleted(); } else { - bo.onNext(nl.getValue(last)); - bo.onCompleted(); + bo.actual.setProducer(new SingleProducer(bo.actual, nl.getValue(last))); } } } diff --git a/src/main/java/rx/subjects/SubjectSubscriptionManager.java b/src/main/java/rx/subjects/SubjectSubscriptionManager.java index 9a0c90ece7..8ac2d59e98 100644 --- a/src/main/java/rx/subjects/SubjectSubscriptionManager.java +++ b/src/main/java/rx/subjects/SubjectSubscriptionManager.java @@ -203,7 +203,7 @@ public State remove(SubjectObserver o) { */ protected static final class SubjectObserver implements Observer { /** The actual Observer. */ - final Observer actual; + final Subscriber actual; /** Was the emitFirst run? Guarded by this. */ boolean first = true; /** Guarded by this. */ @@ -215,7 +215,7 @@ protected static final class SubjectObserver implements Observer { protected volatile boolean caughtUp; /** Indicate where the observer is at replaying. */ private volatile Object index; - public SubjectObserver(Observer actual) { + public SubjectObserver(Subscriber actual) { this.actual = actual; } @Override diff --git a/src/test/java/rx/subjects/AsyncSubjectTest.java b/src/test/java/rx/subjects/AsyncSubjectTest.java index 968e71f571..82f6843f90 100644 --- a/src/test/java/rx/subjects/AsyncSubjectTest.java +++ b/src/test/java/rx/subjects/AsyncSubjectTest.java @@ -430,4 +430,47 @@ public void testAsyncSubjectValueError() { assertNull(async.getValue()); assertFalse(async.hasValue()); } + + @Test + public void backpressureOnline() { + TestSubscriber ts = TestSubscriber.create(0); + + AsyncSubject subject = AsyncSubject.create(); + + subject.subscribe(ts); + + subject.onNext(1); + subject.onCompleted(); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void backpressureOffline() { + TestSubscriber ts = TestSubscriber.create(0); + + AsyncSubject subject = AsyncSubject.create(); + subject.onNext(1); + subject.onCompleted(); + + subject.subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } } From 4dad04a9f5d8404f28345dc9abd2b971e93e6100 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Mon, 4 Apr 2016 20:11:37 +0200 Subject: [PATCH 037/322] 1.x: DoAfterTerminate handle if action throws --- .../operators/OperatorDoAfterTerminate.java | 13 ++++- .../OperatorDoAfterTerminateTest.java | 47 +++++++++++++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorDoAfterTerminate.java b/src/main/java/rx/internal/operators/OperatorDoAfterTerminate.java index a56d28795c..64afca478a 100644 --- a/src/main/java/rx/internal/operators/OperatorDoAfterTerminate.java +++ b/src/main/java/rx/internal/operators/OperatorDoAfterTerminate.java @@ -17,7 +17,9 @@ import rx.Observable.Operator; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.functions.Action0; +import rx.plugins.RxJavaPlugins; /** * Registers an action to be called after an Observable invokes {@code onComplete} or {@code onError}. @@ -53,7 +55,7 @@ public void onError(Throwable e) { try { child.onError(e); } finally { - action.call(); + callAction(); } } @@ -62,7 +64,16 @@ public void onCompleted() { try { child.onCompleted(); } finally { + callAction(); + } + } + + void callAction() { + try { action.call(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.getInstance().getErrorHandler().handleError(ex); } } }; diff --git a/src/test/java/rx/internal/operators/OperatorDoAfterTerminateTest.java b/src/test/java/rx/internal/operators/OperatorDoAfterTerminateTest.java index 6295386ae1..397451161d 100644 --- a/src/test/java/rx/internal/operators/OperatorDoAfterTerminateTest.java +++ b/src/test/java/rx/internal/operators/OperatorDoAfterTerminateTest.java @@ -15,18 +15,14 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; -import rx.Observable; -import rx.Observer; +import rx.*; import rx.functions.Action0; +import rx.observers.TestSubscriber; public class OperatorDoAfterTerminateTest { @@ -65,4 +61,37 @@ public void nullActionShouldBeCheckedInConstructor() { assertEquals("Action can not be null", expected.getMessage()); } } + + @Test + public void nullFinallyActionShouldBeCheckedASAP() { + try { + Observable + .just("value") + .doAfterTerminate(null); + + fail(); + } catch (NullPointerException expected) { + + } + } + + @Test + public void ifFinallyActionThrowsExceptionShouldNotBeSwallowedAndActionShouldBeCalledOnce() { + Action0 finallyAction = mock(Action0.class); + doThrow(new IllegalStateException()).when(finallyAction).call(); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Observable + .just("value") + .doAfterTerminate(finallyAction) + .subscribe(testSubscriber); + + testSubscriber.assertValue("value"); + + verify(finallyAction).call(); + // Actual result: + // Not only IllegalStateException was swallowed + // But finallyAction was called twice! + } } From 3c86972d6630653a81c9764c84856ed6786513bf Mon Sep 17 00:00:00 2001 From: David Karnok Date: Mon, 4 Apr 2016 20:13:48 +0200 Subject: [PATCH 038/322] 1.x: javac 9 compatibility fixes --- src/main/java/rx/Observable.java | 35 ++++++++++--------- src/main/java/rx/Single.java | 29 +++++++-------- .../operators/BlockingOperatorLatest.java | 2 +- .../operators/BlockingOperatorMostRecent.java | 2 +- .../operators/BlockingOperatorNext.java | 2 +- .../operators/BlockingOperatorToFuture.java | 2 +- .../operators/BlockingOperatorToIterator.java | 2 +- .../internal/operators/CachedObservable.java | 2 +- .../CompletableOnSubscribeConcat.java | 4 +-- .../CompletableOnSubscribeMerge.java | 4 +-- .../operators/OnSubscribeCombineLatest.java | 2 +- .../internal/operators/OperatorMapPair.java | 2 +- .../internal/operators/OperatorMulticast.java | 2 +- .../rx/internal/operators/OperatorReplay.java | 4 +-- ...ngleOnSubscribeDelaySubscriptionOther.java | 2 +- .../java/rx/observables/AsyncOnSubscribe.java | 7 ++-- .../rx/observables/BlockingObservable.java | 18 +++++----- src/main/java/rx/singles/BlockingSingle.java | 2 +- 18 files changed, 63 insertions(+), 60 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index b634c92215..439da019ed 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -609,7 +609,7 @@ public static Observable amb(Observable o1, Observable Observable combineLatest(Observable o1, Observable o2, Func2 combineFunction) { - return combineLatest(Arrays.asList(o1, o2), Functions.fromFunc(combineFunction)); + return (Observable)combineLatest(Arrays.asList(o1, o2), Functions.fromFunc(combineFunction)); } /** @@ -637,7 +637,7 @@ public static Observable combineLatest(Observable o */ @SuppressWarnings("unchecked") public static Observable combineLatest(Observable o1, Observable o2, Observable o3, Func3 combineFunction) { - return combineLatest(Arrays.asList(o1, o2, o3), Functions.fromFunc(combineFunction)); + return (Observable)combineLatest(Arrays.asList(o1, o2, o3), Functions.fromFunc(combineFunction)); } /** @@ -668,7 +668,7 @@ public static Observable combineLatest(Observable Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Func4 combineFunction) { - return combineLatest(Arrays.asList(o1, o2, o3, o4), Functions.fromFunc(combineFunction)); + return (Observable)combineLatest(Arrays.asList(o1, o2, o3, o4), Functions.fromFunc(combineFunction)); } /** @@ -701,7 +701,7 @@ public static Observable combineLatest(Observable Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Func5 combineFunction) { - return combineLatest(Arrays.asList(o1, o2, o3, o4, o5), Functions.fromFunc(combineFunction)); + return (Observable)combineLatest(Arrays.asList(o1, o2, o3, o4, o5), Functions.fromFunc(combineFunction)); } /** @@ -736,7 +736,7 @@ public static Observable combineLatest(Observable Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Func6 combineFunction) { - return combineLatest(Arrays.asList(o1, o2, o3, o4, o5, o6), Functions.fromFunc(combineFunction)); + return (Observable)combineLatest(Arrays.asList(o1, o2, o3, o4, o5, o6), Functions.fromFunc(combineFunction)); } /** @@ -773,7 +773,7 @@ public static Observable combineLatest(Observable @SuppressWarnings("unchecked") public static Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Func7 combineFunction) { - return combineLatest(Arrays.asList(o1, o2, o3, o4, o5, o6, o7), Functions.fromFunc(combineFunction)); + return (Observable)combineLatest(Arrays.asList(o1, o2, o3, o4, o5, o6, o7), Functions.fromFunc(combineFunction)); } /** @@ -812,7 +812,7 @@ public static Observable combineLatest(Observ @SuppressWarnings("unchecked") public static Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Func8 combineFunction) { - return combineLatest(Arrays.asList(o1, o2, o3, o4, o5, o6, o7, o8), Functions.fromFunc(combineFunction)); + return (Observable)combineLatest(Arrays.asList(o1, o2, o3, o4, o5, o6, o7, o8), Functions.fromFunc(combineFunction)); } /** @@ -854,7 +854,7 @@ public static Observable combineLatest(Ob public static Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9, Func9 combineFunction) { - return combineLatest(Arrays.asList(o1, o2, o3, o4, o5, o6, o7, o8, o9), Functions.fromFunc(combineFunction)); + return (Observable)combineLatest(Arrays.asList(o1, o2, o3, o4, o5, o6, o7, o8, o9), Functions.fromFunc(combineFunction)); } /** * Combines a list of source Observables by emitting an item that aggregates the latest values of each of @@ -1330,7 +1330,7 @@ public static Observable error(Throwable exception) { * @see ReactiveX operators documentation: From */ public static Observable from(Future future) { - return create(OnSubscribeToObservableFuture.toObservableFuture(future)); + return (Observable)create(OnSubscribeToObservableFuture.toObservableFuture(future)); } /** @@ -1361,7 +1361,7 @@ public static Observable from(Future future) { * @see ReactiveX operators documentation: From */ public static Observable from(Future future, long timeout, TimeUnit unit) { - return create(OnSubscribeToObservableFuture.toObservableFuture(future, timeout, unit)); + return (Observable)create(OnSubscribeToObservableFuture.toObservableFuture(future, timeout, unit)); } /** @@ -1390,7 +1390,8 @@ public static Observable from(Future future, long timeout, T */ public static Observable from(Future future, Scheduler scheduler) { // TODO in a future revision the Scheduler will become important because we'll start polling instead of blocking on the Future - return create(OnSubscribeToObservableFuture.toObservableFuture(future)).subscribeOn(scheduler); + Observable o = (Observable)create(OnSubscribeToObservableFuture.toObservableFuture(future)); + return o.subscribeOn(scheduler); } /** @@ -5834,7 +5835,7 @@ public final Observable flatMapIterable(Func1 Observable flatMapIterable(Func1> collectionSelector, Func2 resultSelector) { - return flatMap(OperatorMapPair.convertSelector(collectionSelector), resultSelector); + return (Observable)flatMap(OperatorMapPair.convertSelector(collectionSelector), resultSelector); } /** @@ -5870,7 +5871,7 @@ public final Observable flatMapIterable(Func1 Observable flatMapIterable(Func1> collectionSelector, Func2 resultSelector, int maxConcurrent) { - return flatMap(OperatorMapPair.convertSelector(collectionSelector), resultSelector, maxConcurrent); + return (Observable)flatMap(OperatorMapPair.convertSelector(collectionSelector), resultSelector, maxConcurrent); } /** @@ -6655,7 +6656,7 @@ public final Observable onErrorResumeNext(final Func1ReactiveX operators documentation: Catch */ public final Observable onErrorResumeNext(final Observable resumeSequence) { - return lift(OperatorOnErrorResumeNextViaFunction.withOther(resumeSequence)); + return lift((Operator)OperatorOnErrorResumeNextViaFunction.withOther(resumeSequence)); } /** @@ -6685,7 +6686,7 @@ public final Observable onErrorResumeNext(final Observable resum * @see ReactiveX operators documentation: Catch */ public final Observable onErrorReturn(Func1 resumeFunction) { - return lift(OperatorOnErrorResumeNextViaFunction.withSingle(resumeFunction)); + return lift((Operator)OperatorOnErrorResumeNextViaFunction.withSingle(resumeFunction)); } /** @@ -6721,7 +6722,7 @@ public final Observable onErrorReturn(Func1 resumeFun * @see ReactiveX operators documentation: Catch */ public final Observable onExceptionResumeNext(final Observable resumeSequence) { - return lift(OperatorOnErrorResumeNextViaFunction.withException(resumeSequence)); + return lift((Operator)OperatorOnErrorResumeNextViaFunction.withException(resumeSequence)); } /** @@ -10564,7 +10565,7 @@ public final Observable zipWith(Iterable other, Func2ReactiveX operators documentation: Zip */ public final Observable zipWith(Observable other, Func2 zipFunction) { - return zip(this, other, zipFunction); + return (Observable)zip(this, other, zipFunction); } /** diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 57dadd12d8..c1565313ab 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -524,7 +524,7 @@ public void call(SingleSubscriber te) { * @see ReactiveX operators documentation: From */ public static Single from(Future future) { - return new Single(OnSubscribeToObservableFuture.toObservableFuture(future)); + return new Single((Observable.OnSubscribe)OnSubscribeToObservableFuture.toObservableFuture(future)); } /** @@ -555,7 +555,7 @@ public static Single from(Future future) { * @see ReactiveX operators documentation: From */ public static Single from(Future future, long timeout, TimeUnit unit) { - return new Single(OnSubscribeToObservableFuture.toObservableFuture(future, timeout, unit)); + return new Single((Observable.OnSubscribe)OnSubscribeToObservableFuture.toObservableFuture(future, timeout, unit)); } /** @@ -583,7 +583,7 @@ public static Single from(Future future, long timeout, TimeU * @see ReactiveX operators documentation: From */ public static Single from(Future future, Scheduler scheduler) { - return new Single(OnSubscribeToObservableFuture.toObservableFuture(future)).subscribeOn(scheduler); + return new Single((Observable.OnSubscribe)OnSubscribeToObservableFuture.toObservableFuture(future)).subscribeOn(scheduler); } /** @@ -949,7 +949,7 @@ public static Observable merge(Single t1, SingleReactiveX operators documentation: Zip */ public static Single zip(Single s1, Single s2, final Func2 zipFunction) { - return SingleOperatorZip.zip(new Single[] {s1, s2}, new FuncN() { + return SingleOperatorZip.zip(new Single[] {s1, s2}, new FuncN() { @Override public R call(Object... args) { return zipFunction.call((T1) args[0], (T2) args[1]); @@ -980,7 +980,7 @@ public R call(Object... args) { * @see ReactiveX operators documentation: Zip */ public static Single zip(Single s1, Single s2, Single s3, final Func3 zipFunction) { - return SingleOperatorZip.zip(new Single[] {s1, s2, s3}, new FuncN() { + return SingleOperatorZip.zip(new Single[] {s1, s2, s3}, new FuncN() { @Override public R call(Object... args) { return zipFunction.call((T1) args[0], (T2) args[1], (T3) args[2]); @@ -1013,7 +1013,7 @@ public R call(Object... args) { * @see ReactiveX operators documentation: Zip */ public static Single zip(Single s1, Single s2, Single s3, Single s4, final Func4 zipFunction) { - return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4}, new FuncN() { + return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4}, new FuncN() { @Override public R call(Object... args) { return zipFunction.call((T1) args[0], (T2) args[1], (T3) args[2], (T4) args[3]); @@ -1048,7 +1048,7 @@ public R call(Object... args) { * @see ReactiveX operators documentation: Zip */ public static Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, final Func5 zipFunction) { - return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5}, new FuncN() { + return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5}, new FuncN() { @Override public R call(Object... args) { return zipFunction.call((T1) args[0], (T2) args[1], (T3) args[2], (T4) args[3], (T5) args[4]); @@ -1086,7 +1086,7 @@ public R call(Object... args) { */ public static Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, Single s6, final Func6 zipFunction) { - return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5, s6}, new FuncN() { + return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5, s6}, new FuncN() { @Override public R call(Object... args) { return zipFunction.call((T1) args[0], (T2) args[1], (T3) args[2], (T4) args[3], (T5) args[4], (T6) args[5]); @@ -1126,7 +1126,7 @@ public R call(Object... args) { */ public static Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, Single s6, Single s7, final Func7 zipFunction) { - return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5, s6, s7}, new FuncN() { + return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5, s6, s7}, new FuncN() { @Override public R call(Object... args) { return zipFunction.call((T1) args[0], (T2) args[1], (T3) args[2], (T4) args[3], (T5) args[4], (T6) args[5], (T7) args[6]); @@ -1168,7 +1168,7 @@ public R call(Object... args) { */ public static Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, Single s6, Single s7, Single s8, final Func8 zipFunction) { - return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5, s6, s7, s8}, new FuncN() { + return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5, s6, s7, s8}, new FuncN() { @Override public R call(Object... args) { return zipFunction.call((T1) args[0], (T2) args[1], (T3) args[2], (T4) args[3], (T5) args[4], (T6) args[5], (T7) args[6], (T8) args[7]); @@ -1212,7 +1212,7 @@ public R call(Object... args) { */ public static Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, Single s6, Single s7, Single s8, Single s9, final Func9 zipFunction) { - return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5, s6, s7, s8, s9}, new FuncN() { + return SingleOperatorZip.zip(new Single[] {s1, s2, s3, s4, s5, s6, s7, s8, s9}, new FuncN() { @Override public R call(Object... args) { return zipFunction.call((T1) args[0], (T2) args[1], (T3) args[2], (T4) args[3], (T5) args[4], (T6) args[5], (T7) args[6], (T8) args[7], (T9) args[8]); @@ -1242,7 +1242,8 @@ public R call(Object... args) { * @see ReactiveX operators documentation: Zip */ public static Single zip(Iterable> singles, FuncN zipFunction) { - return SingleOperatorZip.zip(iterableToArray(singles), zipFunction); + Single[] iterableToArray = iterableToArray(singles); + return SingleOperatorZip.zip(iterableToArray, zipFunction); } /** @@ -1401,7 +1402,7 @@ public final Single observeOn(Scheduler scheduler) { * @see ReactiveX operators documentation: Catch */ public final Single onErrorReturn(Func1 resumeFunction) { - return lift(OperatorOnErrorResumeNextViaFunction.withSingle(resumeFunction)); + return lift((Operator)OperatorOnErrorResumeNextViaFunction.withSingle(resumeFunction)); } /** @@ -2234,7 +2235,7 @@ public final BlockingSingle toBlocking() { * @see ReactiveX operators documentation: Zip */ public final Single zipWith(Single other, Func2 zipFunction) { - return zip(this, other, zipFunction); + return (Single)zip(this, other, zipFunction); } /** diff --git a/src/main/java/rx/internal/operators/BlockingOperatorLatest.java b/src/main/java/rx/internal/operators/BlockingOperatorLatest.java index 5b2b798995..b53ab211a7 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorLatest.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorLatest.java @@ -49,7 +49,7 @@ public static Iterable latest(final Observable source) { @Override public Iterator iterator() { LatestObserverIterator lio = new LatestObserverIterator(); - source.materialize().subscribe(lio); + ((Observable)source).materialize().subscribe(lio); return lio; } }; diff --git a/src/main/java/rx/internal/operators/BlockingOperatorMostRecent.java b/src/main/java/rx/internal/operators/BlockingOperatorMostRecent.java index 9b6cf64bf2..4cd424be29 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorMostRecent.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorMostRecent.java @@ -53,7 +53,7 @@ public Iterator iterator() { * Subscribe instead of unsafeSubscribe since this is the final subscribe in the chain * since it is for BlockingObservable. */ - source.subscribe(mostRecentObserver); + ((Observable)source).subscribe(mostRecentObserver); return mostRecentObserver.getIterable(); } diff --git a/src/main/java/rx/internal/operators/BlockingOperatorNext.java b/src/main/java/rx/internal/operators/BlockingOperatorNext.java index 7a55a663eb..fc6a41487b 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorNext.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorNext.java @@ -96,7 +96,7 @@ private boolean moveToNext() { started = true; // if not started, start now observer.setWaiting(1); - items.materialize().subscribe(observer); + ((Observable)items).materialize().subscribe(observer); } Notification nextNotification = observer.takeNext(); diff --git a/src/main/java/rx/internal/operators/BlockingOperatorToFuture.java b/src/main/java/rx/internal/operators/BlockingOperatorToFuture.java index 29021405ca..daeee2e770 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorToFuture.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorToFuture.java @@ -54,7 +54,7 @@ public static Future toFuture(Observable that) { final AtomicReference value = new AtomicReference(); final AtomicReference error = new AtomicReference(); - final Subscription s = that.single().subscribe(new Subscriber() { + final Subscription s = ((Observable)that).single().subscribe(new Subscriber() { @Override public void onCompleted() { diff --git a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java index 899aaffacb..6f0f9b2616 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java @@ -50,7 +50,7 @@ public static Iterator toIterator(Observable source) { SubscriberIterator subscriber = new SubscriberIterator(); // using subscribe instead of unsafeSubscribe since this is a BlockingObservable "final subscribe" - source.materialize().subscribe(subscriber); + ((Observable)source).materialize().subscribe(subscriber); return subscriber; } diff --git a/src/main/java/rx/internal/operators/CachedObservable.java b/src/main/java/rx/internal/operators/CachedObservable.java index 1995174eff..6d9e7b6da6 100644 --- a/src/main/java/rx/internal/operators/CachedObservable.java +++ b/src/main/java/rx/internal/operators/CachedObservable.java @@ -39,7 +39,7 @@ public final class CachedObservable extends Observable { * @return the CachedObservable instance */ public static CachedObservable from(Observable source) { - return from(source, 16); + return (CachedObservable)from(source, 16); } /** diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeConcat.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeConcat.java index c7da20df07..8257c158a7 100644 --- a/src/main/java/rx/internal/operators/CompletableOnSubscribeConcat.java +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeConcat.java @@ -26,11 +26,11 @@ import rx.subscriptions.SerialSubscription; public final class CompletableOnSubscribeConcat implements CompletableOnSubscribe { - final Observable sources; + final Observable sources; final int prefetch; public CompletableOnSubscribeConcat(Observable sources, int prefetch) { - this.sources = sources; + this.sources = (Observable)sources; this.prefetch = prefetch; } diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java index a1c3cf64e9..7c3fff300c 100644 --- a/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java @@ -28,12 +28,12 @@ import rx.subscriptions.CompositeSubscription; public final class CompletableOnSubscribeMerge implements CompletableOnSubscribe { - final Observable source; + final Observable source; final int maxConcurrency; final boolean delayErrors; public CompletableOnSubscribeMerge(Observable source, int maxConcurrency, boolean delayErrors) { - this.source = source; + this.source = (Observable)source; this.maxConcurrency = maxConcurrency; this.delayErrors = delayErrors; } diff --git a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java index 93dcb5de5d..f254331913 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java +++ b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java @@ -140,7 +140,7 @@ public void subscribe(Observable[] sources) { if (cancelled) { return; } - sources[i].subscribe(as[i]); + ((Observable)sources[i]).subscribe(as[i]); } } diff --git a/src/main/java/rx/internal/operators/OperatorMapPair.java b/src/main/java/rx/internal/operators/OperatorMapPair.java index 29848d2f78..b16a9b3c41 100644 --- a/src/main/java/rx/internal/operators/OperatorMapPair.java +++ b/src/main/java/rx/internal/operators/OperatorMapPair.java @@ -47,7 +47,7 @@ public static Func1> convertSelector(final Func1>() { @Override public Observable call(T t1) { - return Observable.from(selector.call(t1)); + return (Observable)Observable.from(selector.call(t1)); } }; } diff --git a/src/main/java/rx/internal/operators/OperatorMulticast.java b/src/main/java/rx/internal/operators/OperatorMulticast.java index fcdededbed..6b760161da 100644 --- a/src/main/java/rx/internal/operators/OperatorMulticast.java +++ b/src/main/java/rx/internal/operators/OperatorMulticast.java @@ -149,6 +149,6 @@ public void onCompleted() { sub = subscription; } if (sub != null) - source.subscribe(sub); + ((Observable)source).subscribe(sub); } } \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index f4af56bcb9..4ca4bda3e9 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -135,7 +135,7 @@ public static ConnectableObservable create(Observable source public static ConnectableObservable create(Observable source, final int bufferSize) { if (bufferSize == Integer.MAX_VALUE) { - return create(source); + return (ConnectableObservable)create(source); } return create(source, new Func0>() { @Override @@ -155,7 +155,7 @@ public ReplayBuffer call() { */ public static ConnectableObservable create(Observable source, long maxAge, TimeUnit unit, Scheduler scheduler) { - return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE); + return (ConnectableObservable)create(source, maxAge, unit, scheduler, Integer.MAX_VALUE); } /** diff --git a/src/main/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOther.java b/src/main/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOther.java index efd9cf517b..a2d47dfee7 100644 --- a/src/main/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOther.java +++ b/src/main/java/rx/internal/operators/SingleOnSubscribeDelaySubscriptionOther.java @@ -86,6 +86,6 @@ public void onCompleted() { serial.set(otherSubscriber); - other.subscribe(otherSubscriber); + ((Observable)other).subscribe(otherSubscriber); } } diff --git a/src/main/java/rx/observables/AsyncOnSubscribe.java b/src/main/java/rx/observables/AsyncOnSubscribe.java index 24de19c149..c0e5a166b8 100644 --- a/src/main/java/rx/observables/AsyncOnSubscribe.java +++ b/src/main/java/rx/observables/AsyncOnSubscribe.java @@ -608,12 +608,13 @@ public void onCompleted() { }; subscriptions.add(s); - t.doOnTerminate(new Action0() { + Observable doOnTerminate = t.doOnTerminate(new Action0() { @Override public void call() { subscriptions.remove(s); - }}) - .subscribe(s); + }}); + + ((Observable)doOnTerminate).subscribe(s); merger.onNext(buffer); } diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index a44bf85558..33bde77d6a 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -99,7 +99,7 @@ public void forEach(final Action1 onNext) { * Use 'subscribe' instead of 'unsafeSubscribe' for Rx contract behavior * (see http://reactivex.io/documentation/contract.html) as this is the final subscribe in the chain. */ - Subscription subscription = o.subscribe(new Subscriber() { + Subscription subscription = ((Observable)o).subscribe(new Subscriber() { @Override public void onCompleted() { latch.countDown(); @@ -144,7 +144,7 @@ public void onNext(T args) { * @see ReactiveX documentation: To */ public Iterator getIterator() { - return BlockingOperatorToIterator.toIterator(o); + return BlockingOperatorToIterator.toIterator((Observable)o); } /** @@ -299,7 +299,7 @@ public Iterable mostRecent(T initialValue) { * @see ReactiveX documentation: TakeLast */ public Iterable next() { - return BlockingOperatorNext.next(o); + return BlockingOperatorNext.next((Observable)o); } /** @@ -316,7 +316,7 @@ public Iterable next() { * @see ReactiveX documentation: First */ public Iterable latest() { - return BlockingOperatorLatest.latest(o); + return BlockingOperatorLatest.latest((Observable)o); } /** @@ -398,7 +398,7 @@ public T singleOrDefault(T defaultValue, Func1 predicate) { * @see ReactiveX documentation: To */ public Future toFuture() { - return BlockingOperatorToFuture.toFuture(o); + return BlockingOperatorToFuture.toFuture((Observable)o); } /** @@ -430,7 +430,7 @@ private T blockForSingle(final Observable observable) { final AtomicReference returnException = new AtomicReference(); final CountDownLatch latch = new CountDownLatch(1); - Subscription subscription = observable.subscribe(new Subscriber() { + Subscription subscription = ((Observable)observable).subscribe(new Subscriber() { @Override public void onCompleted() { latch.countDown(); @@ -467,7 +467,7 @@ public void onNext(final T item) { public void subscribe() { final CountDownLatch cdl = new CountDownLatch(1); final Throwable[] error = { null }; - Subscription s = o.subscribe(new Subscriber() { + Subscription s = ((Observable)o).subscribe(new Subscriber() { @Override public void onNext(T t) { @@ -504,7 +504,7 @@ public void subscribe(Observer observer) { final NotificationLite nl = NotificationLite.instance(); final BlockingQueue queue = new LinkedBlockingQueue(); - Subscription s = o.subscribe(new Subscriber() { + Subscription s = ((Observable)o).subscribe(new Subscriber() { @Override public void onNext(T t) { queue.offer(nl.next(t)); @@ -592,7 +592,7 @@ public void call() { } })); - o.subscribe(s); + ((Observable)o).subscribe(s); try { for (;;) { diff --git a/src/main/java/rx/singles/BlockingSingle.java b/src/main/java/rx/singles/BlockingSingle.java index 6821bc5b82..9d327f6b6a 100644 --- a/src/main/java/rx/singles/BlockingSingle.java +++ b/src/main/java/rx/singles/BlockingSingle.java @@ -100,7 +100,7 @@ public void onError(Throwable error) { */ @Experimental public Future toFuture() { - return BlockingOperatorToFuture.toFuture(single.toObservable()); + return BlockingOperatorToFuture.toFuture(((Single)single).toObservable()); } } From c8e1b039ca6e6fddf5558657645043448d75ee77 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Mon, 4 Apr 2016 20:39:52 +0200 Subject: [PATCH 039/322] 1.x: make defensive copy of the properties in RxJavaPlugins --- src/main/java/rx/plugins/RxJavaPlugins.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/plugins/RxJavaPlugins.java b/src/main/java/rx/plugins/RxJavaPlugins.java index 6391a91185..acedd07449 100644 --- a/src/main/java/rx/plugins/RxJavaPlugins.java +++ b/src/main/java/rx/plugins/RxJavaPlugins.java @@ -200,7 +200,11 @@ public void registerSingleExecutionHook(RxJavaSingleExecutionHook impl) { } } - /* test */ static Object getPluginImplementationViaProperty(Class pluginClass, Properties props) { + /* test */ static Object getPluginImplementationViaProperty(Class pluginClass, Properties propsIn) { + // Make a defensive clone because traversal may fail with ConcurrentModificationException + // if the properties get changed by something outside RxJava. + Properties props = (Properties)propsIn.clone(); + final String classSimpleName = pluginClass.getSimpleName(); /* * Check system properties for plugin class. From 6aee6f884bf8d3ad92fcae4158c5667dd5e405d8 Mon Sep 17 00:00:00 2001 From: adi1133 Date: Thu, 7 Apr 2016 09:56:37 +0300 Subject: [PATCH 040/322] 1.x: Add maxConcurrent parameter to concatMapEager * Add maxConcurrent parameter to concatMapEager * Improve ConcatMapEager performance and add unit test * Add test case and cleanup whitespace --- src/main/java/rx/Observable.java | 36 ++++++++++++++++++- .../operators/OperatorEagerConcatMap.java | 10 ++++-- .../operators/OperatorEagerConcatMapTest.java | 36 ++++++++++++++++--- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 439da019ed..0b2baab702 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5353,7 +5353,41 @@ public final Observable concatMapEager(Func1 0 required but it was " + capacityHint); } - return lift(new OperatorEagerConcatMap(mapper, capacityHint)); + return lift(new OperatorEagerConcatMap(mapper, capacityHint, Integer.MAX_VALUE)); + } + + /** + * Maps a sequence of values into Observables and concatenates these Observables eagerly into a single + * Observable. + *

+ * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source Observables. The operator buffers the values emitted by these Observables and then drains them in + * order, each one after the previous one completes. + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources + * are subscribed to in unbounded mode and their values are queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param mapper the function that maps a sequence of values into a sequence of Observables that will be + * eagerly concatenated + * @param capacityHint hints about the number of expected source sequence values + * @param maxConcurrent the maximum number of concurrent subscribed observables + * @return + * @warn javadoc fails to describe the return value + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public final Observable concatMapEager(Func1> mapper, int capacityHint, int maxConcurrent) { + if (capacityHint < 1) { + throw new IllegalArgumentException("capacityHint > 0 required but it was " + capacityHint); + } + if (maxConcurrent < 1) { + throw new IllegalArgumentException("maxConcurrent > 0 required but it was " + capacityHint); + } + return lift(new OperatorEagerConcatMap(mapper, capacityHint, maxConcurrent)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java b/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java index bbf2bcc48b..cfa46837b5 100644 --- a/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java +++ b/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java @@ -31,14 +31,16 @@ public final class OperatorEagerConcatMap implements Operator { final Func1> mapper; final int bufferSize; - public OperatorEagerConcatMap(Func1> mapper, int bufferSize) { + private final int maxConcurrent; + public OperatorEagerConcatMap(Func1> mapper, int bufferSize, int maxConcurrent) { this.mapper = mapper; this.bufferSize = bufferSize; + this.maxConcurrent = maxConcurrent; } @Override public Subscriber call(Subscriber t) { - EagerOuterSubscriber outer = new EagerOuterSubscriber(mapper, bufferSize, t); + EagerOuterSubscriber outer = new EagerOuterSubscriber(mapper, bufferSize, maxConcurrent, t); outer.init(); return outer; } @@ -82,12 +84,13 @@ static final class EagerOuterSubscriber extends Subscriber { private EagerOuterProducer sharedProducer; public EagerOuterSubscriber(Func1> mapper, int bufferSize, - Subscriber actual) { + int maxConcurrent, Subscriber actual) { this.mapper = mapper; this.bufferSize = bufferSize; this.actual = actual; this.subscribers = new LinkedList>(); this.wip = new AtomicInteger(); + request(maxConcurrent == Integer.MAX_VALUE ? Long.MAX_VALUE : maxConcurrent); } void init() { @@ -223,6 +226,7 @@ void drain() { } innerSubscriber.unsubscribe(); innerDone = true; + request(1); break; } } diff --git a/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java b/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java index 8d2d40bed4..e6298230e4 100644 --- a/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java +++ b/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java @@ -16,6 +16,8 @@ package rx.internal.operators; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.*; import org.junit.*; @@ -302,11 +304,16 @@ public Observable call(Integer t) { ts.assertNotCompleted(); ts.assertError(TestException.class); } - + @Test(expected = IllegalArgumentException.class) public void testInvalidCapacityHint() { Observable.just(1).concatMapEager(toJust, 0); } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidMaxConcurrent() { + Observable.just(1).concatMapEager(toJust, RxRingBuffer.SIZE, 0); + } @Test public void testBackpressure() { @@ -397,17 +404,38 @@ public void call(Integer t) { @Test public void testInnerNull() { - TestSubscriber ts = TestSubscriber.create(); - Observable.just(1).concatMapEager(new Func1>() { @Override public Observable call(Integer t) { return Observable.just(null); } }).subscribe(ts); - + ts.assertNoErrors(); ts.assertCompleted(); ts.assertValue(null); } + + + @Test + public void testMaxConcurrent5() { + final List requests = new ArrayList(); + Observable.range(1, 100).doOnRequest(new Action1() { + @Override + public void call(Long reqCount) { + requests.add(reqCount); + } + }).concatMapEager(toJust, RxRingBuffer.SIZE, 5).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValueCount(100); + ts.assertCompleted(); + + Assert.assertEquals(5, (long) requests.get(0)); + Assert.assertEquals(1, (long) requests.get(1)); + Assert.assertEquals(1, (long) requests.get(2)); + Assert.assertEquals(1, (long) requests.get(3)); + Assert.assertEquals(1, (long) requests.get(4)); + Assert.assertEquals(1, (long) requests.get(5)); + } } From 7593d8c4eb5c457ef7afe4ad0162093ec8e22d83 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Thu, 7 Apr 2016 09:57:14 +0300 Subject: [PATCH 041/322] 1.x fromCallable() @Experimental -> @Beta --- src/main/java/rx/Observable.java | 2 +- src/main/java/rx/Single.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 0b2baab702..bd96ee4556 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1464,7 +1464,7 @@ public static Observable from(T[] array) { * @see #defer(Func0) * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ - @Experimental + @Beta public static Observable fromCallable(Callable func) { return create(new OnSubscribeFromCallable(func)); } diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index c1565313ab..b1e2ed4074 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -603,7 +603,7 @@ public static Single from(Future future, Scheduler scheduler * the type of the item emitted by the {@link Single}. * @return a {@link Single} whose {@link Observer}s' subscriptions trigger an invocation of the given function. */ - @Experimental + @Beta public static Single fromCallable(final Callable func) { return create(new OnSubscribe() { @Override From bcd7fa1ebb73594c69e85c6060fef27e74006e79 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Thu, 7 Apr 2016 08:59:56 +0200 Subject: [PATCH 042/322] 1.x: fix switchMap/switchOnNext producer retention and backpressure --- .../rx/internal/operators/OperatorSwitch.java | 382 ++++++++++-------- .../util/atomic/SpscLinkedArrayQueue.java | 31 +- .../operators/OperatorSwitchTest.java | 91 ++++- 3 files changed, 325 insertions(+), 179 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSwitch.java b/src/main/java/rx/internal/operators/OperatorSwitch.java index 7d706f2a95..bbbcd9879d 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitch.java +++ b/src/main/java/rx/internal/operators/OperatorSwitch.java @@ -16,14 +16,17 @@ package rx.internal.operators; import java.util.*; +import java.util.concurrent.atomic.AtomicLong; import rx.*; import rx.Observable; import rx.Observable.Operator; import rx.exceptions.CompositeException; -import rx.internal.producers.ProducerArbiter; +import rx.functions.Action0; +import rx.internal.util.RxRingBuffer; +import rx.internal.util.atomic.SpscLinkedArrayQueue; import rx.plugins.RxJavaPlugins; -import rx.subscriptions.SerialSubscription; +import rx.subscriptions.*; /** * Transforms an Observable that emits Observables into a single Observable that @@ -45,6 +48,9 @@ private static final class HolderDelayError { static final OperatorSwitch INSTANCE = new OperatorSwitch(true); } /** + * Returns a singleton instance of the operator based on the delayError parameter. + * @param the value type + * @param delayError should the errors of the inner sources delayed until the main sequence completes? * @return a singleton instance of this stateless operator. */ @SuppressWarnings({ "unchecked" }) @@ -72,51 +78,80 @@ public Subscriber> call(final Subscriber extends Subscriber> { final Subscriber child; final SerialSubscription ssub; - final ProducerArbiter arbiter; - final boolean delayError; + final AtomicLong index; + final SpscLinkedArrayQueue queue; + final NotificationLite nl; + + boolean emitting; - long index; + boolean missed; - Throwable error; + long requested; - boolean mainDone; + Producer producer; - List queue; + volatile boolean mainDone; + + Throwable error; boolean innerActive; - boolean emitting; - - boolean missed; + static final Throwable TERMINAL_ERROR = new Throwable("Terminal error"); SwitchSubscriber(Subscriber child, boolean delayError) { this.child = child; - this.arbiter = new ProducerArbiter(); this.ssub = new SerialSubscription(); this.delayError = delayError; + this.index = new AtomicLong(); + this.queue = new SpscLinkedArrayQueue(RxRingBuffer.SIZE); + this.nl = NotificationLite.instance(); } void init() { child.add(ssub); + child.add(Subscriptions.create(new Action0() { + @Override + public void call() { + clearProducer(); + } + })); child.setProducer(new Producer(){ @Override public void request(long n) { - if (n > 0) { - arbiter.request(n); + if (n > 0L) { + childRequested(n); + } else + if (n < 0L) { + throw new IllegalArgumentException("n >= 0 expected but it was " + n); } } }); } - + + void clearProducer() { + synchronized (this) { + producer = null; + } + } + @Override public void onNext(Observable t) { + long id = index.incrementAndGet(); + + Subscription s = ssub.get(); + if (s != null) { + s.unsubscribe(); + } + InnerSubscriber inner; + synchronized (this) { - long id = ++index; inner = new InnerSubscriber(id, this); + innerActive = true; + producer = null; } ssub.set(inner); @@ -125,201 +160,228 @@ public void onNext(Observable t) { @Override public void onError(Throwable e) { + boolean success; + synchronized (this) { - e = updateError(e); + success = updateError(e); + } + if (success) { mainDone = true; - - if (emitting) { - missed = true; - return; - } - if (delayError && innerActive) { - return; - } - emitting = true; + drain(); + } else { + pluginError(e); } - - child.onError(e); } + boolean updateError(Throwable next) { + Throwable e = error; + if (e == TERMINAL_ERROR) { + return false; + } else + if (e == null) { + error = next; + } else + if (e instanceof CompositeException) { + List list = new ArrayList(((CompositeException)e).getExceptions()); + list.add(next); + error = new CompositeException(list); + } else { + error = new CompositeException(e, next); + } + return true; + } + @Override public void onCompleted() { - Throwable ex; + mainDone = true; + drain(); + } + + void emit(T value, InnerSubscriber inner) { synchronized (this) { - mainDone = true; - if (emitting) { - missed = true; + if (index.get() != inner.id) { return; } - if (innerActive) { - return; + + queue.offer(inner, nl.next(value)); + } + drain(); + } + + void error(Throwable e, long id) { + boolean success; + synchronized (this) { + if (index.get() == id) { + success = updateError(e); + innerActive = false; + producer = null; + } else { + success = true; } - emitting = true; - ex = error; } - if (ex == null) { - child.onCompleted(); + if (success) { + drain(); } else { - child.onError(ex); + pluginError(e); } } - Throwable updateError(Throwable e) { - Throwable ex = error; - if (ex == null) { - error = e; - } else - if (ex instanceof CompositeException) { - CompositeException ce = (CompositeException) ex; - List list = new ArrayList(ce.getExceptions()); - list.add(e); - e = new CompositeException(list); - error = e; - } else { - e = new CompositeException(Arrays.asList(ex, e)); - error = e; + void complete(long id) { + synchronized (this) { + if (index.get() != id) { + return; + } + innerActive = false; + producer = null; } - return e; + drain(); + } + + void pluginError(Throwable e) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); } - void emit(T value, long id) { + void innerProducer(Producer p, long id) { + long n; synchronized (this) { - if (id != index) { + if (index.get() != id) { return; } - + n = requested; + producer = p; + } + + p.request(n); + } + + void childRequested(long n) { + Producer p; + synchronized (this) { + p = producer; + requested = BackpressureUtils.addCap(requested, n); + } + if (p != null) { + p.request(n); + } + drain(); + } + + void drain() { + boolean localMainDone = mainDone; + boolean localInnerActive; + long localRequested; + Throwable localError; + synchronized (this) { if (emitting) { - List q = queue; - if (q == null) { - q = new ArrayList(4); - queue = q; - } - q.add(value); missed = true; return; } - emitting = true; + localInnerActive = innerActive; + localRequested = requested; + localError = error; + if (localError != null && localError != TERMINAL_ERROR && !delayError) { + error = TERMINAL_ERROR; + } } - - child.onNext(value); - - arbiter.produced(1); - + + final SpscLinkedArrayQueue localQueue = queue; + final AtomicLong localIndex = index; + final Subscriber localChild = child; + for (;;) { - if (child.isUnsubscribed()) { - return; - } - - Throwable localError; - boolean localMainDone; - boolean localActive; - List localQueue; - synchronized (this) { - if (!missed) { - emitting = false; + + long localEmission = 0L; + + while (localEmission != localRequested) { + if (localChild.isUnsubscribed()) { return; } + + boolean empty = localQueue.isEmpty(); - localError = error; - localMainDone = mainDone; - localQueue = queue; - localActive = innerActive; - } - - if (!delayError && localError != null) { - child.onError(localError); - return; - } - - if (localQueue == null && !localActive && localMainDone) { - if (localError != null) { - child.onError(localError); - } else { - child.onCompleted(); + if (checkTerminated(localMainDone, localInnerActive, localError, + localQueue, localChild, empty)) { + return; + } + + if (empty) { + break; + } + + @SuppressWarnings("unchecked") + InnerSubscriber inner = (InnerSubscriber)localQueue.poll(); + T value = nl.getValue(localQueue.poll()); + + if (localIndex.get() == inner.id) { + localChild.onNext(value); + localEmission++; } - return; } - if (localQueue != null) { - int n = 0; - for (T v : localQueue) { - if (child.isUnsubscribed()) { - return; - } - - child.onNext(v); - n++; + if (localEmission == localRequested) { + if (localChild.isUnsubscribed()) { + return; } - arbiter.produced(n); + if (checkTerminated(mainDone, localInnerActive, localError, localQueue, + localChild, localQueue.isEmpty())) { + return; + } } - } - } - - void error(Throwable e, long id) { - boolean drop; - synchronized (this) { - if (id == index) { - innerActive = false; - - e = updateError(e); + + + synchronized (this) { - if (emitting) { - missed = true; - return; + localRequested = requested; + if (localRequested != Long.MAX_VALUE) { + localRequested -= localEmission; + requested = localRequested; } - if (delayError && !mainDone) { + + if (!missed) { + emitting = false; return; } - emitting = true; + missed = false; - drop = false; - } else { - drop = true; + localMainDone = mainDone; + localInnerActive = innerActive; + localError = error; + if (localError != null && localError != TERMINAL_ERROR && !delayError) { + error = TERMINAL_ERROR; + } } } - - if (drop) { - pluginError(e); - } else { - child.onError(e); - } } - - void complete(long id) { - Throwable ex; - synchronized (this) { - if (id != index) { - return; - } - innerActive = false; - - if (emitting) { - missed = true; - return; - } - - ex = error; - if (!mainDone) { - return; + protected boolean checkTerminated(boolean localMainDone, boolean localInnerActive, Throwable localError, + final SpscLinkedArrayQueue localQueue, final Subscriber localChild, boolean empty) { + if (delayError) { + if (localMainDone && !localInnerActive && empty) { + if (localError != null) { + localChild.onError(localError); + } else { + localChild.onCompleted(); + } + return true; } - } - - if (ex != null) { - child.onError(ex); } else { - child.onCompleted(); + if (localError != null) { + localQueue.clear(); + localChild.onError(localError); + return true; + } else + if (localMainDone && !localInnerActive && empty) { + localChild.onCompleted(); + return true; + } } - } - - void pluginError(Throwable e) { - RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + return false; } } - private static final class InnerSubscriber extends Subscriber { + static final class InnerSubscriber extends Subscriber { private final long id; @@ -332,12 +394,12 @@ private static final class InnerSubscriber extends Subscriber { @Override public void setProducer(Producer p) { - parent.arbiter.setProducer(p); + parent.innerProducer(p, id); } @Override public void onNext(T t) { - parent.emit(t, id); + parent.emit(t, this); } @Override diff --git a/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java index 33472a40da..23d8ad7c9e 100644 --- a/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java +++ b/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java @@ -30,23 +30,19 @@ /** * A single-producer single-consumer array-backed queue which can allocate new arrays in case the consumer is slower * than the producer. + * + * @param the element type, not null */ public final class SpscLinkedArrayQueue implements Queue { static final int MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); - protected volatile long producerIndex; - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater PRODUCER_INDEX = - AtomicLongFieldUpdater.newUpdater(SpscLinkedArrayQueue.class, "producerIndex"); + protected final AtomicLong producerIndex; protected int producerLookAheadStep; protected long producerLookAhead; protected int producerMask; protected AtomicReferenceArray producerBuffer; protected int consumerMask; protected AtomicReferenceArray consumerBuffer; - protected volatile long consumerIndex; - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater CONSUMER_INDEX = - AtomicLongFieldUpdater.newUpdater(SpscLinkedArrayQueue.class, "consumerIndex"); + protected final AtomicLong consumerIndex; private static final Object HAS_NEXT = new Object(); public SpscLinkedArrayQueue(final int bufferSize) { @@ -59,7 +55,8 @@ public SpscLinkedArrayQueue(final int bufferSize) { consumerBuffer = buffer; consumerMask = mask; producerLookAhead = mask - 1; // we know it's all empty to start with - soProducerIndex(0L); + producerIndex = new AtomicLong(); + consumerIndex = new AtomicLong(); } /** @@ -219,27 +216,27 @@ private void adjustLookAheadStep(int capacity) { } private long lvProducerIndex() { - return producerIndex; + return producerIndex.get(); } private long lvConsumerIndex() { - return consumerIndex; + return consumerIndex.get(); } private long lpProducerIndex() { - return producerIndex; + return producerIndex.get(); } private long lpConsumerIndex() { - return consumerIndex; + return consumerIndex.get(); } private void soProducerIndex(long v) { - PRODUCER_INDEX.lazySet(this, v); + producerIndex.lazySet(v); } private void soConsumerIndex(long v) { - CONSUMER_INDEX.lazySet(this, v); + consumerIndex.lazySet(v); } private static int calcWrappedOffset(long index, int mask) { @@ -321,11 +318,11 @@ public T element() { *

Don't use the regular offer() with this at all! * @param first * @param second - * @return + * @return always true */ public boolean offer(T first, T second) { final AtomicReferenceArray buffer = producerBuffer; - final long p = producerIndex; + final long p = lvProducerIndex(); final int m = producerMask; int pi = calcWrappedOffset(p + 2, m); diff --git a/src/test/java/rx/internal/operators/OperatorSwitchTest.java b/src/test/java/rx/internal/operators/OperatorSwitchTest.java index 55170ab9ff..b673b56949 100644 --- a/src/test/java/rx/internal/operators/OperatorSwitchTest.java +++ b/src/test/java/rx/internal/operators/OperatorSwitchTest.java @@ -19,6 +19,7 @@ import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; +import java.lang.ref.WeakReference; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; @@ -32,7 +33,7 @@ import rx.exceptions.*; import rx.functions.*; import rx.observers.TestSubscriber; -import rx.schedulers.TestScheduler; +import rx.schedulers.*; import rx.subjects.PublishSubject; public class OperatorSwitchTest { @@ -654,7 +655,7 @@ public Observable call(Long t) { ts.requestMore(Long.MAX_VALUE - 1); ts.awaitTerminalEvent(); assertTrue(ts.getOnNextEvents().size() > 0); - assertEquals(5, requests.size()); + assertEquals(4, requests.size()); // depends on the request pattern assertEquals(Long.MAX_VALUE, (long) requests.get(requests.size()-1)); } @@ -790,4 +791,90 @@ public Observable call(Integer v) { ts.assertCompleted(); } + Object ref; + + @Test + public void producerIsNotRetained() throws Exception { + ref = new Object(); + + WeakReference wr = new WeakReference(ref); + + PublishSubject> ps = PublishSubject.create(); + + Subscriber observer = new Subscriber() { + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Object t) { + } + }; + + Observable.switchOnNext(ps).subscribe(observer); + + ps.onNext(Observable.just(ref)); + + ref = null; + + System.gc(); + + Thread.sleep(500); + + Assert.assertNotNull(observer); // retain every other referenec in the pipeline + Assert.assertNotNull(ps); + Assert.assertNull("Object retained!", wr.get()); + } + + @Test + public void switchAsyncHeavily() { + for (int i = 1; i < 1024; i *= 2) { + System.out.println("switchAsyncHeavily >> " + i); + + final Queue q = new ConcurrentLinkedQueue(); + + final int j = i; + TestSubscriber ts = new TestSubscriber(i) { + int count; + @Override + public void onNext(Integer t) { + super.onNext(t); + if (++count == j) { + count = 0; + requestMore(j); + } + } + }; + + Observable.range(1, 25000) + .observeOn(Schedulers.computation(), i) + .switchMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(1, 1000).observeOn(Schedulers.computation(), j) + .doOnError(new Action1() { + @Override + public void call(Throwable e) { + q.add(e); + } + }); + } + }) + .timeout(10, TimeUnit.SECONDS) + .subscribe(ts); + + ts.awaitTerminalEvent(30, TimeUnit.SECONDS); + if (!q.isEmpty()) { + throw new AssertionError("Dropped exceptions", new CompositeException(q)); + } + ts.assertNoErrors(); + if (ts.getOnCompletedEvents().size() == 0) { + fail("switchAsyncHeavily timed out @ " + j + " (" + ts.getOnNextEvents().size() + " onNexts received)"); + } + } + } } From 1b70eb0afc2123658ddd5bb9fecbdfdf02aeabaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Thu, 7 Apr 2016 17:37:31 +0200 Subject: [PATCH 043/322] 1.x: fix concatMap scalar source behavior --- .../internal/operators/OnSubscribeConcatMap.java | 4 +++- .../rx/internal/operators/OperatorConcatTest.java | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java b/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java index 001058763b..6f0e8435b4 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java +++ b/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java @@ -273,6 +273,8 @@ void drain() { if (source instanceof ScalarSynchronousObservable) { ScalarSynchronousObservable scalarSource = (ScalarSynchronousObservable) source; + active = true; + arbiter.setProducer(new ConcatMapInnerScalarProducer(scalarSource.get(), this)); } else { ConcatMapInnerSubscriber innerSubscriber = new ConcatMapInnerSubscriber(this); @@ -352,7 +354,7 @@ public ConcatMapInnerScalarProducer(R value, ConcatMapSubscriber parent) { @Override public void request(long n) { - if (!once) { + if (!once && n > 0L) { once = true; ConcatMapSubscriber p = parent; p.innerNext(value); diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index 65aa1f5307..8a39b16f9f 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -850,4 +850,18 @@ public Observable call(Integer t) { } } + @Test + public void scalarAndRangeBackpressured() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.just(1).concatWith(Observable.range(2, 3)).subscribe(ts); + + ts.assertNoValues(); + + ts.requestMore(5); + + ts.assertValues(1, 2, 3, 4); + ts.assertCompleted(); + ts.assertNoErrors(); + } } \ No newline at end of file From eff6bb03eb333921bcb132159a92f2cf6717681d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Thu, 7 Apr 2016 17:51:00 +0200 Subject: [PATCH 044/322] More tests and fix empty() case as well --- .../operators/OnSubscribeConcatMap.java | 12 +++-- .../operators/OperatorConcatTest.java | 46 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java b/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java index 6f0e8435b4..c2799df758 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java +++ b/src/main/java/rx/internal/operators/OnSubscribeConcatMap.java @@ -220,7 +220,7 @@ void drain() { final int delayErrorMode = this.delayErrorMode; - do { + for (;;) { if (actual.isUnsubscribed()) { return; } @@ -288,11 +288,17 @@ void drain() { return; } } + request(1); + } else { + request(1); + continue; } - request(1); } } - } while (wip.decrementAndGet() != 0); + if (wip.decrementAndGet() == 0) { + break; + } + } } void drainError(Throwable mapperError) { diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index 8a39b16f9f..a824374659 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -864,4 +864,50 @@ public void scalarAndRangeBackpressured() { ts.assertCompleted(); ts.assertNoErrors(); } + + @Test + public void scalarAndEmptyBackpressured() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.just(1).concatWith(Observable.empty()).subscribe(ts); + + ts.assertNoValues(); + + ts.requestMore(5); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void rangeAndEmptyBackpressured() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.range(1, 2).concatWith(Observable.empty()).subscribe(ts); + + ts.assertNoValues(); + + ts.requestMore(5); + + ts.assertValues(1, 2); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void emptyAndScalarBackpressured() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.empty().concatWith(Observable.just(1)).subscribe(ts); + + ts.assertNoValues(); + + ts.requestMore(5); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + } \ No newline at end of file From ebf0e3f36519ffe471c63deb2c02b3636ef79900 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 8 Apr 2016 05:30:39 +1000 Subject: [PATCH 045/322] fix undesired execution in ExecutorScheduler worker when unsubscribed --- .../java/rx/schedulers/ExecutorScheduler.java | 12 +++++++- .../rx/schedulers/ExecutorSchedulerTest.java | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/schedulers/ExecutorScheduler.java b/src/main/java/rx/schedulers/ExecutorScheduler.java index 8e5c9bf22e..70f217e49b 100644 --- a/src/main/java/rx/schedulers/ExecutorScheduler.java +++ b/src/main/java/rx/schedulers/ExecutorScheduler.java @@ -96,11 +96,20 @@ public Subscription schedule(Action0 action) { @Override public void run() { do { + if (tasks.isUnsubscribed()) { + queue.clear(); + return; + } + ScheduledAction sa = queue.poll(); + if (sa == null) { + return; + } + if (!sa.isUnsubscribed()) { sa.run(); } - } while (wip.decrementAndGet() > 0); + } while (wip.decrementAndGet() != 0); } @Override @@ -170,6 +179,7 @@ public boolean isUnsubscribed() { @Override public void unsubscribe() { tasks.unsubscribe(); + queue.clear(); } } diff --git a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java index ed4e03213d..0777208cab 100644 --- a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java +++ b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java @@ -18,9 +18,11 @@ import static org.junit.Assert.*; import java.lang.management.*; +import java.util.Queue; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Assert; import org.junit.Test; import rx.*; @@ -275,4 +277,32 @@ public void call() { assertFalse(w.tasks.hasSubscriptions()); } + + @Test + public void workerUnderConcurrentUnsubscribeShouldNotAllowLaterTasksToRunDueToUnsubscriptionRace() { + Scheduler scheduler = Schedulers.from(Executors.newFixedThreadPool(1)); + for (int i = 0; i< 1000; i++) { + Worker worker = scheduler.createWorker(); + final Queue q = new ConcurrentLinkedQueue(); + Action0 action1 = new Action0() { + + @Override + public void call() { + q.add(1); + }}; + Action0 action2 = new Action0() { + + @Override + public void call() { + q.add(2); + }}; + worker.schedule(action1); + worker.schedule(action2); + worker.unsubscribe(); + if (q.size()==1 && q.poll() == 2) { + //expect a queue of 1,2 or 1. If queue is just 2 then we have a problem! + Assert.fail("wrong order on loop " + i); + } + } + } } From 53c31cde6906bad5825b2b65c91c1d268a6a32b5 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Fri, 8 Apr 2016 22:56:14 +0200 Subject: [PATCH 046/322] 1.x: observeOn now replenishes with constant rate (#3795) --- .../internal/operators/OperatorObserveOn.java | 45 ++++++++-------- .../operators/OperatorObserveOnTest.java | 51 ++++++++++++++++++- .../rx/observables/AsyncOnSubscribeTest.java | 7 +-- 3 files changed, 75 insertions(+), 28 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index 2a7c7684dd..1720e1dfe2 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -83,7 +83,8 @@ private static final class ObserveOnSubscriber extends Subscriber implemen final NotificationLite on; final boolean delayError; final Queue queue; - final int bufferSize; + /** The emission threshold that should trigger a replenishing request. */ + final int limit; // the status of the current stream volatile boolean finished; @@ -97,6 +98,9 @@ private static final class ObserveOnSubscriber extends Subscriber implemen * reading finished (acquire). */ Throwable error; + + /** Remembers how many elements have been emitted before the requests run out. */ + long emitted; // do NOT pass the Subscriber through to couple the subscription chain ... unsubscribing on the parent should // not prevent anything downstream from consuming, which will happen if the Subscription is chained @@ -105,12 +109,16 @@ public ObserveOnSubscriber(Scheduler scheduler, Subscriber child, boo this.recursiveScheduler = scheduler.createWorker(); this.delayError = delayError; this.on = NotificationLite.instance(); - this.bufferSize = (bufferSize > 0) ? bufferSize : RxRingBuffer.SIZE; + int calculatedSize = (bufferSize > 0) ? bufferSize : RxRingBuffer.SIZE; + // this formula calculates the 75% of the bufferSize, rounded up to the next integer + this.limit = calculatedSize - (calculatedSize >> 2); if (UnsafeAccess.isUnsafeAvailable()) { - queue = new SpscArrayQueue(this.bufferSize); + queue = new SpscArrayQueue(calculatedSize); } else { - queue = new SpscAtomicArrayQueue(this.bufferSize); + queue = new SpscAtomicArrayQueue(calculatedSize); } + // signal that this is an async operator capable of receiving this many + request(calculatedSize); } void init() { @@ -133,12 +141,6 @@ public void request(long n) { localChild.add(this); } - @Override - public void onStart() { - // signal that this is an async operator capable of receiving this many - request(this.bufferSize); - } - @Override public void onNext(final T t) { if (isUnsubscribed() || finished) { @@ -180,9 +182,8 @@ protected void schedule() { // only execute this from schedule() @Override public void call() { - long emitted = 0L; - long missed = 1L; + long currentEmission = emitted; // these are accessed in a tight loop around atomics so // loading them into local variables avoids the mandatory re-reading @@ -197,7 +198,6 @@ public void call() { for (;;) { long requestAmount = requested.get(); - long currentEmission = 0L; while (requestAmount != currentEmission) { boolean done = finished; @@ -215,7 +215,11 @@ public void call() { localChild.onNext(localOn.getValue(v)); currentEmission++; - emitted++; + if (currentEmission == limit) { + requestAmount = BackpressureUtils.produced(requested, currentEmission); + request(currentEmission); + currentEmission = 0L; + } } if (requestAmount == currentEmission) { @@ -223,20 +227,13 @@ public void call() { return; } } - - if (currentEmission != 0L) { - BackpressureUtils.produced(requested, currentEmission); - } - + + emitted = currentEmission; missed = counter.addAndGet(-missed); if (missed == 0L) { break; } } - - if (emitted != 0L) { - request(emitted); - } } boolean checkTerminated(boolean done, boolean isEmpty, Subscriber a, Queue q) { @@ -285,4 +282,4 @@ boolean checkTerminated(boolean done, boolean isEmpty, Subscriber a, return false; } } -} +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java index 323f74b786..29c1a4a8d5 100644 --- a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java @@ -864,7 +864,7 @@ public void testErrorDelayedAsync() { @Test public void requestExactCompletesImmediately() { -TestSubscriber ts = TestSubscriber.create(0); + TestSubscriber ts = TestSubscriber.create(0); TestScheduler test = Schedulers.test(); @@ -884,4 +884,53 @@ public void requestExactCompletesImmediately() { ts.assertNoErrors(); ts.assertCompleted(); } + + @Test + public void fixedReplenishPattern() { + TestSubscriber ts = TestSubscriber.create(0); + + TestScheduler test = Schedulers.test(); + + final List requests = new ArrayList(); + + Observable.range(1, 100) + .doOnRequest(new Action1() { + @Override + public void call(Long v) { + requests.add(v); + } + }) + .observeOn(test, 16).subscribe(ts); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + ts.requestMore(20); + test.advanceTimeBy(1, TimeUnit.SECONDS); + ts.requestMore(10); + test.advanceTimeBy(1, TimeUnit.SECONDS); + ts.requestMore(50); + test.advanceTimeBy(1, TimeUnit.SECONDS); + ts.requestMore(35); + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValueCount(100); + ts.assertCompleted(); + ts.assertNoErrors(); + + assertEquals(Arrays.asList(16L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L), requests); + } + + @Test + public void bufferSizesWork() { + for (int i = 1; i <= 1024; i = i * 2) { + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(1, 1000 * 1000).observeOn(Schedulers.computation(), i) + .subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertValueCount(1000 * 1000); + ts.assertCompleted(); + ts.assertNoErrors(); + } + } } diff --git a/src/test/java/rx/observables/AsyncOnSubscribeTest.java b/src/test/java/rx/observables/AsyncOnSubscribeTest.java index 633d229921..cd74520971 100644 --- a/src/test/java/rx/observables/AsyncOnSubscribeTest.java +++ b/src/test/java/rx/observables/AsyncOnSubscribeTest.java @@ -383,7 +383,8 @@ public void call() { }})); break; case 2: - observer.onNext(Observable.never() + observer.onNext(Observable.just(1) + .concatWith(Observable.never()) .subscribeOn(scheduler) .doOnUnsubscribe(new Action0(){ @Override @@ -397,7 +398,7 @@ public void call() { return state + 1; }}); Subscription subscription = Observable.create(os) - .observeOn(scheduler) + .observeOn(scheduler, 1) .subscribe(subscriber); sub.set(subscription); subscriber.assertNoValues(); @@ -465,4 +466,4 @@ public Integer call(Integer state, Long requested, Observer Date: Fri, 8 Apr 2016 23:39:57 +0200 Subject: [PATCH 047/322] 1.x: fix takeLast() backpressure (#3839) --- .../internal/operators/BackpressureUtils.java | 99 ++++++++++-- .../internal/operators/OperatorTakeLast.java | 95 +++++++----- .../operators/OperatorTakeLastTimed.java | 142 +++++++++++------- .../operators/TakeLastQueueProducer.java | 124 --------------- .../operators/OperatorTakeLastTest.java | 98 ++++++++++-- .../operators/OperatorTakeLastTimedTest.java | 80 +++++++++- 6 files changed, 396 insertions(+), 242 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/TakeLastQueueProducer.java diff --git a/src/main/java/rx/internal/operators/BackpressureUtils.java b/src/main/java/rx/internal/operators/BackpressureUtils.java index 3d199567c6..4a2fa90f48 100644 --- a/src/main/java/rx/internal/operators/BackpressureUtils.java +++ b/src/main/java/rx/internal/operators/BackpressureUtils.java @@ -19,6 +19,8 @@ import java.util.concurrent.atomic.*; import rx.Subscriber; +import rx.functions.Func1; +import rx.internal.util.UtilityFunctions; /** * Utility functions for use with backpressure. @@ -140,6 +142,59 @@ public static long addCap(long a, long b) { * @param actual the subscriber to receive the values */ public static void postCompleteDone(AtomicLong requested, Queue queue, Subscriber actual) { + postCompleteDone(requested, queue, actual, UtilityFunctions.identity()); + } + + /** + * Accumulates requests (validated) and handles the completed mode draining of the queue based on the requests. + * + *

+ * Post-completion backpressure handles the case when a source produces values based on + * requests when it is active but more values are available even after its completion. + * In this case, the onCompleted() can't just emit the contents of the queue but has to + * coordinate with the requested amounts. This requires two distinct modes: active and + * completed. In active mode, requests flow through and the queue is not accessed but + * in completed mode, requests no-longer reach the upstream but help in draining the queue. + * + * @param the value type to emit + * @param requested the holder of current requested amount + * @param n the value requested; + * @param queue the queue holding values to be emitted after completion + * @param actual the subscriber to receive the values + * @return true if in the active mode and the request amount of n can be relayed to upstream, false if + * in the post-completed mode and the queue is draining. + */ + public static boolean postCompleteRequest(AtomicLong requested, long n, Queue queue, Subscriber actual) { + return postCompleteRequest(requested, n, queue, actual, UtilityFunctions.identity()); + } + + /** + * Signals the completion of the main sequence and switches to post-completion replay mode + * and allows exit transformation on the queued values. + * + *

+ * Don't modify the queue after calling this method! + * + *

+ * Post-completion backpressure handles the case when a source produces values based on + * requests when it is active but more values are available even after its completion. + * In this case, the onCompleted() can't just emit the contents of the queue but has to + * coordinate with the requested amounts. This requires two distinct modes: active and + * completed. In active mode, requests flow through and the queue is not accessed but + * in completed mode, requests no-longer reach the upstream but help in draining the queue. + *

+ * The algorithm utilizes the most significant bit (bit 63) of a long value (AtomicLong) since + * request amount only goes up to Long.MAX_VALUE (bits 0-62) and negative values aren't + * allowed. + * + * @param the value type in the queue + * @param the value type to emit + * @param requested the holder of current requested amount + * @param queue the queue holding values to be emitted after completion + * @param actual the subscriber to receive the values + * @param exitTransform the transformation to apply on the dequeued value to get the value to be emitted + */ + public static void postCompleteDone(AtomicLong requested, Queue queue, Subscriber actual, Func1 exitTransform) { for (;;) { long r = requested.get(); @@ -156,7 +211,7 @@ public static void postCompleteDone(AtomicLong requested, Queue queue, Su // are requests available start draining the queue if (r != 0L) { // if the switch happened when there was outstanding requests, start draining - postCompleteDrain(requested, queue, actual); + postCompleteDrain(requested, queue, actual, exitTransform); } return; } @@ -164,7 +219,8 @@ public static void postCompleteDone(AtomicLong requested, Queue queue, Su } /** - * Accumulates requests (validated) and handles the completed mode draining of the queue based on the requests. + * Accumulates requests (validated) and handles the completed mode draining of the queue based on the requests + * and allows exit transformation on the queued values. * *

* Post-completion backpressure handles the case when a source produces values based on @@ -174,15 +230,17 @@ public static void postCompleteDone(AtomicLong requested, Queue queue, Su * completed. In active mode, requests flow through and the queue is not accessed but * in completed mode, requests no-longer reach the upstream but help in draining the queue. * - * @param the value type to emit + * @param the value type in the queue + * @param the value type to emit * @param requested the holder of current requested amount * @param n the value requested; * @param queue the queue holding values to be emitted after completion * @param actual the subscriber to receive the values + * @param exitTransform the transformation to apply on the dequeued value to get the value to be emitted * @return true if in the active mode and the request amount of n can be relayed to upstream, false if * in the post-completed mode and the queue is draining. */ - public static boolean postCompleteRequest(AtomicLong requested, long n, Queue queue, Subscriber actual) { + public static boolean postCompleteRequest(AtomicLong requested, long n, Queue queue, Subscriber actual, Func1 exitTransform) { if (n < 0L) { throw new IllegalArgumentException("n >= 0 required but it was " + n); } @@ -209,7 +267,7 @@ public static boolean postCompleteRequest(AtomicLong requested, long n, Queu // if there was no outstanding request before and in // the post-completed state, start draining if (r == COMPLETED_MASK) { - postCompleteDrain(requested, queue, actual); + postCompleteDrain(requested, queue, actual, exitTransform); return false; } // returns true for active mode and false if the completed flag was set @@ -219,16 +277,37 @@ public static boolean postCompleteRequest(AtomicLong requested, long n, Queu } /** - * Drains the queue based on the outstanding requests in post-completed mode (only!). + * Drains the queue based on the outstanding requests in post-completed mode (only!) + * and allows exit transformation on the queued values. * - * @param the value type to emit + * @param the value type in the queue + * @param the value type to emit * @param requested the holder of current requested amount * @param queue the queue holding values to be emitted after completion - * @param actual the subscriber to receive the values + * @param subscriber the subscriber to receive the values + * @param exitTransform the transformation to apply on the dequeued value to get the value to be emitted */ - static void postCompleteDrain(AtomicLong requested, Queue queue, Subscriber subscriber) { + static void postCompleteDrain(AtomicLong requested, Queue queue, Subscriber subscriber, Func1 exitTransform) { long r = requested.get(); + + // Run on a fast-path if the downstream is unbounded + if (r == Long.MAX_VALUE) { + for (;;) { + if (subscriber.isUnsubscribed()) { + return; + } + + T v = queue.poll(); + + if (v == null) { + subscriber.onCompleted(); + return; + } + + subscriber.onNext(exitTransform.call(v)); + } + } /* * Since we are supposed to be in the post-complete state, * requested will have its top bit set. @@ -264,7 +343,7 @@ static void postCompleteDrain(AtomicLong requested, Queue queue, Subscrib return; } - subscriber.onNext(v); + subscriber.onNext(exitTransform.call(v)); e++; } diff --git a/src/main/java/rx/internal/operators/OperatorTakeLast.java b/src/main/java/rx/internal/operators/OperatorTakeLast.java index 2812c4e87c..77f8c93993 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeLast.java +++ b/src/main/java/rx/internal/operators/OperatorTakeLast.java @@ -16,15 +16,18 @@ package rx.internal.operators; import java.util.ArrayDeque; -import java.util.Deque; +import java.util.concurrent.atomic.AtomicLong; +import rx.*; import rx.Observable.Operator; -import rx.Subscriber; +import rx.functions.Func1; /** * Returns an Observable that emits the at most the last count items emitted by the source Observable. *

* + * + * @param the value type */ public final class OperatorTakeLast implements Operator { @@ -39,44 +42,62 @@ public OperatorTakeLast(int count) { @Override public Subscriber call(final Subscriber subscriber) { - final Deque deque = new ArrayDeque(); - final NotificationLite notification = NotificationLite.instance(); - final TakeLastQueueProducer producer = new TakeLastQueueProducer(notification, deque, subscriber); - subscriber.setProducer(producer); - - return new Subscriber(subscriber) { - - // no backpressure up as it wants to receive and discard all but the last + final TakeLastSubscriber parent = new TakeLastSubscriber(subscriber, count); + + subscriber.add(parent); + subscriber.setProducer(new Producer() { @Override - public void onStart() { - // we do this to break the chain of the child subscriber being passed through - request(Long.MAX_VALUE); + public void request(long n) { + parent.requestMore(n); } - - @Override - public void onCompleted() { - deque.offer(notification.completed()); - producer.startEmitting(); - } - - @Override - public void onError(Throwable e) { - deque.clear(); - subscriber.onError(e); + }); + + return parent; + } + + static final class TakeLastSubscriber extends Subscriber implements Func1 { + final Subscriber actual; + final AtomicLong requested; + final ArrayDeque queue; + final int count; + final NotificationLite nl; + + public TakeLastSubscriber(Subscriber actual, int count) { + this.actual = actual; + this.count = count; + this.requested = new AtomicLong(); + this.queue = new ArrayDeque(); + this.nl = NotificationLite.instance(); + } + + @Override + public void onNext(T t) { + if (queue.size() == count) { + queue.poll(); } - - @Override - public void onNext(T value) { - if (count == 0) { - // If count == 0, we do not need to put value into deque and - // remove it at once. We can ignore the value directly. - return; - } - if (deque.size() == count) { - deque.removeFirst(); - } - deque.offerLast(notification.next(value)); + queue.offer(nl.next(t)); + } + + @Override + public void onError(Throwable e) { + queue.clear(); + actual.onError(e); + } + + @Override + public void onCompleted() { + BackpressureUtils.postCompleteDone(requested, queue, actual, this); + } + + @Override + public T call(Object t) { + return nl.getValue(t); + } + + void requestMore(long n) { + if (n > 0L) { + BackpressureUtils.postCompleteRequest(requested, n, queue, actual, this); } - }; + } } } diff --git a/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java b/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java index ec7cc12493..383f41715c 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java +++ b/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java @@ -15,18 +15,20 @@ */ package rx.internal.operators; -import rx.Observable.Operator; -import rx.Scheduler; -import rx.Subscriber; - import java.util.ArrayDeque; -import java.util.Deque; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import rx.*; +import rx.Observable.Operator; +import rx.functions.Func1; /** * Returns an Observable that emits the last count items emitted by the source Observable. *

* + * + * @param the value type */ public final class OperatorTakeLastTimed implements Operator { @@ -51,60 +53,92 @@ public OperatorTakeLastTimed(int count, long time, TimeUnit unit, Scheduler sche @Override public Subscriber call(final Subscriber subscriber) { - final Deque buffer = new ArrayDeque(); - final Deque timestampBuffer = new ArrayDeque(); - final NotificationLite notification = NotificationLite.instance(); - final TakeLastQueueProducer producer = new TakeLastQueueProducer(notification, buffer, subscriber); - subscriber.setProducer(producer); - return new Subscriber(subscriber) { - - protected void runEvictionPolicy(long now) { - // trim size - while (count >= 0 && buffer.size() > count) { - timestampBuffer.pollFirst(); - buffer.pollFirst(); - } - // remove old entries - while (!buffer.isEmpty()) { - long v = timestampBuffer.peekFirst(); - if (v < now - ageMillis) { - timestampBuffer.pollFirst(); - buffer.pollFirst(); - } else { - break; - } - } - } - - // no backpressure up as it wants to receive and discard all but the last + final TakeLastTimedSubscriber parent = new TakeLastTimedSubscriber(subscriber, count, ageMillis, scheduler); + + subscriber.add(parent); + subscriber.setProducer(new Producer() { @Override - public void onStart() { - // we do this to break the chain of the child subscriber being passed through - request(Long.MAX_VALUE); - } - - @Override - public void onNext(T args) { - long t = scheduler.now(); - timestampBuffer.add(t); - buffer.add(notification.next(args)); - runEvictionPolicy(t); + public void request(long n) { + parent.requestMore(n); } + }); + + return parent; + } + + static final class TakeLastTimedSubscriber extends Subscriber implements Func1 { + final Subscriber actual; + final long ageMillis; + final Scheduler scheduler; + final int count; + final AtomicLong requested; + final ArrayDeque queue; + final ArrayDeque queueTimes; + final NotificationLite nl; - @Override - public void onError(Throwable e) { - timestampBuffer.clear(); - buffer.clear(); - subscriber.onError(e); + public TakeLastTimedSubscriber(Subscriber actual, int count, long ageMillis, Scheduler scheduler) { + this.actual = actual; + this.count = count; + this.ageMillis = ageMillis; + this.scheduler = scheduler; + this.requested = new AtomicLong(); + this.queue = new ArrayDeque(); + this.queueTimes = new ArrayDeque(); + this.nl = NotificationLite.instance(); + } + + @Override + public void onNext(T t) { + if (count != 0) { + long now = scheduler.now(); + + if (queue.size() == count) { + queue.poll(); + queueTimes.poll(); + } + + evictOld(now); + + queue.offer(nl.next(t)); + queueTimes.offer(now); } + } - @Override - public void onCompleted() { - runEvictionPolicy(scheduler.now()); - timestampBuffer.clear(); - buffer.offer(notification.completed()); - producer.startEmitting(); + protected void evictOld(long now) { + long minTime = now - ageMillis; + for (;;) { + Long time = queueTimes.peek(); + if (time == null || time >= minTime) { + break; + } + queue.poll(); + queueTimes.poll(); } - }; + } + + @Override + public void onError(Throwable e) { + queue.clear(); + queueTimes.clear(); + actual.onError(e); + } + + @Override + public void onCompleted() { + evictOld(scheduler.now()); + + queueTimes.clear(); + + BackpressureUtils.postCompleteDone(requested, queue, actual, this); + } + + @Override + public T call(Object t) { + return nl.getValue(t); + } + + void requestMore(long n) { + BackpressureUtils.postCompleteRequest(requested, n, queue, actual, this); + } } } diff --git a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java deleted file mode 100644 index 664dfd0e3a..0000000000 --- a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.internal.operators; - - -import java.util.Deque; -import java.util.concurrent.atomic.AtomicLong; - -import rx.Producer; -import rx.Subscriber; -import rx.exceptions.Exceptions; - -final class TakeLastQueueProducer extends AtomicLong implements Producer { - - private final NotificationLite notification; - private final Deque deque; - private final Subscriber subscriber; - private volatile boolean emittingStarted = false; - - public TakeLastQueueProducer(NotificationLite n, Deque q, Subscriber subscriber) { - this.notification = n; - this.deque = q; - this.subscriber = subscriber; - } - - void startEmitting() { - if (!emittingStarted) { - emittingStarted = true; - emit(0); // start emitting - } - } - - @Override - public void request(long n) { - if (get() == Long.MAX_VALUE) { - return; - } - long _c; - if (n == Long.MAX_VALUE) { - _c = getAndSet(Long.MAX_VALUE); - } else { - _c = BackpressureUtils.getAndAddRequest(this, n); - } - if (!emittingStarted) { - // we haven't started yet, so record what was requested and return - return; - } - emit(_c); - } - - void emit(long previousRequested) { - if (get() == Long.MAX_VALUE) { - // fast-path without backpressure - if (previousRequested == 0) { - try { - for (Object value : deque) { - if (subscriber.isUnsubscribed()) - return; - notification.accept(subscriber, value); - } - } catch (Throwable e) { - Exceptions.throwOrReport(e, subscriber); - } finally { - deque.clear(); - } - } else { - // backpressure path will handle Long.MAX_VALUE and emit the rest events. - } - } else { - // backpressure is requested - if (previousRequested == 0) { - while (true) { - /* - * This complicated logic is done to avoid touching the volatile `requested` value - * during the loop itself. If it is touched during the loop the performance is impacted significantly. - */ - long numToEmit = get(); - int emitted = 0; - Object o; - while (--numToEmit >= 0 && (o = deque.poll()) != null) { - if (subscriber.isUnsubscribed()) { - return; - } - if (notification.accept(subscriber, o)) { - // terminal event - return; - } else { - emitted++; - } - } - for (; ; ) { - long oldRequested = get(); - long newRequested = oldRequested - emitted; - if (oldRequested == Long.MAX_VALUE) { - // became unbounded during the loop - // continue the outer loop to emit the rest events. - break; - } - if (compareAndSet(oldRequested, newRequested)) { - if (newRequested == 0) { - // we're done emitting the number requested so return - return; - } - break; - } - } - } - } - } - } -} diff --git a/src/test/java/rx/internal/operators/OperatorTakeLastTest.java b/src/test/java/rx/internal/operators/OperatorTakeLastTest.java index c3297db0a0..154b3067b0 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeLastTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeLastTest.java @@ -17,28 +17,24 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.*; import org.mockito.InOrder; import rx.Observable; import rx.Observer; +import rx.Scheduler.Worker; import rx.Subscriber; -import rx.functions.Func1; -import rx.internal.util.RxRingBuffer; -import rx.internal.util.UtilityFunctions; +import rx.functions.*; +import rx.internal.util.*; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; public class OperatorTakeLastTest { @@ -323,4 +319,76 @@ public void onNext(Integer t) { }}); assertEquals(50, list.size()); } + + @Test(timeout = 30000) // original could get into an infinite loop + public void completionRequestRace() { + Worker w = Schedulers.computation().createWorker(); + try { + final int n = 1000; + for (int i = 0; i < 25000; i++) { + if (i % 1000 == 0) { + System.out.println("completionRequestRace >> " + i); + } + PublishSubject ps = PublishSubject.create(); + final TestSubscriber ts = new TestSubscriber(0); + + ps.takeLast(n).subscribe(ts); + + for (int j = 0; j < n; j++) { + ps.onNext(j); + } + + final AtomicBoolean go = new AtomicBoolean(); + + w.schedule(new Action0() { + @Override + public void call() { + while (!go.get()); + ts.requestMore(n + 1); + } + }); + + go.set(true); + ps.onCompleted(); + + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + + ts.assertValueCount(n); + ts.assertNoErrors(); + ts.assertCompleted(); + + List list = ts.getOnNextEvents(); + for (int j = 0; j < n; j++) { + Assert.assertEquals(j, list.get(j).intValue()); + } + } + } finally { + w.unsubscribe(); + } + } + + @Test + public void nullElements() { + TestSubscriber ts = new TestSubscriber(0); + + Observable.from(new Integer[] { 1, null, 2}).takeLast(4) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(1, null, 2); + ts.assertCompleted(); + ts.assertNoErrors(); + } + } diff --git a/src/test/java/rx/internal/operators/OperatorTakeLastTimedTest.java b/src/test/java/rx/internal/operators/OperatorTakeLastTimedTest.java index 800a2cd673..c227339702 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeLastTimedTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeLastTimedTest.java @@ -22,15 +22,20 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Test; +import org.junit.*; import org.mockito.InOrder; import rx.Observable; import rx.Observer; +import rx.Scheduler.Worker; import rx.exceptions.TestException; -import rx.schedulers.TestScheduler; +import rx.functions.Action0; +import rx.observers.TestSubscriber; +import rx.schedulers.*; import rx.subjects.PublishSubject; public class OperatorTakeLastTimedTest { @@ -208,4 +213,75 @@ public void takeLastTimedWithZeroCapacity() { verify(o, never()).onNext(any()); verify(o, never()).onError(any(Throwable.class)); } + + @Test(timeout = 30000) // original could get into an infinite loop + public void completionRequestRace() { + Worker w = Schedulers.computation().createWorker(); + try { + final int n = 1000; + for (int i = 0; i < 25000; i++) { + if (i % 1000 == 0) { + System.out.println("completionRequestRace >> " + i); + } + PublishSubject ps = PublishSubject.create(); + final TestSubscriber ts = new TestSubscriber(0); + + ps.takeLast(n, 1, TimeUnit.DAYS).subscribe(ts); + + for (int j = 0; j < n; j++) { + ps.onNext(j); + } + + final AtomicBoolean go = new AtomicBoolean(); + + w.schedule(new Action0() { + @Override + public void call() { + while (!go.get()); + ts.requestMore(n + 1); + } + }); + + go.set(true); + ps.onCompleted(); + + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + + ts.assertValueCount(n); + ts.assertNoErrors(); + ts.assertCompleted(); + + List list = ts.getOnNextEvents(); + for (int j = 0; j < n; j++) { + Assert.assertEquals(j, list.get(j).intValue()); + } + } + } finally { + w.unsubscribe(); + } + } + + @Test + public void nullElements() { + TestSubscriber ts = new TestSubscriber(0); + + Observable.from(new Integer[] { 1, null, 2}).takeLast(4, 1, TimeUnit.DAYS) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(1, null, 2); + ts.assertCompleted(); + ts.assertNoErrors(); + } } From b126c6ce7d045f6b7499f7a51905434f513c6fda Mon Sep 17 00:00:00 2001 From: David Karnok Date: Fri, 8 Apr 2016 23:45:03 +0200 Subject: [PATCH 048/322] 1.x: fix delaySubscription(Observable) unsubscription before triggered (#3845) --- .../OnSubscribeDelaySubscriptionOther.java | 9 ++- ...OnSubscribeDelaySubscriptionOtherTest.java | 67 ++++++++++++++++++- .../internal/operators/OperatorDelayTest.java | 51 ++++++++++++-- 3 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionOther.java b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionOther.java index 2a8b7e1601..dc3146d7e6 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionOther.java +++ b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionOther.java @@ -20,7 +20,7 @@ import rx.Observable.OnSubscribe; import rx.observers.Subscribers; import rx.plugins.*; -import rx.subscriptions.SerialSubscription; +import rx.subscriptions.*; /** * Delays the subscription to the main source until the other @@ -39,9 +39,12 @@ public OnSubscribeDelaySubscriptionOther(Observable main, Observabl @Override public void call(Subscriber t) { + final SerialSubscription serial = new SerialSubscription(); + + t.add(serial); + final Subscriber child = Subscribers.wrap(t); - final SerialSubscription serial = new SerialSubscription(); Subscriber otherSubscriber = new Subscriber() { boolean done; @@ -66,7 +69,7 @@ public void onCompleted() { return; } done = true; - serial.set(child); + serial.set(Subscriptions.unsubscribed()); main.unsafeSubscribe(child); } diff --git a/src/test/java/rx/internal/operators/OnSubscribeDelaySubscriptionOtherTest.java b/src/test/java/rx/internal/operators/OnSubscribeDelaySubscriptionOtherTest.java index e157a788e5..b44b720b41 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeDelaySubscriptionOtherTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeDelaySubscriptionOtherTest.java @@ -16,7 +16,7 @@ package rx.internal.operators; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.*; import org.junit.*; @@ -243,4 +243,69 @@ public void call() { ts.assertNoErrors(); ts.assertCompleted(); } + + @Test + public void unsubscriptionPropagatesBeforeSubscribe() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.delaySubscription(other).subscribe(ts); + + Assert.assertFalse("source subscribed?", source.hasObservers()); + Assert.assertTrue("other not subscribed?", other.hasObservers()); + + ts.unsubscribe(); + + Assert.assertFalse("source subscribed?", source.hasObservers()); + Assert.assertFalse("other still subscribed?", other.hasObservers()); + } + + @Test + public void unsubscriptionPropagatesAfterSubscribe() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.delaySubscription(other).subscribe(ts); + + Assert.assertFalse("source subscribed?", source.hasObservers()); + Assert.assertTrue("other not subscribed?", other.hasObservers()); + + other.onCompleted(); + + Assert.assertTrue("source not subscribed?", source.hasObservers()); + Assert.assertFalse("other still subscribed?", other.hasObservers()); + + ts.unsubscribe(); + + Assert.assertFalse("source subscribed?", source.hasObservers()); + Assert.assertFalse("other still subscribed?", other.hasObservers()); + } + + @Test + public void delayAndTakeUntilNeverSubscribeToSource() { + PublishSubject delayUntil = PublishSubject.create(); + PublishSubject interrupt = PublishSubject.create(); + final AtomicBoolean subscribed = new AtomicBoolean(false); + + Observable.just(1) + .doOnSubscribe(new Action0() { + @Override + public void call() { + subscribed.set(true); + } + }) + .delaySubscription(delayUntil) + .takeUntil(interrupt) + .subscribe(); + + interrupt.onNext(9000); + delayUntil.onNext(1); + + Assert.assertFalse(subscribed.get()); + } + } diff --git a/src/test/java/rx/internal/operators/OperatorDelayTest.java b/src/test/java/rx/internal/operators/OperatorDelayTest.java index e4db021eaf..315248a5db 100644 --- a/src/test/java/rx/internal/operators/OperatorDelayTest.java +++ b/src/test/java/rx/internal/operators/OperatorDelayTest.java @@ -30,9 +30,9 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; import org.mockito.InOrder; import org.mockito.Mock; @@ -41,9 +41,7 @@ import rx.Observer; import rx.Subscription; import rx.exceptions.TestException; -import rx.functions.Action1; -import rx.functions.Func0; -import rx.functions.Func1; +import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observers.TestObserver; import rx.observers.TestSubscriber; @@ -821,4 +819,47 @@ public void testErrorRunsBeforeOnNext() { ts.assertError(TestException.class); ts.assertNotCompleted(); } + + @Test + public void delaySubscriptionCancelBeforeTime() { + PublishSubject source = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.delaySubscription(100, TimeUnit.MILLISECONDS, scheduler).subscribe(ts); + + Assert.assertFalse("source subscribed?", source.hasObservers()); + + ts.unsubscribe(); + + Assert.assertFalse("source subscribed?", source.hasObservers()); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + Assert.assertFalse("source subscribed?", source.hasObservers()); + } + + @Test + public void delayAndTakeUntilNeverSubscribeToSource() { + PublishSubject interrupt = PublishSubject.create(); + final AtomicBoolean subscribed = new AtomicBoolean(false); + TestScheduler testScheduler = new TestScheduler(); + + Observable.just(1) + .doOnSubscribe(new Action0() { + @Override + public void call() { + subscribed.set(true); + } + }) + .delaySubscription(1, TimeUnit.SECONDS, testScheduler) + .takeUntil(interrupt) + .subscribe(); + + interrupt.onNext(9000); + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertFalse(subscribed.get()); + } + } From 6abde0cc99035f079976ead0fc8f812e76f13edd Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 8 Apr 2016 23:56:43 +0200 Subject: [PATCH 049/322] 1.x: Release 1.1.3 CHANGES.md update --- CHANGES.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 1d6ec229ea..39da0ad41b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,43 @@ # RxJava Releases # +### Version 1.1.3 - April 8, 2016 ([Maven](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.1.3%7C)) + +#### API enhancements + + - [Pull 3780](https://github.com/ReactiveX/RxJava/pull/3780): `SyncOnSubscribe` has been promoted to `@Beta` API. + - [Pull 3799](https://github.com/ReactiveX/RxJava/pull/3799): Added `Completable.andThen(Single)` operator + - [Pull 3777](https://github.com/ReactiveX/RxJava/pull/3777): Added `observeOn` overload to configure the prefetch/buffer size + - [Pull 3790](https://github.com/ReactiveX/RxJava/pull/3790): Make `Single.lift` public and `@Experimental` + - [Pull 3818](https://github.com/ReactiveX/RxJava/pull/3818): `fromCallable` promotion to `@Beta` + - [Pull 3842](https://github.com/ReactiveX/RxJava/pull/3842): improve `ExecutorScheduler` worker unsubscription + +#### API deprecations + + - [Pull 3762](https://github.com/ReactiveX/RxJava/pull/3762): Deprecate `CompositeException` constructor with message prefix + +#### General enhancements + + - [Pull 3828](https://github.com/ReactiveX/RxJava/pull/3828): `AsyncSubject` now supports backpressure + - [Pull 3829](https://github.com/ReactiveX/RxJava/pull/3829): Added `rx.unsafe-disable` system property to disable use of `sun.misc.Unsafe` even if it is available + - [Pull 3757](https://github.com/ReactiveX/RxJava/pull/3757): **Warning: behavior change!** Operator `sample` emits last sampled value before termination + +#### Performance enhancements + + - [Pull 3795](https://github.com/ReactiveX/RxJava/pull/3795): `observeOn` now replenishes with constant rate + +#### Bugfixes + + - [Pull 3809](https://github.com/ReactiveX/RxJava/pull/3809): fix `merge`/`flatMap` crash when the inner source was `just(null)` + - [Pull 3789](https://github.com/ReactiveX/RxJava/pull/3789): Prevent `Single.zip()` of zero `Single`s + - [Pull 3787](https://github.com/ReactiveX/RxJava/pull/3787): fix `groupBy` delaying group completion till all groups were emitted + - [Pull 3823](https://github.com/ReactiveX/RxJava/pull/3823): fix `DoAfterTerminate` handle if action throws + - [Pull 3822](https://github.com/ReactiveX/RxJava/pull/3822): make defensive copy of the properties in `RxJavaPlugins` + - [Pull 3836](https://github.com/ReactiveX/RxJava/pull/3836): fix `switchMap`/`switchOnNext` producer retention and backpressure + - [Pull 3840](https://github.com/ReactiveX/RxJava/pull/3840): fix `concatMap` scalar/empty source behavior + - [Pull 3839](https://github.com/ReactiveX/RxJava/pull/3839): fix `takeLast()` backpressure + - [Pull 3845](https://github.com/ReactiveX/RxJava/pull/3845): fix delaySubscription(Observable) unsubscription before triggered + + ### Version 1.1.2 - March 18, 2016 ([Maven](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.1.2%7C)) #### API Enhancements From 3e148a8832c18b0ac3297da4620d2d8c8a78da40 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Sun, 10 Apr 2016 17:52:59 -0400 Subject: [PATCH 050/322] Remove unused local. --- src/main/java/rx/subjects/ReplaySubject.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index db8490f731..f1b693dc9a 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -717,8 +717,7 @@ public boolean isEmpty() { @SuppressWarnings("unchecked") public T[] toArray(T[] a) { List list = new ArrayList(); - NodeList.Node l = head(); - NodeList.Node next = l.next; + NodeList.Node next = head().next; while (next != null) { Object o = leaveTransform.call(next.value); @@ -727,7 +726,6 @@ public T[] toArray(T[] a) { } else { list.add((T)o); } - l = next; next = next.next; } return list.toArray(a); From d799ecac4cb496303f774b1864882dbcabbf7ec7 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Thu, 14 Apr 2016 19:50:26 +0200 Subject: [PATCH 051/322] 1.x: deanonymize Observable inner classes (#3848) * 1.x: deanonymize Observable inner classes * Further simplification of types * Fix indentation, rename Lambda to Action --- src/main/java/rx/Observable.java | 520 +++--------------- src/main/java/rx/functions/Actions.java | 23 + .../operators/EmptyObservableHolder.java | 46 ++ .../operators/NeverObservableHolder.java | 45 ++ .../internal/operators/OnSubscribeLift.java | 65 +++ .../internal/operators/OnSubscribeThrow.java | 46 ++ .../util/ActionNotificationObserver.java | 48 ++ .../rx/internal/util/ActionSubscriber.java | 51 ++ .../util/InternalObservableUtils.java | 379 +++++++++++++ .../rx/internal/util/ObserverSubscriber.java | 46 ++ 10 files changed, 825 insertions(+), 444 deletions(-) create mode 100644 src/main/java/rx/internal/operators/EmptyObservableHolder.java create mode 100644 src/main/java/rx/internal/operators/NeverObservableHolder.java create mode 100644 src/main/java/rx/internal/operators/OnSubscribeLift.java create mode 100644 src/main/java/rx/internal/operators/OnSubscribeThrow.java create mode 100644 src/main/java/rx/internal/util/ActionNotificationObserver.java create mode 100644 src/main/java/rx/internal/util/ActionSubscriber.java create mode 100644 src/main/java/rx/internal/util/InternalObservableUtils.java create mode 100644 src/main/java/rx/internal/util/ObserverSubscriber.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index bd96ee4556..92e9ea56fa 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -19,7 +19,6 @@ import rx.exceptions.*; import rx.functions.*; import rx.internal.operators.*; -import rx.internal.producers.SingleProducer; import rx.internal.util.*; import rx.observables.*; import rx.observers.SafeSubscriber; @@ -191,11 +190,23 @@ public interface Operator extends Func1, Subscriber< */ @Experimental public R extend(Func1, ? extends R> conversion) { - return conversion.call(new OnSubscribe() { - @Override - public void call(Subscriber subscriber) { - subscriber.add(Observable.subscribe(subscriber, Observable.this)); - }}); + return conversion.call(new OnSubscribeExtend(this)); + } + + /** + * Transforms a OnSubscribe.call() into an Observable.subscribe() call. + *

Note: has to be in Observable because it calls the private subscribe() method + * @param the value type + */ + static final class OnSubscribeExtend implements OnSubscribe { + final Observable parent; + OnSubscribeExtend(Observable parent) { + this.parent = parent; + } + @Override + public void call(Subscriber subscriber) { + subscriber.add(subscribe(subscriber, parent)); + } } /** @@ -222,30 +233,7 @@ public void call(Subscriber subscriber) { * @see RxJava wiki: Implementing Your Own Operators */ public final Observable lift(final Operator operator) { - return new Observable(new OnSubscribe() { - @Override - public void call(Subscriber o) { - try { - Subscriber st = hook.onLift(operator).call(o); - try { - // new Subscriber created and being subscribed with so 'onStart' it - st.onStart(); - onSubscribe.call(st); - } catch (Throwable e) { - // localized capture of errors rather than it skipping all operators - // and ending up in the try/catch of the subscribe method which then - // prevents onErrorResumeNext and other similar approaches to error handling - Exceptions.throwIfFatal(e); - st.onError(e); - } - } catch (Throwable e) { - Exceptions.throwIfFatal(e); - // if the lift function failed all we can do is pass the error to the final Subscriber - // as we don't have the operator available to us - o.onError(e); - } - } - }); + return new Observable(new OnSubscribeLift(onSubscribe, operator)); } /** @@ -1253,16 +1241,6 @@ public static Observable defer(Func0> observableFactory) { return create(new OnSubscribeDefer(observableFactory)); } - /** Lazy initialized Holder for an empty observable which just emits onCompleted to any subscriber. */ - private static final class EmptyHolder { - final static Observable INSTANCE = create(new OnSubscribe() { - @Override - public void call(Subscriber subscriber) { - subscriber.onCompleted(); - } - }); - } - /** * Returns an Observable that emits no items to the {@link Observer} and immediately invokes its * {@link Observer#onCompleted onCompleted} method. @@ -1279,9 +1257,8 @@ public void call(Subscriber subscriber) { * {@link Observer}'s {@link Observer#onCompleted() onCompleted} method * @see ReactiveX operators documentation: Empty */ - @SuppressWarnings("unchecked") public static Observable empty() { - return (Observable) EmptyHolder.INSTANCE; + return EmptyObservableHolder.instance(); } /** @@ -1303,7 +1280,7 @@ public static Observable empty() { * @see ReactiveX operators documentation: Throw */ public static Observable error(Throwable exception) { - return new ThrowObservable(exception); + return create(new OnSubscribeThrow(exception)); } /** @@ -2740,7 +2717,7 @@ public final Observable> nest() { * @see ReactiveX operators documentation: Never */ public static Observable never() { - return NeverObservable.instance(); + return NeverObservableHolder.instance(); } /** @@ -2821,17 +2798,9 @@ public static Observable range(int start, int count, Scheduler schedule * @see ReactiveX operators documentation: SequenceEqual */ public static Observable sequenceEqual(Observable first, Observable second) { - return sequenceEqual(first, second, new Func2() { - @Override - public final Boolean call(T first, T second) { - if (first == null) { - return second == null; - } - return first.equals(second); - } - }); + return sequenceEqual(first, second, InternalObservableUtils.OBJECT_EQUALS); } - + /** * Returns an Observable that emits a Boolean value that indicates whether two Observable sequences are the * same by comparing the items emitted by each Observable pairwise based on the results of a specified @@ -3146,16 +3115,9 @@ public static Observable zip(Iterable> ws, FuncN< * @see ReactiveX operators documentation: Zip */ public static Observable zip(Observable> ws, final FuncN zipFunction) { - return ws.toList().map(new Func1>, Observable[]>() { - - @Override - public Observable[] call(List> o) { - return o.toArray(new Observable[o.size()]); - } - - }).lift(new OperatorZip(zipFunction)); + return ws.toList().map(InternalObservableUtils.TO_ARRAY).lift(new OperatorZip(zipFunction)); } - + /** * Returns an Observable that emits the results of a specified combiner function applied to combinations of * two items emitted, in sequence, by two other Observables. @@ -4016,15 +3978,7 @@ public final Observable cast(final Class klass) { * @see ReactiveX operators documentation: Reduce */ public final Observable collect(Func0 stateFactory, final Action2 collector) { - Func2 accumulator = new Func2() { - - @Override - public final R call(R state, T value) { - collector.call(state, value); - return state; - } - - }; + Func2 accumulator = InternalObservableUtils.createCollectorCaller(collector); /* * Discussion and confirmation of implementation at @@ -4147,12 +4101,7 @@ public final Observable concatWith(Observable t1) { * @see ReactiveX operators documentation: Contains */ public final Observable contains(final Object element) { - return exists(new Func1() { - @Override - public final Boolean call(T t1) { - return element == null ? t1 == null : element.equals(t1); - } - }); + return exists(InternalObservableUtils.equalsWith(element)); } /** @@ -4172,16 +4121,7 @@ public final Boolean call(T t1) { * @see #countLong() */ public final Observable count() { - return reduce(0, CountHolder.INSTANCE); - } - - private static final class CountHolder { - static final Func2 INSTANCE = new Func2() { - @Override - public final Integer call(Integer count, Object o) { - return count + 1; - } - }; + return reduce(0, InternalObservableUtils.COUNTER); } /** @@ -4203,18 +4143,9 @@ public final Integer call(Integer count, Object o) { * @see #count() */ public final Observable countLong() { - return reduce(0L, CountLongHolder.INSTANCE); + return reduce(0L, InternalObservableUtils.LONG_COUNTER); } - private static final class CountLongHolder { - static final Func2 INSTANCE = new Func2() { - @Override - public final Long call(Long count, Object o) { - return count + 1; - } - }; - } - /** * Returns an Observable that mirrors the source Observable, except that it drops items emitted by the * source Observable that are followed by another item within a computed debounce duration. @@ -4340,12 +4271,7 @@ public final Observable debounce(long timeout, TimeUnit unit, Scheduler sched */ public final Observable defaultIfEmpty(final T defaultValue) { //if empty switch to an observable that emits defaultValue and supports backpressure - return switchIfEmpty(Observable.create(new OnSubscribe() { - - @Override - public void call(Subscriber subscriber) { - subscriber.setProducer(new SingleProducer(subscriber, defaultValue)); - }})); + return switchIfEmpty(just(defaultValue)); } /** @@ -4676,21 +4602,9 @@ public final Observable distinctUntilChanged(Func1ReactiveX operators documentation: Do */ public final Observable doOnCompleted(final Action0 onCompleted) { - Observer observer = new Observer() { - @Override - public final void onCompleted() { - onCompleted.call(); - } - - @Override - public final void onError(Throwable e) { - } - - @Override - public final void onNext(T args) { - } - - }; + Action1 onNext = Actions.empty(); + Action1 onError = Actions.empty(); + Observer observer = new ActionSubscriber(onNext, onError, onCompleted); return lift(new OperatorDoOnEach(observer)); } @@ -4710,23 +4624,7 @@ public final void onNext(T args) { * @see ReactiveX operators documentation: Do */ public final Observable doOnEach(final Action1> onNotification) { - Observer observer = new Observer() { - @Override - public final void onCompleted() { - onNotification.call(Notification.createOnCompleted()); - } - - @Override - public final void onError(Throwable e) { - onNotification.call(Notification.createOnError(e)); - } - - @Override - public final void onNext(T v) { - onNotification.call(Notification.createOnNext(v)); - } - - }; + Observer observer = new ActionNotificationObserver(onNotification); return lift(new OperatorDoOnEach(observer)); } @@ -4772,21 +4670,9 @@ public final Observable doOnEach(Observer observer) { * @see ReactiveX operators documentation: Do */ public final Observable doOnError(final Action1 onError) { - Observer observer = new Observer() { - @Override - public final void onCompleted() { - } - - @Override - public final void onError(Throwable e) { - onError.call(e); - } - - @Override - public final void onNext(T args) { - } - - }; + Action1 onNext = Actions.empty(); + Action0 onCompleted = Actions.empty(); + Observer observer = new ActionSubscriber(onNext, onError, onCompleted); return lift(new OperatorDoOnEach(observer)); } @@ -4806,21 +4692,9 @@ public final void onNext(T args) { * @see ReactiveX operators documentation: Do */ public final Observable doOnNext(final Action1 onNext) { - Observer observer = new Observer() { - @Override - public final void onCompleted() { - } - - @Override - public final void onError(Throwable e) { - } - - @Override - public final void onNext(T args) { - onNext.call(args); - } - - }; + Action1 onError = Actions.empty(); + Action0 onCompleted = Actions.empty(); + Observer observer = new ActionSubscriber(onNext, onError, onCompleted); return lift(new OperatorDoOnEach(observer)); } @@ -4891,22 +4765,10 @@ public final Observable doOnSubscribe(final Action0 subscribe) { * @see #finallyDo(Action0) */ public final Observable doOnTerminate(final Action0 onTerminate) { - Observer observer = new Observer() { - @Override - public final void onCompleted() { - onTerminate.call(); - } - - @Override - public final void onError(Throwable e) { - onTerminate.call(); - } - - @Override - public final void onNext(T args) { - } - - }; + Action1 onNext = Actions.empty(); + Action1 onError = Actions.toAction1(onTerminate); + + Observer observer = new ActionSubscriber(onNext, onError, onTerminate); return lift(new OperatorDoOnEach(observer)); } @@ -6111,15 +5973,10 @@ public final Observable ignoreElements() { * @return an Observable that emits a Boolean * @see ReactiveX operators documentation: Contains */ - @SuppressWarnings("unchecked") public final Observable isEmpty() { - return lift((OperatorAny) HolderAnyForEmpty.INSTANCE); + return lift(InternalObservableUtils.IS_EMPTY); } - private static class HolderAnyForEmpty { - static final OperatorAny INSTANCE = new OperatorAny(UtilityFunctions.alwaysTrue(), true); - } - /** * Correlates the items emitted by two Observables based on overlapping durations. *

@@ -6459,12 +6316,7 @@ public final Observable observeOn(Scheduler scheduler, boolean delayError, in * @see ReactiveX operators documentation: Filter */ public final Observable ofType(final Class klass) { - return filter(new Func1() { - @Override - public final Boolean call(T t) { - return klass.isInstance(t); - } - }).cast(klass); + return filter(InternalObservableUtils.isInstanceOf(klass)).cast(klass); } /** @@ -6976,18 +6828,7 @@ public final Observable repeat(final long count, Scheduler scheduler) { * @see ReactiveX operators documentation: Repeat */ public final Observable repeatWhen(final Func1, ? extends Observable> notificationHandler, Scheduler scheduler) { - Func1>, ? extends Observable> dematerializedNotificationHandler = new Func1>, Observable>() { - @Override - public Observable call(Observable> notifications) { - return notificationHandler.call(notifications.map(new Func1, Void>() { - @Override - public Void call(Notification notification) { - return null; - } - })); - } - }; - return OnSubscribeRedo.repeat(this, dematerializedNotificationHandler, scheduler); + return OnSubscribeRedo.repeat(this, InternalObservableUtils.createRepeatDematerializer(notificationHandler), scheduler); } /** @@ -7010,18 +6851,7 @@ public Void call(Notification notification) { * @see ReactiveX operators documentation: Repeat */ public final Observable repeatWhen(final Func1, ? extends Observable> notificationHandler) { - Func1>, ? extends Observable> dematerializedNotificationHandler = new Func1>, Observable>() { - @Override - public Observable call(Observable> notifications) { - return notificationHandler.call(notifications.map(new Func1, Void>() { - @Override - public Void call(Notification notification) { - return null; - } - })); - } - }; - return OnSubscribeRedo.repeat(this, dematerializedNotificationHandler); + return OnSubscribeRedo.repeat(this, InternalObservableUtils.createRepeatDematerializer(notificationHandler)); } /** @@ -7072,12 +6902,7 @@ public final ConnectableObservable replay() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector) { - return OperatorReplay.multicastSelector(new Func0>() { - @Override - public ConnectableObservable call() { - return Observable.this.replay(); - } - }, selector); + return OperatorReplay.multicastSelector(InternalObservableUtils.createReplaySupplier(this), selector); } /** @@ -7108,12 +6933,7 @@ public ConnectableObservable call() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize) { - return OperatorReplay.multicastSelector(new Func0>() { - @Override - public ConnectableObservable call() { - return Observable.this.replay(bufferSize); - } - }, selector); + return OperatorReplay.multicastSelector(InternalObservableUtils.createReplaySupplier(this, bufferSize), selector); } /** @@ -7192,12 +7012,8 @@ public final Observable replay(Func1, ? extends Obs if (bufferSize < 0) { throw new IllegalArgumentException("bufferSize < 0"); } - return OperatorReplay.multicastSelector(new Func0>() { - @Override - public ConnectableObservable call() { - return Observable.this.replay(bufferSize, time, unit, scheduler); - } - }, selector); + return OperatorReplay.multicastSelector( + InternalObservableUtils.createReplaySupplier(this, bufferSize, time, unit, scheduler), selector); } /** @@ -7230,17 +7046,8 @@ public ConnectableObservable call() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(final Func1, ? extends Observable> selector, final int bufferSize, final Scheduler scheduler) { - return OperatorReplay.multicastSelector(new Func0>() { - @Override - public ConnectableObservable call() { - return Observable.this.replay(bufferSize); - } - }, new Func1, Observable>() { - @Override - public Observable call(Observable t) { - return selector.call(t).observeOn(scheduler); - } - }); + return OperatorReplay.multicastSelector(InternalObservableUtils.createReplaySupplier(this, bufferSize), + InternalObservableUtils.createReplaySelectorAndObserveOn(selector, scheduler)); } /** @@ -7308,12 +7115,8 @@ public final Observable replay(Func1, ? extends Obs * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final long time, final TimeUnit unit, final Scheduler scheduler) { - return OperatorReplay.multicastSelector(new Func0>() { - @Override - public ConnectableObservable call() { - return Observable.this.replay(time, unit, scheduler); - } - }, selector); + return OperatorReplay.multicastSelector( + InternalObservableUtils.createReplaySupplier(this, time, unit, scheduler), selector); } /** @@ -7343,17 +7146,9 @@ public ConnectableObservable call() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(final Func1, ? extends Observable> selector, final Scheduler scheduler) { - return OperatorReplay.multicastSelector(new Func0>() { - @Override - public ConnectableObservable call() { - return Observable.this.replay(); - } - }, new Func1, Observable>() { - @Override - public Observable call(Observable t) { - return selector.call(t).observeOn(scheduler); - } - }); + return OperatorReplay.multicastSelector( + InternalObservableUtils.createReplaySupplier(this), + InternalObservableUtils.createReplaySelectorAndObserveOn(selector, scheduler)); } /** @@ -7689,18 +7484,7 @@ public final Observable retry(Func2 predicate) { * @see ReactiveX operators documentation: Retry */ public final Observable retryWhen(final Func1, ? extends Observable> notificationHandler) { - Func1>, ? extends Observable> dematerializedNotificationHandler = new Func1>, Observable>() { - @Override - public Observable call(Observable> notifications) { - return notificationHandler.call(notifications.map(new Func1, Throwable>() { - @Override - public Throwable call(Notification notification) { - return notification.getThrowable(); - } - })); - } - }; - return OnSubscribeRedo. retry(this, dematerializedNotificationHandler); + return OnSubscribeRedo.retry(this, InternalObservableUtils.createRetryDematerializer(notificationHandler)); } /** @@ -7727,18 +7511,7 @@ public Throwable call(Notification notification) { * @see ReactiveX operators documentation: Retry */ public final Observable retryWhen(final Func1, ? extends Observable> notificationHandler, Scheduler scheduler) { - Func1>, ? extends Observable> dematerializedNotificationHandler = new Func1>, Observable>() { - @Override - public Observable call(Observable> notifications) { - return notificationHandler.call(notifications.map(new Func1, Throwable>() { - @Override - public Throwable call(Notification notification) { - return notification.getThrowable(); - } - })); - } - }; - return OnSubscribeRedo. retry(this, dematerializedNotificationHandler, scheduler); + return OnSubscribeRedo. retry(this, InternalObservableUtils.createRetryDematerializer(notificationHandler), scheduler); } /** @@ -8512,24 +8285,10 @@ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe() { - return subscribe(new Subscriber() { - - @Override - public final void onCompleted() { - // do nothing - } - - @Override - public final void onError(Throwable e) { - throw new OnErrorNotImplementedException(e); - } - - @Override - public final void onNext(T args) { - // do nothing - } - - }); + Action1 onNext = Actions.empty(); + Action1 onError = InternalObservableUtils.ERROR_NOT_IMPLEMENTED; + Action0 onCompleted = Actions.empty(); + return subscribe(new ActionSubscriber(onNext, onError, onCompleted)); } /** @@ -8554,24 +8313,9 @@ public final Subscription subscribe(final Action1 onNext) { throw new IllegalArgumentException("onNext can not be null"); } - return subscribe(new Subscriber() { - - @Override - public final void onCompleted() { - // do nothing - } - - @Override - public final void onError(Throwable e) { - throw new OnErrorNotImplementedException(e); - } - - @Override - public final void onNext(T args) { - onNext.call(args); - } - - }); + Action1 onError = InternalObservableUtils.ERROR_NOT_IMPLEMENTED; + Action0 onCompleted = Actions.empty(); + return subscribe(new ActionSubscriber(onNext, onError, onCompleted)); } /** @@ -8602,24 +8346,8 @@ public final Subscription subscribe(final Action1 onNext, final Actio throw new IllegalArgumentException("onError can not be null"); } - return subscribe(new Subscriber() { - - @Override - public final void onCompleted() { - // do nothing - } - - @Override - public final void onError(Throwable e) { - onError.call(e); - } - - @Override - public final void onNext(T args) { - onNext.call(args); - } - - }); + Action0 onCompleted = Actions.empty(); + return subscribe(new ActionSubscriber(onNext, onError, onCompleted)); } /** @@ -8635,7 +8363,7 @@ public final void onNext(T args) { * @param onError * the {@code Action1} you have designed to accept any error notification from the * Observable - * @param onComplete + * @param onCompleted * the {@code Action0} you have designed to accept a completion notification from the * Observable * @return a {@link Subscription} reference with which the {@link Observer} can stop receiving items before @@ -8646,35 +8374,18 @@ public final void onNext(T args) { * if {@code onComplete} is null * @see ReactiveX operators documentation: Subscribe */ - public final Subscription subscribe(final Action1 onNext, final Action1 onError, final Action0 onComplete) { + public final Subscription subscribe(final Action1 onNext, final Action1 onError, final Action0 onCompleted) { if (onNext == null) { throw new IllegalArgumentException("onNext can not be null"); } if (onError == null) { throw new IllegalArgumentException("onError can not be null"); } - if (onComplete == null) { + if (onCompleted == null) { throw new IllegalArgumentException("onComplete can not be null"); } - return subscribe(new Subscriber() { - - @Override - public final void onCompleted() { - onComplete.call(); - } - - @Override - public final void onError(Throwable e) { - onError.call(e); - } - - @Override - public final void onNext(T args) { - onNext.call(args); - } - - }); + return subscribe(new ActionSubscriber(onNext, onError, onCompleted)); } /** @@ -8695,24 +8406,7 @@ public final Subscription subscribe(final Observer observer) { if (observer instanceof Subscriber) { return subscribe((Subscriber)observer); } - return subscribe(new Subscriber() { - - @Override - public void onCompleted() { - observer.onCompleted(); - } - - @Override - public void onError(Throwable e) { - observer.onError(e); - } - - @Override - public void onNext(T t) { - observer.onNext(t); - } - - }); + return subscribe(new ObserverSubscriber(observer)); } /** @@ -8801,7 +8495,7 @@ public final Subscription subscribe(Subscriber subscriber) { return Observable.subscribe(subscriber, this); } - private static Subscription subscribe(Subscriber subscriber, Observable observable) { + static Subscription subscribe(Subscriber subscriber, Observable observable) { // validate and proceed if (subscriber == null) { throw new IllegalArgumentException("observer can not be null"); @@ -10601,66 +10295,4 @@ public final Observable zipWith(Iterable other, Func2 Observable zipWith(Observable other, Func2 zipFunction) { return (Observable)zip(this, other, zipFunction); } - - /** - * An Observable that never sends any information to an {@link Observer}. - * This Observable is useful primarily for testing purposes. - * - * @param - * the type of item (not) emitted by the Observable - */ - private static class NeverObservable extends Observable { - - private static class Holder { - static final NeverObservable INSTANCE = new NeverObservable(); - } - - /** - * Returns a singleton instance of NeverObservable (cast to the generic type). - * - * @return singleton instance of NeverObservable (cast to the generic type) - */ - @SuppressWarnings("unchecked") - static NeverObservable instance() { - return (NeverObservable) Holder.INSTANCE; - } - - NeverObservable() { - super(new OnSubscribe() { - - @Override - public void call(Subscriber observer) { - // do nothing - } - - }); - } - } - - /** - * An Observable that invokes {@link Observer#onError onError} when the {@link Observer} subscribes to it. - * - * @param - * the type of item (ostensibly) emitted by the Observable - */ - private static class ThrowObservable extends Observable { - - public ThrowObservable(final Throwable exception) { - super(new OnSubscribe() { - - /** - * Accepts an {@link Observer} and calls its {@link Observer#onError onError} method. - * - * @param observer - * an {@link Observer} of this Observable - */ - @Override - public void call(Subscriber observer) { - observer.onError(exception); - } - - }); - } - } - } diff --git a/src/main/java/rx/functions/Actions.java b/src/main/java/rx/functions/Actions.java index ea18eaed91..bbf9d0b151 100644 --- a/src/main/java/rx/functions/Actions.java +++ b/src/main/java/rx/functions/Actions.java @@ -432,4 +432,27 @@ public R call(Object... args) { } }; } + + /** + * Wraps an Action0 instance into an Action1 instance where the latter calls + * the former. + * @param action the action to call + * @return the new Action1 instance + */ + public static Action1 toAction1(Action0 action) { + return new Action1CallsAction0(action); + } + + static final class Action1CallsAction0 implements Action1 { + final Action0 action; + + public Action1CallsAction0(Action0 action) { + this.action = action; + } + + @Override + public void call(T t) { + action.call(); + } + } } diff --git a/src/main/java/rx/internal/operators/EmptyObservableHolder.java b/src/main/java/rx/internal/operators/EmptyObservableHolder.java new file mode 100644 index 0000000000..20091ca211 --- /dev/null +++ b/src/main/java/rx/internal/operators/EmptyObservableHolder.java @@ -0,0 +1,46 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rx.internal.operators; + +import rx.*; +import rx.Observable.OnSubscribe; + +/** + * Holds a singleton instance of an empty Observable which is stateless and completes + * the child subscriber immediately. + */ +public enum EmptyObservableHolder implements OnSubscribe { + INSTANCE + ; + + /** + * Returns a type-corrected singleton instance of the empty Observable. + * @return a type-corrected singleton instance of the empty Observable. + */ + @SuppressWarnings("unchecked") + public static Observable instance() { + return (Observable)EMPTY; + } + + /** The singleton instance. */ + static final Observable EMPTY = Observable.create(INSTANCE); + + @Override + public void call(Subscriber child) { + child.onCompleted(); + } +} diff --git a/src/main/java/rx/internal/operators/NeverObservableHolder.java b/src/main/java/rx/internal/operators/NeverObservableHolder.java new file mode 100644 index 0000000000..451f31c67b --- /dev/null +++ b/src/main/java/rx/internal/operators/NeverObservableHolder.java @@ -0,0 +1,45 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rx.internal.operators; + +import rx.*; +import rx.Observable.OnSubscribe; + +/** + * Holds a singleton instance of a never Observable which is stateless doesn't + * call any of the Subscriber's methods. + */ +public enum NeverObservableHolder implements OnSubscribe { + INSTANCE + ; + + /** + * Returns a type-corrected singleton instance of the never Observable. + * @return a type-corrected singleton instance of the never Observable. + */ + @SuppressWarnings("unchecked") + public static Observable instance() { + return (Observable)NEVER; + } + + /** The singleton instance. */ + static final Observable NEVER = Observable.create(INSTANCE); + + @Override + public void call(Subscriber child) { + } +} diff --git a/src/main/java/rx/internal/operators/OnSubscribeLift.java b/src/main/java/rx/internal/operators/OnSubscribeLift.java new file mode 100644 index 0000000000..ba6210f38b --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribeLift.java @@ -0,0 +1,65 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rx.internal.operators; + +import rx.Observable.*; +import rx.Subscriber; +import rx.exceptions.Exceptions; +import rx.plugins.*; + +/** + * Transforms the downstream Subscriber into a Subscriber via an operator + * callback and calls the parent OnSubscribe.call() method with it. + * @param the source value type + * @param the result value type + */ +public final class OnSubscribeLift implements OnSubscribe { + + static final RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); + + final OnSubscribe parent; + + final Operator operator; + + public OnSubscribeLift(OnSubscribe parent, Operator operator) { + this.parent = parent; + this.operator = operator; + } + + @Override + public void call(Subscriber o) { + try { + Subscriber st = hook.onLift(operator).call(o); + try { + // new Subscriber created and being subscribed with so 'onStart' it + st.onStart(); + parent.call(st); + } catch (Throwable e) { + // localized capture of errors rather than it skipping all operators + // and ending up in the try/catch of the subscribe method which then + // prevents onErrorResumeNext and other similar approaches to error handling + Exceptions.throwIfFatal(e); + st.onError(e); + } + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // if the lift function failed all we can do is pass the error to the final Subscriber + // as we don't have the operator available to us + o.onError(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/OnSubscribeThrow.java b/src/main/java/rx/internal/operators/OnSubscribeThrow.java new file mode 100644 index 0000000000..d39bba5939 --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribeThrow.java @@ -0,0 +1,46 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rx.internal.operators; + +import rx.*; +import rx.Observable.OnSubscribe; + +/** + * An Observable that invokes {@link Observer#onError onError} when the {@link Observer} subscribes to it. + * + * @param + * the type of item (ostensibly) emitted by the Observable + */ +public final class OnSubscribeThrow implements OnSubscribe { + + private final Throwable exception; + + public OnSubscribeThrow(Throwable exception) { + this.exception = exception; + } + + /** + * Accepts an {@link Observer} and calls its {@link Observer#onError onError} method. + * + * @param observer + * an {@link Observer} of this Observable + */ + @Override + public void call(Subscriber observer) { + observer.onError(exception); + } +} diff --git a/src/main/java/rx/internal/util/ActionNotificationObserver.java b/src/main/java/rx/internal/util/ActionNotificationObserver.java new file mode 100644 index 0000000000..162d9371b4 --- /dev/null +++ b/src/main/java/rx/internal/util/ActionNotificationObserver.java @@ -0,0 +1,48 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.internal.util; + +import rx.*; +import rx.functions.*; + +/** + * An Observer that forwards the onXXX method calls to a notification callback + * by transforming each signal type into Notifications. + * @param the value type + */ +public final class ActionNotificationObserver implements Observer { + + final Action1> onNotification; + + public ActionNotificationObserver(Action1> onNotification) { + this.onNotification = onNotification; + } + + @Override + public void onNext(T t) { + onNotification.call(Notification.createOnNext(t)); + } + + @Override + public void onError(Throwable e) { + onNotification.call(Notification.createOnError(e)); + } + + @Override + public void onCompleted() { + onNotification.call(Notification.createOnCompleted()); + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/ActionSubscriber.java b/src/main/java/rx/internal/util/ActionSubscriber.java new file mode 100644 index 0000000000..33a88fe5ed --- /dev/null +++ b/src/main/java/rx/internal/util/ActionSubscriber.java @@ -0,0 +1,51 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.internal.util; + +import rx.Subscriber; +import rx.functions.*; + +/** + * A Subscriber that forwards the onXXX method calls to callbacks. + * @param the value type + */ +public final class ActionSubscriber extends Subscriber { + + final Action1 onNext; + final Action1 onError; + final Action0 onCompleted; + + public ActionSubscriber(Action1 onNext, Action1 onError, Action0 onCompleted) { + this.onNext = onNext; + this.onError = onError; + this.onCompleted = onCompleted; + } + + @Override + public void onNext(T t) { + onNext.call(t); + } + + @Override + public void onError(Throwable e) { + onError.call(e); + } + + @Override + public void onCompleted() { + onCompleted.call(); + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/InternalObservableUtils.java b/src/main/java/rx/internal/util/InternalObservableUtils.java new file mode 100644 index 0000000000..0e8e3d6878 --- /dev/null +++ b/src/main/java/rx/internal/util/InternalObservableUtils.java @@ -0,0 +1,379 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rx.internal.util; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import rx.*; +import rx.Observable.Operator; +import rx.exceptions.OnErrorNotImplementedException; +import rx.functions.*; +import rx.internal.operators.OperatorAny; +import rx.observables.ConnectableObservable; + +/** + * Holder of named utility classes factored out from Observable to save + * source space and help with debugging with properly named objects. + */ +public enum InternalObservableUtils { + ; + + /** + * A BiFunction that expects an integer as its first parameter and returns +1. + */ + public static final PlusOneFunc2 COUNTER = new PlusOneFunc2(); + + static final class PlusOneFunc2 implements Func2 { + @Override + public Integer call(Integer count, Object o) { + return count + 1; + } + } + + /** + * A BiFunction that expects a long as its first parameter and returns +1. + */ + public static final PlusOneLongFunc2 LONG_COUNTER = new PlusOneLongFunc2(); + + static final class PlusOneLongFunc2 implements Func2 { + @Override + public Long call(Long count, Object o) { + return count + 1; + } + } + + /** + * A bifunction comparing two objects via null-safe equals. + */ + public static final ObjectEqualsFunc2 OBJECT_EQUALS = new ObjectEqualsFunc2(); + + static final class ObjectEqualsFunc2 implements Func2 { + @Override + public Boolean call(Object first, Object second) { + return first == second || (first != null && first.equals(second)); + } + } + + /** + * A function that converts a List of Observables into an array of Observables. + */ + public static final ToArrayFunc1 TO_ARRAY = new ToArrayFunc1(); + + static final class ToArrayFunc1 implements Func1>, Observable[]> { + @Override + public Observable[] call(List> o) { + return o.toArray(new Observable[o.size()]); + } + } + + /** + * Returns a Func1 that checks if its argument is null-safe equals with the given + * constant reference. + * @param other the other object to check against (nulls allowed) + * @return the comparison function + */ + public static Func1 equalsWith(Object other) { + return new EqualsWithFunc1(other); + } + + static final class EqualsWithFunc1 implements Func1 { + final Object other; + + public EqualsWithFunc1(Object other) { + this.other = other; + } + + @Override + public Boolean call(Object t) { + return t == other || (t != null && t.equals(other)); + } + } + + /** + * Returns a Func1 that checks if its argument is an instance of + * the supplied class. + * @param clazz the class to check against + * @return the comparison function + */ + public static Func1 isInstanceOf(Class clazz) { + return new IsInstanceOfFunc1(clazz); + } + + static final class IsInstanceOfFunc1 implements Func1 { + final Class clazz; + + public IsInstanceOfFunc1(Class other) { + this.clazz = other; + } + + @Override + public Boolean call(Object t) { + return clazz.isInstance(t); + } + } + + /** + * Returns a function that dematerializes the notification signal from an Observable and calls + * a notification handler with a null for non-terminal events. + * @param notificationHandler the handler to notify with nulls + * @return the Func1 instance + */ + public static final Func1>, Observable> createRepeatDematerializer(Func1, ? extends Observable> notificationHandler) { + return new RepeatNotificationDematerializer(notificationHandler); + } + + static final class RepeatNotificationDematerializer implements Func1>, Observable> { + + final Func1, ? extends Observable> notificationHandler; + + public RepeatNotificationDematerializer(Func1, ? extends Observable> notificationHandler) { + this.notificationHandler = notificationHandler; + } + + @Override + public Observable call(Observable> notifications) { + return notificationHandler.call(notifications.map(RETURNS_VOID)); + } + }; + + static final ReturnsVoidFunc1 RETURNS_VOID = new ReturnsVoidFunc1(); + + static final class ReturnsVoidFunc1 implements Func1 { + @Override + public Void call(Object t) { + return null; + } + } + + /** + * Creates a Func1 which calls the selector function with the received argument, applies an + * observeOn on the result and returns the resulting Observable. + * @param selector the selector function + * @param scheduler the scheduler to apply on the output of the selector + * @return the new Func1 instance + */ + public static Func1, Observable> createReplaySelectorAndObserveOn( + Func1, ? extends Observable> selector, + Scheduler scheduler) { + return new SelectorAndObserveOn(selector, scheduler); + } + + static final class SelectorAndObserveOn implements Func1, Observable> { + final Func1, ? extends Observable> selector; + final Scheduler scheduler; + + public SelectorAndObserveOn(Func1, ? extends Observable> selector, + Scheduler scheduler) { + super(); + this.selector = selector; + this.scheduler = scheduler; + } + + + + @Override + public Observable call(Observable t) { + return selector.call(t).observeOn(scheduler); + } + } + + /** + * Returns a function that dematerializes the notification signal from an Observable and calls + * a notification handler with the Throwable. + * @param notificationHandler the handler to notify with Throwables + * @return the Func1 instance + */ + public static final Func1>, Observable> createRetryDematerializer(Func1, ? extends Observable> notificationHandler) { + return new RetryNotificationDematerializer(notificationHandler); + } + + static final class RetryNotificationDematerializer implements Func1>, Observable> { + final Func1, ? extends Observable> notificationHandler; + + public RetryNotificationDematerializer(Func1, ? extends Observable> notificationHandler) { + this.notificationHandler = notificationHandler; + } + + @Override + public Observable call(Observable> notifications) { + return notificationHandler.call(notifications.map(ERROR_EXTRACTOR)); + } + } + + static final NotificationErrorExtractor ERROR_EXTRACTOR = new NotificationErrorExtractor(); + + static final class NotificationErrorExtractor implements Func1, Throwable> { + @Override + public Throwable call(Notification t) { + return t.getThrowable(); + } + } + + /** + * Returns a Func0 that supplies the ConnectableObservable returned by calling replay() on the source. + * @param source the source to call replay on by the supplier function + * @return the new Func0 instance + */ + public static Func0> createReplaySupplier(final Observable source) { + return new ReplaySupplierNoParams(source); + } + + private static final class ReplaySupplierNoParams implements Func0> { + private final Observable source; + + private ReplaySupplierNoParams(Observable source) { + this.source = source; + } + + @Override + public ConnectableObservable call() { + return source.replay(); + } + } + /** + * Returns a Func0 that supplies the ConnectableObservable returned by calling a parameterized replay() on the source. + * @param source the source to call replay on by the supplier function + * @param bufferSize + * the buffer size that limits the number of items the connectable observable can replay + * @return the new Func0 instance + */ + public static Func0> createReplaySupplier(final Observable source, final int bufferSize) { + return new ReplaySupplierBuffer(source, bufferSize); + } + + static final class ReplaySupplierBuffer implements Func0> { + private final Observable source; + private final int bufferSize; + + private ReplaySupplierBuffer(Observable source, int bufferSize) { + this.source = source; + this.bufferSize = bufferSize; + } + + @Override + public ConnectableObservable call() { + return source.replay(bufferSize); + } + } + + /** + * Returns a Func0 that supplies the ConnectableObservable returned by calling a parameterized replay() on the source. + * @param source the source to call replay on by the supplier function + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @return the new Func0 instance + */ + public static Func0> createReplaySupplier(final Observable source, final long time, final TimeUnit unit, final Scheduler scheduler) { + return new ReplaySupplierBufferTime(source, time, unit, scheduler); + } + + static final class ReplaySupplierBufferTime implements Func0> { + private final TimeUnit unit; + private final Observable source; + private final long time; + private final Scheduler scheduler; + + private ReplaySupplierBufferTime(Observable source, long time, TimeUnit unit, Scheduler scheduler) { + this.unit = unit; + this.source = source; + this.time = time; + this.scheduler = scheduler; + } + + @Override + public ConnectableObservable call() { + return source.replay(time, unit, scheduler); + } + } + + /** + * Returns a Func0 that supplies the ConnectableObservable returned by calling a parameterized replay() on the source. + * @param source the source to call replay on by the supplier function + * @param bufferSize + * the buffer size that limits the number of items the connectable observable 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} + * @return the new Func0 instance + */ + public static Func0> createReplaySupplier(final Observable source, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler) { + return new ReplaySupplierTime(source, bufferSize, time, unit, scheduler); + } + + static final class ReplaySupplierTime implements Func0> { + private final long time; + private final TimeUnit unit; + private final Scheduler scheduler; + private final int bufferSize; + private final Observable source; + + private ReplaySupplierTime(Observable source, int bufferSize, long time, TimeUnit unit, + Scheduler scheduler) { + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.bufferSize = bufferSize; + this.source = source; + } + + @Override + public ConnectableObservable call() { + return source.replay(bufferSize, time, unit, scheduler); + } + } + + /** + * Returns a Func2 which calls a collector with its parameters and returns the first (R) parameter. + * @param collector the collector action to call + * @return the new Func2 instance + */ + public static Func2 createCollectorCaller(Action2 collector) { + return new CollectorCaller(collector); + } + + static final class CollectorCaller implements Func2 { + final Action2 collector; + + public CollectorCaller(Action2 collector) { + this.collector = collector; + } + + @Override + public R call(R state, T value) { + collector.call(state, value); + return state; + } + } + + /** + * Throws an OnErrorNotImplementedException when called. + */ + public static final Action1 ERROR_NOT_IMPLEMENTED = new ErrorNotImplementedAction(); + + static final class ErrorNotImplementedAction implements Action1 { + @Override + public void call(Throwable t) { + throw new OnErrorNotImplementedException(t); + } + } + + public static final Operator IS_EMPTY = new OperatorAny(UtilityFunctions.alwaysTrue(), true); +} diff --git a/src/main/java/rx/internal/util/ObserverSubscriber.java b/src/main/java/rx/internal/util/ObserverSubscriber.java new file mode 100644 index 0000000000..dbf519c263 --- /dev/null +++ b/src/main/java/rx/internal/util/ObserverSubscriber.java @@ -0,0 +1,46 @@ +/** + * Copyright 2016 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rx.internal.util; + +import rx.*; + +/** + * Wraps an Observer and forwards the onXXX method calls to it. + * @param the value type + */ +public final class ObserverSubscriber extends Subscriber { + final Observer observer; + + public ObserverSubscriber(Observer observer) { + this.observer = observer; + } + + @Override + public void onNext(T t) { + observer.onNext(t); + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onCompleted() { + observer.onCompleted(); + } +} From 54c6ed2f3fe4fabb00baca3df0f97901dee5b587 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Thu, 14 Apr 2016 19:51:04 +0200 Subject: [PATCH 052/322] 1.x: ConcatMap vs ConcatMapIterable perf (#3853) --- .../rx/operators/ConcatMapInterablePerf.java | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/perf/java/rx/operators/ConcatMapInterablePerf.java diff --git a/src/perf/java/rx/operators/ConcatMapInterablePerf.java b/src/perf/java/rx/operators/ConcatMapInterablePerf.java new file mode 100644 index 0000000000..40ea778acb --- /dev/null +++ b/src/perf/java/rx/operators/ConcatMapInterablePerf.java @@ -0,0 +1,181 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rx.operators; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import rx.Observable; +import rx.functions.Func1; +import rx.jmh.LatchedObserver; + +/** + * Benchmark ConcatMapIterable. + *

+ * gradlew benchmarks "-Pjmh=-f 1 -tu s -bm thrpt -wi 5 -i 5 -r 1 .*ConcatMapInterablePerf.*" + *

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*ConcatMapInterablePerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class ConcatMapInterablePerf { + + @Param({"1", "10", "100", "1000", "10000", "100000", "1000000"}) + public int count; + + Observable justPlain; + + Observable justIterable; + + Observable rangePlain; + + Observable rangeIterable; + + Observable xrangePlain; + + Observable xrangeIterable; + + Observable chainPlain; + + Observable chainIterable; + + @Setup + public void setup() { + Integer[] values = new Integer[count]; + for (int i = 0; i < count; i++) { + values[i] = i; + } + + int c = 1000000 / count; + Integer[] xvalues = new Integer[c]; + for (int i = 0; i < c; i++) { + xvalues[i] = i; + } + + Observable source = Observable.from(values); + + justPlain = source.concatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.just(v); + } + }); + justIterable = source.concatMapIterable(new Func1>() { + @Override + public Iterable call(Integer v) { + return Collections.singleton(v); + } + }); + + final Observable range = Observable.range(1, 2); + final List xrange = Arrays.asList(1, 2); + + rangePlain = source.concatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return range; + } + }); + rangeIterable = source.concatMapIterable(new Func1>() { + @Override + public Iterable call(Integer v) { + return xrange; + } + }); + + final Observable xsource = Observable.from(xvalues); + final List xvaluesList = Arrays.asList(xvalues); + + xrangePlain = source.concatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return xsource; + } + }); + xrangeIterable = source.concatMapIterable(new Func1>() { + @Override + public Iterable call(Integer v) { + return xvaluesList; + } + }); + + chainPlain = xrangePlain.concatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.just(v); + } + }); + chainIterable = xrangeIterable.concatMapIterable(new Func1>() { + @Override + public Iterable call(Integer v) { + return Collections.singleton(v); + } + }); + } + + @Benchmark + public void justPlain(Blackhole bh) { + justPlain.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void justIterable(Blackhole bh) { + justIterable.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void rangePlain(Blackhole bh) { + rangePlain.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void rangeIterable(Blackhole bh) { + rangeIterable.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void xrangePlain(Blackhole bh) { + xrangePlain.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void xrangeIterable(Blackhole bh) { + xrangeIterable.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void chainPlain(Blackhole bh) { + chainPlain.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void chainIterable(Blackhole bh) { + chainIterable.subscribe(new LatchedObserver(bh)); + } + +} From a4298155f8bb1a5677495941a5d0f3ab6cbbbf77 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Mon, 18 Apr 2016 06:34:36 -0400 Subject: [PATCH 053/322] Provide factories for creating the default scheduler instances. (#3856) --- .../schedulers/CachedThreadScheduler.java | 7 ++-- .../schedulers/NewThreadScheduler.java | 36 +++++++++++++++++++ .../java/rx/plugins/RxJavaSchedulersHook.java | 29 +++++++++++++++ .../rx/schedulers/NewThreadScheduler.java | 18 +++------- src/main/java/rx/schedulers/Schedulers.java | 15 ++++---- 5 files changed, 81 insertions(+), 24 deletions(-) rename src/main/java/rx/{ => internal}/schedulers/CachedThreadScheduler.java (98%) create mode 100644 src/main/java/rx/internal/schedulers/NewThreadScheduler.java diff --git a/src/main/java/rx/schedulers/CachedThreadScheduler.java b/src/main/java/rx/internal/schedulers/CachedThreadScheduler.java similarity index 98% rename from src/main/java/rx/schedulers/CachedThreadScheduler.java rename to src/main/java/rx/internal/schedulers/CachedThreadScheduler.java index 31c6f9288f..8e131e58e5 100644 --- a/src/main/java/rx/schedulers/CachedThreadScheduler.java +++ b/src/main/java/rx/internal/schedulers/CachedThreadScheduler.java @@ -13,18 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package rx.schedulers; +package rx.internal.schedulers; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import rx.*; import rx.functions.Action0; -import rx.internal.schedulers.*; import rx.internal.util.RxThreadFactory; import rx.subscriptions.*; -/* package */final class CachedThreadScheduler extends Scheduler implements SchedulerLifecycle { +public final class CachedThreadScheduler extends Scheduler implements SchedulerLifecycle { private static final String WORKER_THREAD_NAME_PREFIX = "RxCachedThreadScheduler-"; static final RxThreadFactory WORKER_THREAD_FACTORY = new RxThreadFactory(WORKER_THREAD_NAME_PREFIX); @@ -234,4 +233,4 @@ public void setExpirationTime(long expirationTime) { this.expirationTime = expirationTime; } } -} \ No newline at end of file +} diff --git a/src/main/java/rx/internal/schedulers/NewThreadScheduler.java b/src/main/java/rx/internal/schedulers/NewThreadScheduler.java new file mode 100644 index 0000000000..19aaedf8db --- /dev/null +++ b/src/main/java/rx/internal/schedulers/NewThreadScheduler.java @@ -0,0 +1,36 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.internal.schedulers; + +import rx.Scheduler; +import rx.internal.util.RxThreadFactory; + +/** + * Schedules work on a new thread. + */ +public final class NewThreadScheduler extends Scheduler { + + private static final String THREAD_NAME_PREFIX = "RxNewThreadScheduler-"; + private static final RxThreadFactory THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX); + + public NewThreadScheduler() { + } + + @Override + public Worker createWorker() { + return new NewThreadWorker(THREAD_FACTORY); + } +} diff --git a/src/main/java/rx/plugins/RxJavaSchedulersHook.java b/src/main/java/rx/plugins/RxJavaSchedulersHook.java index 133cdc363a..38d09dede9 100644 --- a/src/main/java/rx/plugins/RxJavaSchedulersHook.java +++ b/src/main/java/rx/plugins/RxJavaSchedulersHook.java @@ -17,7 +17,12 @@ package rx.plugins; import rx.Scheduler; +import rx.annotations.Experimental; import rx.functions.Action0; +import rx.internal.schedulers.CachedThreadScheduler; +import rx.internal.schedulers.EventLoopsScheduler; +import rx.internal.schedulers.NewThreadScheduler; +import rx.schedulers.Schedulers; /** * This plugin class provides 2 ways to customize {@link Scheduler} functionality @@ -35,6 +40,30 @@ */ public class RxJavaSchedulersHook { + /** + * Create an instance of the default {@link Scheduler} used for {@link Schedulers#computation()}. + */ + @Experimental + public static Scheduler createComputationScheduler() { + return new EventLoopsScheduler(); + } + + /** + * Create an instance of the default {@link Scheduler} used for {@link Schedulers#io()}. + */ + @Experimental + public static Scheduler createIoScheduler() { + return new CachedThreadScheduler(); + } + + /** + * Create an instance of the default {@link Scheduler} used for {@link Schedulers#newThread()}. + */ + @Experimental + public static Scheduler createNewThreadScheduler() { + return new NewThreadScheduler(); + } + protected RxJavaSchedulersHook() { } diff --git a/src/main/java/rx/schedulers/NewThreadScheduler.java b/src/main/java/rx/schedulers/NewThreadScheduler.java index d8c87bd245..5dc6046268 100644 --- a/src/main/java/rx/schedulers/NewThreadScheduler.java +++ b/src/main/java/rx/schedulers/NewThreadScheduler.java @@ -16,28 +16,18 @@ package rx.schedulers; import rx.Scheduler; -import rx.internal.schedulers.NewThreadWorker; -import rx.internal.util.RxThreadFactory; /** - * Schedules work on a new thread. + * @deprecated This type was never publicly instantiable. Use {@link Schedulers#newThread()}. */ +@Deprecated public final class NewThreadScheduler extends Scheduler { - - private static final String THREAD_NAME_PREFIX = "RxNewThreadScheduler-"; - private static final RxThreadFactory THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX); - private static final NewThreadScheduler INSTANCE = new NewThreadScheduler(); - - /* package */static NewThreadScheduler instance() { - return INSTANCE; - } - private NewThreadScheduler() { - + throw new AssertionError(); } @Override public Worker createWorker() { - return new NewThreadWorker(THREAD_FACTORY); + return null; } } diff --git a/src/main/java/rx/schedulers/Schedulers.java b/src/main/java/rx/schedulers/Schedulers.java index 0ec2d3a273..f81235347c 100644 --- a/src/main/java/rx/schedulers/Schedulers.java +++ b/src/main/java/rx/schedulers/Schedulers.java @@ -19,6 +19,7 @@ import rx.internal.schedulers.*; import rx.internal.util.RxRingBuffer; import rx.plugins.RxJavaPlugins; +import rx.plugins.RxJavaSchedulersHook; import java.util.concurrent.Executor; @@ -34,25 +35,27 @@ public final class Schedulers { private static final Schedulers INSTANCE = new Schedulers(); private Schedulers() { - Scheduler c = RxJavaPlugins.getInstance().getSchedulersHook().getComputationScheduler(); + RxJavaSchedulersHook hook = RxJavaPlugins.getInstance().getSchedulersHook(); + + Scheduler c = hook.getComputationScheduler(); if (c != null) { computationScheduler = c; } else { - computationScheduler = new EventLoopsScheduler(); + computationScheduler = RxJavaSchedulersHook.createComputationScheduler(); } - Scheduler io = RxJavaPlugins.getInstance().getSchedulersHook().getIOScheduler(); + Scheduler io = hook.getIOScheduler(); if (io != null) { ioScheduler = io; } else { - ioScheduler = new CachedThreadScheduler(); + ioScheduler = RxJavaSchedulersHook.createIoScheduler(); } - Scheduler nt = RxJavaPlugins.getInstance().getSchedulersHook().getNewThreadScheduler(); + Scheduler nt = hook.getNewThreadScheduler(); if (nt != null) { newThreadScheduler = nt; } else { - newThreadScheduler = NewThreadScheduler.instance(); + newThreadScheduler = RxJavaSchedulersHook.createNewThreadScheduler(); } } From 6efc2cfa863ddf45f336249bf554d88ed8fa7f2d Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Tue, 19 Apr 2016 09:18:23 +0300 Subject: [PATCH 054/322] 1.x: Add Single.toCompletable() (#3866) Closes #3865. --- src/main/java/rx/Single.java | 25 +++++++++++++++++++++++++ src/test/java/rx/SingleTest.java | 23 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index b1e2ed4074..e954612b4f 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -2095,6 +2095,31 @@ public final Observable toObservable() { return asObservable(this); } + /** + * Returns a {@link Completable} that discards result of the {@link Single} (similar to + * {@link Observable#ignoreElements()}) and calls {@code onCompleted} when this source {@link Single} calls + * {@code onSuccess}. Error terminal event is propagated. + *

+ * + *

+ *
Scheduler:
+ *
{@code toCompletable} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @return a {@link Completable} that calls {@code onCompleted} on it's subscriber when the source {@link Single} + * calls {@code onSuccess}. + * @see ReactiveX documentation: + * Completable. + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical + * with the release number). + */ + @Experimental + public final Completable toCompletable() { + return Completable.fromSingle(this); + } + /** * Returns a Single that mirrors the source Single but applies a timeout policy for its emitted item. If it * is not emitted within the specified timeout duration, the resulting Single terminates and notifies diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 2952e22bfd..3760330eb4 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -812,6 +812,29 @@ public void testToObservable() { ts.assertCompleted(); } + @Test + public void toCompletableSuccess() { + Completable completable = Single.just("value").toCompletable(); + TestSubscriber testSubscriber = new TestSubscriber(); + completable.subscribe(testSubscriber); + + testSubscriber.assertCompleted(); + testSubscriber.assertNoValues(); + testSubscriber.assertNoErrors(); + } + + @Test + public void toCompletableError() { + TestException exception = new TestException(); + Completable completable = Single.error(exception).toCompletable(); + TestSubscriber testSubscriber = new TestSubscriber(); + completable.subscribe(testSubscriber); + + testSubscriber.assertError(exception); + testSubscriber.assertNoValues(); + testSubscriber.assertNotCompleted(); + } + @Test public void doOnErrorShouldNotCallActionIfNoErrorHasOccurred() { Action1 action = mock(Action1.class); From 8af2bc96808267b3e3d07021ab4e95a8467a05b9 Mon Sep 17 00:00:00 2001 From: Shixiong Zhu Date: Wed, 20 Apr 2016 10:03:04 -0700 Subject: [PATCH 055/322] Fix an unsubscribe race in EventLoopWorker (#3868) There is an unsubscribe race condition similar to #3842 in `CachedThreadScheduler.EventLoopWorker` and `EventLoopsScheduler.EventLoopWorker`. Image the following execution order: | Execution Order | thread 1 | thread 2 | | ------------- | ------------- | ------------- | | 1 | | submit task A | | 2 | | submit task B | | 3 | unsubscribe Worker | | | 4 | unsubscribe task A | | | 5 | | task A won't run as it's unsubscribed | | 6 | | run task B | | 7 | unsubscribe task B | | So task B will run but its previous task A will be skipped. This PR adds a check before running an action and moves `workerUnderConcurrentUnsubscribeShouldNotAllowLaterTasksToRunDueToUnsubscriptionRace` to `AbstractSchedulerConcurrencyTests` to test all concurrent schedulers. --- .../schedulers/CachedThreadScheduler.java | 12 +++++-- .../schedulers/EventLoopsScheduler.java | 27 +++++++++++++--- .../AbstractSchedulerConcurrencyTests.java | 31 +++++++++++++++++++ .../rx/schedulers/ExecutorSchedulerTest.java | 30 ------------------ 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/src/main/java/rx/internal/schedulers/CachedThreadScheduler.java b/src/main/java/rx/internal/schedulers/CachedThreadScheduler.java index 8e131e58e5..472ade9109 100644 --- a/src/main/java/rx/internal/schedulers/CachedThreadScheduler.java +++ b/src/main/java/rx/internal/schedulers/CachedThreadScheduler.java @@ -204,13 +204,21 @@ public Subscription schedule(Action0 action) { } @Override - public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { + public Subscription schedule(final Action0 action, long delayTime, TimeUnit unit) { if (innerSubscription.isUnsubscribed()) { // don't schedule, we are unsubscribed return Subscriptions.unsubscribed(); } - ScheduledAction s = threadWorker.scheduleActual(action, delayTime, unit); + ScheduledAction s = threadWorker.scheduleActual(new Action0() { + @Override + public void call() { + if (isUnsubscribed()) { + return; + } + action.call(); + } + }, delayTime, unit); innerSubscription.add(s); s.addParent(innerSubscription); return s; diff --git a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java index afcf7464ed..1eef164937 100644 --- a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java @@ -156,20 +156,37 @@ public boolean isUnsubscribed() { } @Override - public Subscription schedule(Action0 action) { + public Subscription schedule(final Action0 action) { if (isUnsubscribed()) { return Subscriptions.unsubscribed(); } - return poolWorker.scheduleActual(action, 0, null, serial); + return poolWorker.scheduleActual(new Action0() { + @Override + public void call() { + if (isUnsubscribed()) { + return; + } + action.call(); + } + }, 0, null, serial); } + @Override - public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { + public Subscription schedule(final Action0 action, long delayTime, TimeUnit unit) { if (isUnsubscribed()) { return Subscriptions.unsubscribed(); } - return poolWorker.scheduleActual(action, delayTime, unit, timed); + return poolWorker.scheduleActual(new Action0() { + @Override + public void call() { + if (isUnsubscribed()) { + return; + } + action.call(); + } + }, delayTime, unit, timed); } } @@ -178,4 +195,4 @@ static final class PoolWorker extends NewThreadWorker { super(threadFactory); } } -} \ No newline at end of file +} diff --git a/src/test/java/rx/schedulers/AbstractSchedulerConcurrencyTests.java b/src/test/java/rx/schedulers/AbstractSchedulerConcurrencyTests.java index 2eab100310..9e3fdf94d3 100644 --- a/src/test/java/rx/schedulers/AbstractSchedulerConcurrencyTests.java +++ b/src/test/java/rx/schedulers/AbstractSchedulerConcurrencyTests.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -423,4 +425,33 @@ public void call(Integer t) { assertEquals(5, count.get()); } + @Test + public void workerUnderConcurrentUnsubscribeShouldNotAllowLaterTasksToRunDueToUnsubscriptionRace() { + Scheduler scheduler = getScheduler(); + for (int i = 0; i < 1000; i++) { + Worker worker = scheduler.createWorker(); + final Queue q = new ConcurrentLinkedQueue(); + Action0 action1 = new Action0() { + + @Override + public void call() { + q.add(1); + } + }; + Action0 action2 = new Action0() { + + @Override + public void call() { + q.add(2); + } + }; + worker.schedule(action1); + worker.schedule(action2); + worker.unsubscribe(); + if (q.size() == 1 && q.poll() == 2) { + //expect a queue of 1,2 or 1. If queue is just 2 then we have a problem! + fail("wrong order on loop " + i); + } + } + } } diff --git a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java index 0777208cab..ed4e03213d 100644 --- a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java +++ b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java @@ -18,11 +18,9 @@ import static org.junit.Assert.*; import java.lang.management.*; -import java.util.Queue; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Assert; import org.junit.Test; import rx.*; @@ -277,32 +275,4 @@ public void call() { assertFalse(w.tasks.hasSubscriptions()); } - - @Test - public void workerUnderConcurrentUnsubscribeShouldNotAllowLaterTasksToRunDueToUnsubscriptionRace() { - Scheduler scheduler = Schedulers.from(Executors.newFixedThreadPool(1)); - for (int i = 0; i< 1000; i++) { - Worker worker = scheduler.createWorker(); - final Queue q = new ConcurrentLinkedQueue(); - Action0 action1 = new Action0() { - - @Override - public void call() { - q.add(1); - }}; - Action0 action2 = new Action0() { - - @Override - public void call() { - q.add(2); - }}; - worker.schedule(action1); - worker.schedule(action2); - worker.unsubscribe(); - if (q.size()==1 && q.poll() == 2) { - //expect a queue of 1,2 or 1. If queue is just 2 then we have a problem! - Assert.fail("wrong order on loop " + i); - } - } - } } From 3439dd8f73affa9b80277e72777ec3f15baafcfc Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 21 Apr 2016 03:15:37 +1000 Subject: [PATCH 056/322] ensure waiting tasks are cancelled on worker unsubscription (#3867) --- src/main/java/rx/schedulers/ExecutorScheduler.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/schedulers/ExecutorScheduler.java b/src/main/java/rx/schedulers/ExecutorScheduler.java index 70f217e49b..11d10214f9 100644 --- a/src/main/java/rx/schedulers/ExecutorScheduler.java +++ b/src/main/java/rx/schedulers/ExecutorScheduler.java @@ -100,14 +100,17 @@ public void run() { queue.clear(); return; } - ScheduledAction sa = queue.poll(); if (sa == null) { return; } - if (!sa.isUnsubscribed()) { - sa.run(); + if (!tasks.isUnsubscribed()) { + sa.run(); + } else { + queue.clear(); + return; + } } } while (wip.decrementAndGet() != 0); } From 95389c260fe93972421bfe8092bceb283d0db346 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Thu, 21 Apr 2016 03:06:35 -0400 Subject: [PATCH 057/322] Deprecate remaining public scheduler types. (#3871) --- .../internal/operators/OperatorObserveOn.java | 2 +- .../schedulers/ExecutorScheduler.java | 5 +- .../GenericScheduledExecutorService.java | 1 - .../schedulers/ImmediateScheduler.java | 73 ++++++++++ .../schedulers/SleepingAction.java | 2 +- .../schedulers/TrampolineScheduler.java | 131 ++++++++++++++++++ .../rx/schedulers/ImmediateScheduler.java | 55 +------- .../rx/schedulers/NewThreadScheduler.java | 1 + src/main/java/rx/schedulers/Schedulers.java | 14 +- .../rx/schedulers/TrampolineScheduler.java | 113 +-------------- .../schedulers/ExecutorSchedulerTest.java | 81 +---------- .../schedulers/ComputationSchedulerTests.java | 4 +- ...chedulerTest.java => IoSchedulerTest.java} | 8 +- .../java/rx/schedulers/SchedulerTests.java | 78 ++++++++++- 14 files changed, 316 insertions(+), 252 deletions(-) rename src/main/java/rx/{ => internal}/schedulers/ExecutorScheduler.java (98%) create mode 100644 src/main/java/rx/internal/schedulers/ImmediateScheduler.java rename src/main/java/rx/{ => internal}/schedulers/SleepingAction.java (98%) create mode 100644 src/main/java/rx/internal/schedulers/TrampolineScheduler.java rename src/test/java/rx/{ => internal}/schedulers/ExecutorSchedulerTest.java (70%) rename src/test/java/rx/schedulers/{CachedThreadSchedulerTest.java => IoSchedulerTest.java} (91%) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index 1720e1dfe2..f09a424020 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -22,11 +22,11 @@ import rx.Observable.Operator; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; +import rx.internal.schedulers.*; import rx.internal.util.*; import rx.internal.util.atomic.SpscAtomicArrayQueue; import rx.internal.util.unsafe.*; import rx.plugins.RxJavaPlugins; -import rx.schedulers.*; /** * Delivers events on the specified {@code Scheduler} asynchronously via an unbounded buffer. diff --git a/src/main/java/rx/schedulers/ExecutorScheduler.java b/src/main/java/rx/internal/schedulers/ExecutorScheduler.java similarity index 98% rename from src/main/java/rx/schedulers/ExecutorScheduler.java rename to src/main/java/rx/internal/schedulers/ExecutorScheduler.java index 11d10214f9..b40c0b4900 100644 --- a/src/main/java/rx/schedulers/ExecutorScheduler.java +++ b/src/main/java/rx/internal/schedulers/ExecutorScheduler.java @@ -13,14 +13,13 @@ * License for the specific language governing permissions and limitations under * the License. */ -package rx.schedulers; +package rx.internal.schedulers; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import rx.*; import rx.functions.Action0; -import rx.internal.schedulers.*; import rx.plugins.RxJavaPlugins; import rx.subscriptions.*; @@ -30,7 +29,7 @@ * Note that thread-hopping is unavoidable with this kind of Scheduler as we don't know about the underlying * threading behavior of the executor. */ -/* public */final class ExecutorScheduler extends Scheduler { +public final class ExecutorScheduler extends Scheduler { final Executor executor; public ExecutorScheduler(Executor executor) { this.executor = executor; diff --git a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java index e322945068..3bc60f076b 100644 --- a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java +++ b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java @@ -20,7 +20,6 @@ import rx.Scheduler; import rx.internal.util.RxThreadFactory; -import rx.schedulers.*; /** * A default {@link ScheduledExecutorService} that can be used for scheduling actions when a {@link Scheduler} implementation doesn't have that ability. diff --git a/src/main/java/rx/internal/schedulers/ImmediateScheduler.java b/src/main/java/rx/internal/schedulers/ImmediateScheduler.java new file mode 100644 index 0000000000..c3dd7a6f85 --- /dev/null +++ b/src/main/java/rx/internal/schedulers/ImmediateScheduler.java @@ -0,0 +1,73 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.internal.schedulers; + +import java.util.concurrent.TimeUnit; + +import rx.Scheduler; +import rx.Subscription; +import rx.functions.Action0; +import rx.subscriptions.BooleanSubscription; +import rx.subscriptions.Subscriptions; + +/** + * Executes work immediately on the current thread. + */ +public final class ImmediateScheduler extends Scheduler { + public static final ImmediateScheduler INSTANCE = new ImmediateScheduler(); + + private ImmediateScheduler() { + } + + @Override + public Worker createWorker() { + return new InnerImmediateScheduler(); + } + + private class InnerImmediateScheduler extends Scheduler.Worker implements Subscription { + + final BooleanSubscription innerSubscription = new BooleanSubscription(); + + InnerImmediateScheduler() { + } + + @Override + public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { + // since we are executing immediately on this thread we must cause this thread to sleep + long execTime = ImmediateScheduler.this.now() + unit.toMillis(delayTime); + + return schedule(new SleepingAction(action, this, execTime)); + } + + @Override + public Subscription schedule(Action0 action) { + action.call(); + return Subscriptions.unsubscribed(); + } + + @Override + public void unsubscribe() { + innerSubscription.unsubscribe(); + } + + @Override + public boolean isUnsubscribed() { + return innerSubscription.isUnsubscribed(); + } + + } + +} diff --git a/src/main/java/rx/schedulers/SleepingAction.java b/src/main/java/rx/internal/schedulers/SleepingAction.java similarity index 98% rename from src/main/java/rx/schedulers/SleepingAction.java rename to src/main/java/rx/internal/schedulers/SleepingAction.java index bb13734475..c41c01caf0 100644 --- a/src/main/java/rx/schedulers/SleepingAction.java +++ b/src/main/java/rx/internal/schedulers/SleepingAction.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package rx.schedulers; +package rx.internal.schedulers; import rx.Scheduler; import rx.functions.Action0; diff --git a/src/main/java/rx/internal/schedulers/TrampolineScheduler.java b/src/main/java/rx/internal/schedulers/TrampolineScheduler.java new file mode 100644 index 0000000000..94fea1964a --- /dev/null +++ b/src/main/java/rx/internal/schedulers/TrampolineScheduler.java @@ -0,0 +1,131 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.internal.schedulers; + +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import rx.Scheduler; +import rx.Subscription; +import rx.functions.Action0; +import rx.subscriptions.BooleanSubscription; +import rx.subscriptions.Subscriptions; + +/** + * Schedules work on the current thread but does not execute immediately. Work is put in a queue and executed + * after the current unit of work is completed. + */ +public final class TrampolineScheduler extends Scheduler { + public static final TrampolineScheduler INSTANCE = new TrampolineScheduler(); + + @Override + public Worker createWorker() { + return new InnerCurrentThreadScheduler(); + } + + private TrampolineScheduler() { + } + + private static class InnerCurrentThreadScheduler extends Scheduler.Worker implements Subscription { + + final AtomicInteger counter = new AtomicInteger(); + final PriorityBlockingQueue queue = new PriorityBlockingQueue(); + private final BooleanSubscription innerSubscription = new BooleanSubscription(); + private final AtomicInteger wip = new AtomicInteger(); + + InnerCurrentThreadScheduler() { + } + + @Override + public Subscription schedule(Action0 action) { + return enqueue(action, now()); + } + + @Override + public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { + long execTime = now() + unit.toMillis(delayTime); + + return enqueue(new SleepingAction(action, this, execTime), execTime); + } + + private Subscription enqueue(Action0 action, long execTime) { + if (innerSubscription.isUnsubscribed()) { + return Subscriptions.unsubscribed(); + } + final TimedAction timedAction = new TimedAction(action, execTime, counter.incrementAndGet()); + queue.add(timedAction); + + if (wip.getAndIncrement() == 0) { + do { + final TimedAction polled = queue.poll(); + if (polled != null) { + polled.action.call(); + } + } while (wip.decrementAndGet() > 0); + return Subscriptions.unsubscribed(); + } else { + // queue wasn't empty, a parent is already processing so we just add to the end of the queue + return Subscriptions.create(new Action0() { + + @Override + public void call() { + queue.remove(timedAction); + } + + }); + } + } + + @Override + public void unsubscribe() { + innerSubscription.unsubscribe(); + } + + @Override + public boolean isUnsubscribed() { + return innerSubscription.isUnsubscribed(); + } + + } + + private static final class TimedAction implements Comparable { + final Action0 action; + final Long execTime; + final int count; // In case if time between enqueueing took less than 1ms + + TimedAction(Action0 action, Long execTime, int count) { + this.action = action; + this.execTime = execTime; + this.count = count; + } + + @Override + public int compareTo(TimedAction that) { + int result = execTime.compareTo(that.execTime); + if (result == 0) { + return compare(count, that.count); + } + return result; + } + } + + // because I can't use Integer.compare from Java 7 + static int compare(int x, int y) { + return (x < y) ? -1 : ((x == y) ? 0 : 1); + } + +} diff --git a/src/main/java/rx/schedulers/ImmediateScheduler.java b/src/main/java/rx/schedulers/ImmediateScheduler.java index e480754a58..3be8edda4a 100644 --- a/src/main/java/rx/schedulers/ImmediateScheduler.java +++ b/src/main/java/rx/schedulers/ImmediateScheduler.java @@ -15,63 +15,20 @@ */ package rx.schedulers; -import java.util.concurrent.TimeUnit; - import rx.Scheduler; -import rx.Subscription; -import rx.functions.Action0; -import rx.subscriptions.BooleanSubscription; -import rx.subscriptions.Subscriptions; /** - * Executes work immediately on the current thread. + * @deprecated This type was never publicly instantiable. Use {@link Schedulers#immediate()}. */ +@Deprecated +@SuppressWarnings("unused") // Class was part of public API. public final class ImmediateScheduler extends Scheduler { - private static final ImmediateScheduler INSTANCE = new ImmediateScheduler(); - - /* package */static ImmediateScheduler instance() { - return INSTANCE; - } - - /* package accessible for unit tests */ImmediateScheduler() { + private ImmediateScheduler() { + throw new AssertionError(); } @Override public Worker createWorker() { - return new InnerImmediateScheduler(); + return null; } - - private class InnerImmediateScheduler extends Scheduler.Worker implements Subscription { - - final BooleanSubscription innerSubscription = new BooleanSubscription(); - - InnerImmediateScheduler() { - } - - @Override - public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { - // since we are executing immediately on this thread we must cause this thread to sleep - long execTime = ImmediateScheduler.this.now() + unit.toMillis(delayTime); - - return schedule(new SleepingAction(action, this, execTime)); - } - - @Override - public Subscription schedule(Action0 action) { - action.call(); - return Subscriptions.unsubscribed(); - } - - @Override - public void unsubscribe() { - innerSubscription.unsubscribe(); - } - - @Override - public boolean isUnsubscribed() { - return innerSubscription.isUnsubscribed(); - } - - } - } diff --git a/src/main/java/rx/schedulers/NewThreadScheduler.java b/src/main/java/rx/schedulers/NewThreadScheduler.java index 5dc6046268..192f8b29a8 100644 --- a/src/main/java/rx/schedulers/NewThreadScheduler.java +++ b/src/main/java/rx/schedulers/NewThreadScheduler.java @@ -21,6 +21,7 @@ * @deprecated This type was never publicly instantiable. Use {@link Schedulers#newThread()}. */ @Deprecated +@SuppressWarnings("unused") // Class was part of public API. public final class NewThreadScheduler extends Scheduler { private NewThreadScheduler() { throw new AssertionError(); diff --git a/src/main/java/rx/schedulers/Schedulers.java b/src/main/java/rx/schedulers/Schedulers.java index f81235347c..eae594ef08 100644 --- a/src/main/java/rx/schedulers/Schedulers.java +++ b/src/main/java/rx/schedulers/Schedulers.java @@ -61,21 +61,21 @@ private Schedulers() { /** * Creates and returns a {@link Scheduler} that executes work immediately on the current thread. - * - * @return an {@link ImmediateScheduler} instance + * + * @return a {@link Scheduler} that executes work immediately */ public static Scheduler immediate() { - return ImmediateScheduler.instance(); + return rx.internal.schedulers.ImmediateScheduler.INSTANCE; } /** * Creates and returns a {@link Scheduler} that queues work on the current thread to be executed after the * current work completes. - * - * @return a {@link TrampolineScheduler} instance + * + * @return a {@link Scheduler} that queues work on the current thread */ public static Scheduler trampoline() { - return TrampolineScheduler.instance(); + return rx.internal.schedulers.TrampolineScheduler.INSTANCE; } /** @@ -83,7 +83,7 @@ public static Scheduler trampoline() { *

* Unhandled errors will be delivered to the scheduler Thread's {@link java.lang.Thread.UncaughtExceptionHandler}. * - * @return a {@link NewThreadScheduler} instance + * @return a {@link Scheduler} that creates new threads */ public static Scheduler newThread() { return INSTANCE.newThreadScheduler; diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index 45bb18546c..5f708cdc23 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -15,121 +15,20 @@ */ package rx.schedulers; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - import rx.Scheduler; -import rx.Subscription; -import rx.functions.Action0; -import rx.subscriptions.BooleanSubscription; -import rx.subscriptions.Subscriptions; /** - * Schedules work on the current thread but does not execute immediately. Work is put in a queue and executed - * after the current unit of work is completed. + * @deprecated This type was never publicly instantiable. Use {@link Schedulers#trampoline()}. */ +@Deprecated +@SuppressWarnings("unused") // Class was part of public API. public final class TrampolineScheduler extends Scheduler { - private static final TrampolineScheduler INSTANCE = new TrampolineScheduler(); - - /* package */static TrampolineScheduler instance() { - return INSTANCE; + private TrampolineScheduler() { + throw new AssertionError(); } @Override public Worker createWorker() { - return new InnerCurrentThreadScheduler(); - } - - /* package accessible for unit tests */TrampolineScheduler() { + return null; } - - private static class InnerCurrentThreadScheduler extends Scheduler.Worker implements Subscription { - - final AtomicInteger counter = new AtomicInteger(); - final PriorityBlockingQueue queue = new PriorityBlockingQueue(); - private final BooleanSubscription innerSubscription = new BooleanSubscription(); - private final AtomicInteger wip = new AtomicInteger(); - - InnerCurrentThreadScheduler() { - } - - @Override - public Subscription schedule(Action0 action) { - return enqueue(action, now()); - } - - @Override - public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { - long execTime = now() + unit.toMillis(delayTime); - - return enqueue(new SleepingAction(action, this, execTime), execTime); - } - - private Subscription enqueue(Action0 action, long execTime) { - if (innerSubscription.isUnsubscribed()) { - return Subscriptions.unsubscribed(); - } - final TimedAction timedAction = new TimedAction(action, execTime, counter.incrementAndGet()); - queue.add(timedAction); - - if (wip.getAndIncrement() == 0) { - do { - final TimedAction polled = queue.poll(); - if (polled != null) { - polled.action.call(); - } - } while (wip.decrementAndGet() > 0); - return Subscriptions.unsubscribed(); - } else { - // queue wasn't empty, a parent is already processing so we just add to the end of the queue - return Subscriptions.create(new Action0() { - - @Override - public void call() { - queue.remove(timedAction); - } - - }); - } - } - - @Override - public void unsubscribe() { - innerSubscription.unsubscribe(); - } - - @Override - public boolean isUnsubscribed() { - return innerSubscription.isUnsubscribed(); - } - - } - - private static final class TimedAction implements Comparable { - final Action0 action; - final Long execTime; - final int count; // In case if time between enqueueing took less than 1ms - - TimedAction(Action0 action, Long execTime, int count) { - this.action = action; - this.execTime = execTime; - this.count = count; - } - - @Override - public int compareTo(TimedAction that) { - int result = execTime.compareTo(that.execTime); - if (result == 0) { - return compare(count, that.count); - } - return result; - } - } - - // because I can't use Integer.compare from Java 7 - static int compare(int x, int y) { - return (x < y) ? -1 : ((x == y) ? 0 : 1); - } - } diff --git a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java b/src/test/java/rx/internal/schedulers/ExecutorSchedulerTest.java similarity index 70% rename from src/test/java/rx/schedulers/ExecutorSchedulerTest.java rename to src/test/java/rx/internal/schedulers/ExecutorSchedulerTest.java index ed4e03213d..fad2435ce1 100644 --- a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java +++ b/src/test/java/rx/internal/schedulers/ExecutorSchedulerTest.java @@ -13,22 +13,21 @@ * License for the specific language governing permissions and limitations under * the License. */ -package rx.schedulers; +package rx.internal.schedulers; import static org.junit.Assert.*; -import java.lang.management.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; - import org.junit.Test; - import rx.*; import rx.Scheduler.Worker; import rx.functions.*; -import rx.internal.schedulers.NewThreadWorker; +import rx.internal.schedulers.ExecutorScheduler.ExecutorSchedulerWorker; import rx.internal.util.RxThreadFactory; -import rx.schedulers.ExecutorScheduler.ExecutorSchedulerWorker; +import rx.schedulers.AbstractSchedulerConcurrencyTests; +import rx.schedulers.SchedulerTests; +import rx.schedulers.Schedulers; public class ExecutorSchedulerTest extends AbstractSchedulerConcurrencyTests { @@ -49,72 +48,6 @@ public final void testHandledErrorIsNotDeliveredToThreadHandler() throws Interru SchedulerTests.testHandledErrorIsNotDeliveredToThreadHandler(getScheduler()); } - public static void testCancelledRetention(Scheduler.Worker w, boolean periodic) throws InterruptedException { - System.out.println("Wait before GC"); - Thread.sleep(1000); - - System.out.println("GC"); - System.gc(); - - Thread.sleep(1000); - - - MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); - MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); - long initial = memHeap.getUsed(); - - System.out.printf("Starting: %.3f MB%n", initial / 1024.0 / 1024.0); - - int n = 500 * 1000; - if (periodic) { - final CountDownLatch cdl = new CountDownLatch(n); - final Action0 action = new Action0() { - @Override - public void call() { - cdl.countDown(); - } - }; - for (int i = 0; i < n; i++) { - if (i % 50000 == 0) { - System.out.println(" -> still scheduling: " + i); - } - w.schedulePeriodically(action, 0, 1, TimeUnit.DAYS); - } - - System.out.println("Waiting for the first round to finish..."); - cdl.await(); - } else { - for (int i = 0; i < n; i++) { - if (i % 50000 == 0) { - System.out.println(" -> still scheduling: " + i); - } - w.schedule(Actions.empty(), 1, TimeUnit.DAYS); - } - } - - memHeap = memoryMXBean.getHeapMemoryUsage(); - long after = memHeap.getUsed(); - System.out.printf("Peak: %.3f MB%n", after / 1024.0 / 1024.0); - - w.unsubscribe(); - - System.out.println("Wait before second GC"); - Thread.sleep(NewThreadWorker.PURGE_FREQUENCY + 2000); - - System.out.println("Second GC"); - System.gc(); - - Thread.sleep(1000); - - memHeap = memoryMXBean.getHeapMemoryUsage(); - long finish = memHeap.getUsed(); - System.out.printf("After: %.3f MB%n", finish / 1024.0 / 1024.0); - - if (finish > initial * 5) { - fail(String.format("Tasks retained: %.3f -> %.3f -> %.3f", initial / 1024 / 1024.0, after / 1024 / 1024.0, finish / 1024 / 1024d)); - } - } - @Test(timeout = 30000) public void testCancelledTaskRetention() throws InterruptedException { ExecutorService exec = Executors.newSingleThreadExecutor(); @@ -122,14 +55,14 @@ public void testCancelledTaskRetention() throws InterruptedException { try { Scheduler.Worker w = s.createWorker(); try { - testCancelledRetention(w, false); + SchedulerTests.testCancelledRetention(w, false); } finally { w.unsubscribe(); } w = s.createWorker(); try { - testCancelledRetention(w, true); + SchedulerTests.testCancelledRetention(w, true); } finally { w.unsubscribe(); } diff --git a/src/test/java/rx/schedulers/ComputationSchedulerTests.java b/src/test/java/rx/schedulers/ComputationSchedulerTests.java index 7191f60015..7e9163e7c0 100644 --- a/src/test/java/rx/schedulers/ComputationSchedulerTests.java +++ b/src/test/java/rx/schedulers/ComputationSchedulerTests.java @@ -157,13 +157,13 @@ public final void testHandledErrorIsNotDeliveredToThreadHandler() throws Interru public void testCancelledTaskRetention() throws InterruptedException { Worker w = Schedulers.computation().createWorker(); try { - ExecutorSchedulerTest.testCancelledRetention(w, false); + SchedulerTests.testCancelledRetention(w, false); } finally { w.unsubscribe(); } w = Schedulers.computation().createWorker(); try { - ExecutorSchedulerTest.testCancelledRetention(w, true); + SchedulerTests.testCancelledRetention(w, true); } finally { w.unsubscribe(); } diff --git a/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java b/src/test/java/rx/schedulers/IoSchedulerTest.java similarity index 91% rename from src/test/java/rx/schedulers/CachedThreadSchedulerTest.java rename to src/test/java/rx/schedulers/IoSchedulerTest.java index 9abb52b7ec..a16a19a61c 100644 --- a/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java +++ b/src/test/java/rx/schedulers/IoSchedulerTest.java @@ -24,7 +24,7 @@ import rx.Scheduler.Worker; import rx.functions.*; -public class CachedThreadSchedulerTest extends AbstractSchedulerConcurrencyTests { +public class IoSchedulerTest extends AbstractSchedulerConcurrencyTests { @Override protected Scheduler getScheduler() { @@ -71,16 +71,16 @@ public final void testHandledErrorIsNotDeliveredToThreadHandler() throws Interru public void testCancelledTaskRetention() throws InterruptedException { Worker w = Schedulers.io().createWorker(); try { - ExecutorSchedulerTest.testCancelledRetention(w, false); + SchedulerTests.testCancelledRetention(w, false); } finally { w.unsubscribe(); } w = Schedulers.io().createWorker(); try { - ExecutorSchedulerTest.testCancelledRetention(w, true); + SchedulerTests.testCancelledRetention(w, true); } finally { w.unsubscribe(); } } -} \ No newline at end of file +} diff --git a/src/test/java/rx/schedulers/SchedulerTests.java b/src/test/java/rx/schedulers/SchedulerTests.java index a9146fafde..cd1d0a1912 100644 --- a/src/test/java/rx/schedulers/SchedulerTests.java +++ b/src/test/java/rx/schedulers/SchedulerTests.java @@ -4,14 +4,20 @@ import rx.Observable; import rx.Observer; import rx.Scheduler; +import rx.functions.Action0; +import rx.functions.Actions; +import rx.internal.schedulers.NewThreadWorker; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -final class SchedulerTests { +public final class SchedulerTests { private SchedulerTests() { // No instances. } @@ -23,7 +29,7 @@ private SchedulerTests() { * Schedulers which execute on a separate thread from their calling thread should exhibit this behavior. Schedulers * which execute on their calling thread may not. */ - static void testUnhandledErrorIsDeliveredToThreadHandler(Scheduler scheduler) throws InterruptedException { + public static void testUnhandledErrorIsDeliveredToThreadHandler(Scheduler scheduler) throws InterruptedException { Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler(); try { CapturingUncaughtExceptionHandler handler = new CapturingUncaughtExceptionHandler(); @@ -57,7 +63,7 @@ static void testUnhandledErrorIsDeliveredToThreadHandler(Scheduler scheduler) th * This is a companion test to {@link #testUnhandledErrorIsDeliveredToThreadHandler}, and is needed only for the * same Schedulers. */ - static void testHandledErrorIsNotDeliveredToThreadHandler(Scheduler scheduler) throws InterruptedException { + public static void testHandledErrorIsNotDeliveredToThreadHandler(Scheduler scheduler) throws InterruptedException { Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler(); try { CapturingUncaughtExceptionHandler handler = new CapturingUncaughtExceptionHandler(); @@ -88,6 +94,72 @@ static void testHandledErrorIsNotDeliveredToThreadHandler(Scheduler scheduler) t } } + public static void testCancelledRetention(Scheduler.Worker w, boolean periodic) throws InterruptedException { + System.out.println("Wait before GC"); + Thread.sleep(1000); + + System.out.println("GC"); + System.gc(); + + Thread.sleep(1000); + + + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); + long initial = memHeap.getUsed(); + + System.out.printf("Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + int n = 500 * 1000; + if (periodic) { + final CountDownLatch cdl = new CountDownLatch(n); + final Action0 action = new Action0() { + @Override + public void call() { + cdl.countDown(); + } + }; + for (int i = 0; i < n; i++) { + if (i % 50000 == 0) { + System.out.println(" -> still scheduling: " + i); + } + w.schedulePeriodically(action, 0, 1, TimeUnit.DAYS); + } + + System.out.println("Waiting for the first round to finish..."); + cdl.await(); + } else { + for (int i = 0; i < n; i++) { + if (i % 50000 == 0) { + System.out.println(" -> still scheduling: " + i); + } + w.schedule(Actions.empty(), 1, TimeUnit.DAYS); + } + } + + memHeap = memoryMXBean.getHeapMemoryUsage(); + long after = memHeap.getUsed(); + System.out.printf("Peak: %.3f MB%n", after / 1024.0 / 1024.0); + + w.unsubscribe(); + + System.out.println("Wait before second GC"); + Thread.sleep(NewThreadWorker.PURGE_FREQUENCY + 2000); + + System.out.println("Second GC"); + System.gc(); + + Thread.sleep(1000); + + memHeap = memoryMXBean.getHeapMemoryUsage(); + long finish = memHeap.getUsed(); + System.out.printf("After: %.3f MB%n", finish / 1024.0 / 1024.0); + + if (finish > initial * 5) { + fail(String.format("Tasks retained: %.3f -> %.3f -> %.3f", initial / 1024 / 1024.0, after / 1024 / 1024.0, finish / 1024 / 1024d)); + } + } + private static final class CapturingObserver implements Observer { CountDownLatch completed = new CountDownLatch(1); int errorCount = 0; From 3f6c4fd164801fd994034f99e663dc17163617c9 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Thu, 21 Apr 2016 21:58:08 +0200 Subject: [PATCH 058/322] 1.x: fix from(Iterable) error handling of Iterable/Iterator (#3862) * 1.x: fix from(Iterable) error handling of Iterable/Iterator * Check dead-on-arrival Subscribers * Use n again to avoid a potential cache-miss with get() --- .../operators/OnSubscribeFromIterable.java | 130 +++++++--- .../OnSubscribeFromIterableTest.java | 244 +++++++++++++++++- 2 files changed, 325 insertions(+), 49 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java index b94e35c35c..9389c4480c 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java +++ b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java @@ -20,6 +20,7 @@ import rx.*; import rx.Observable.OnSubscribe; +import rx.exceptions.Exceptions; /** * Converts an {@code Iterable} sequence into an {@code Observable}. @@ -42,11 +43,25 @@ public OnSubscribeFromIterable(Iterable iterable) { @Override public void call(final Subscriber o) { - final Iterator it = is.iterator(); - if (!it.hasNext() && !o.isUnsubscribed()) - o.onCompleted(); - else - o.setProducer(new IterableProducer(o, it)); + final Iterator it; + boolean b; + + try { + it = is.iterator(); + + b = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwOrReport(ex, o); + return; + } + + if (!o.isUnsubscribed()) { + if (!b) { + o.onCompleted(); + } else { + o.setProducer(new IterableProducer(o, it)); + } + } } private static final class IterableProducer extends AtomicLong implements Producer { @@ -81,38 +96,58 @@ void slowpath(long n) { final Iterator it = this.it; long r = n; - while (true) { - /* - * This complicated logic is done to avoid touching the - * volatile `requested` value during the loop itself. If - * it is touched during the loop the performance is - * impacted significantly. - */ - long numToEmit = r; - while (true) { + long e = 0; + + for (;;) { + while (e != r) { if (o.isUnsubscribed()) { return; - } else if (it.hasNext()) { - if (--numToEmit >= 0) { - o.onNext(it.next()); - } else - break; - } else if (!o.isUnsubscribed()) { - o.onCompleted(); + } + + T value; + + try { + value = it.next(); + } catch (Throwable ex) { + Exceptions.throwOrReport(ex, o); return; - } else { - // is unsubscribed + } + + o.onNext(value); + + if (o.isUnsubscribed()) { return; } + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwOrReport(ex, o); + return; + } + + if (!b) { + if (!o.isUnsubscribed()) { + o.onCompleted(); + } + return; + } + + e++; } - r = addAndGet(-r); - if (r == 0L) { - // we're done emitting the number requested so - // return - return; + + r = get(); + if (e == r) { + r = BackpressureUtils.produced(this, e); + if (r == 0L) { + break; + } + e = 0L; } - } + } void fastpath() { @@ -120,16 +155,39 @@ void fastpath() { final Subscriber o = this.o; final Iterator it = this.it; - while (true) { + for (;;) { if (o.isUnsubscribed()) { return; - } else if (it.hasNext()) { - o.onNext(it.next()); - } else if (!o.isUnsubscribed()) { - o.onCompleted(); + } + + T value; + + try { + value = it.next(); + } catch (Throwable ex) { + Exceptions.throwOrReport(ex, o); + return; + } + + o.onNext(value); + + if (o.isUnsubscribed()) { return; - } else { - // is unsubscribed + } + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwOrReport(ex, o); + return; + } + + if (!b) { + if (!o.isUnsubscribed()) { + o.onCompleted(); + } return; } } diff --git a/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java b/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java index a75e733951..00956b9cae 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java @@ -15,28 +15,21 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; import rx.Observable; import rx.Observer; import rx.Subscriber; +import rx.exceptions.TestException; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -313,5 +306,230 @@ public void onNext(Integer t) { }); assertFalse(called.get()); } + + @Test + public void getIteratorThrows() { + Iterable it = new Iterable() { + @Override + public Iterator iterator() { + throw new TestException("Forced failure"); + } + }; + + TestSubscriber ts = new TestSubscriber(); + + Observable.from(it).unsafeSubscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void hasNextThrowsImmediately() { + Iterable it = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + throw new TestException("Forced failure"); + } + + @Override + public Integer next() { + return null; + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber ts = new TestSubscriber(); + + Observable.from(it).unsafeSubscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void hasNextThrowsSecondTimeFastpath() { + Iterable it = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + int count; + @Override + public boolean hasNext() { + if (++count >= 2) { + throw new TestException("Forced failure"); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber ts = new TestSubscriber(); + + Observable.from(it).unsafeSubscribe(ts); + + ts.assertValues(1); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void hasNextThrowsSecondTimeSlowpath() { + Iterable it = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + int count; + @Override + public boolean hasNext() { + if (++count >= 2) { + throw new TestException("Forced failure"); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber ts = new TestSubscriber(5); + + Observable.from(it).unsafeSubscribe(ts); + + ts.assertValues(1); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + @Test + public void nextThrowsFastpath() { + Iterable it = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException("Forced failure"); + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber ts = new TestSubscriber(); + + Observable.from(it).unsafeSubscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void nextThrowsSlowpath() { + Iterable it = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException("Forced failure"); + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber ts = new TestSubscriber(5); + + Observable.from(it).unsafeSubscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void deadOnArrival() { + Iterable it = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public Integer next() { + throw new NoSuchElementException(); + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber ts = new TestSubscriber(5); + ts.unsubscribe(); + + Observable.from(it).unsafeSubscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + } } From 7b11b1c5e47e22c9f24fc0ee5c3c9e89bb091903 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 23 Apr 2016 08:45:04 +1000 Subject: [PATCH 059/322] remove unused field baseCapacity (#3874) --- .../rx/internal/operators/OperatorOnBackpressureBuffer.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java index 4f66bbb4d7..04e6e81be9 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -110,7 +110,6 @@ public Subscriber call(final Subscriber child) { private static final class BufferSubscriber extends Subscriber implements BackpressureDrainManager.BackpressureQueueCallback { // TODO get a different queue implementation private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); - private final Long baseCapacity; private final AtomicLong capacity; private final Subscriber child; private final AtomicBoolean saturated = new AtomicBoolean(false); @@ -122,7 +121,6 @@ private static final class BufferSubscriber extends Subscriber implements public BufferSubscriber(final Subscriber child, Long capacity, Action0 onOverflow, BackpressureOverflow.Strategy overflowStrategy) { this.child = child; - this.baseCapacity = capacity; this.capacity = capacity != null ? new AtomicLong(capacity) : null; this.onOverflow = onOverflow; this.manager = new BackpressureDrainManager(this); From 9b0887017a09e3c1ee7e9c2df066d8b2da30db48 Mon Sep 17 00:00:00 2001 From: Daniel Lew Date: Fri, 29 Apr 2016 00:56:57 -0500 Subject: [PATCH 060/322] throwIfFatal() now throws OnCompletedFailedException (#3886) Otherwise, if there's an error in onCompleted, the exception is swallowed and unreported. --- src/main/java/rx/exceptions/Exceptions.java | 3 +++ .../java/rx/exceptions/ExceptionsTest.java | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index 6c37167c3e..f427018f53 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -61,6 +61,7 @@ public static RuntimeException propagate(Throwable t) { *